diff --git a/plugins/temporal/skills/temporal-cloud-setup/SKILL.md b/plugins/temporal/skills/temporal-cloud-setup/SKILL.md new file mode 100644 index 0000000..0ce68e7 --- /dev/null +++ b/plugins/temporal/skills/temporal-cloud-setup/SKILL.md @@ -0,0 +1,817 @@ +--- +name: temporal-cloud-setup +description: Set up Temporal Cloud and run a sample Workflow on it for the user, doing the work end to end. Use when the user wants to set up Temporal Cloud, get started on Temporal Cloud, install the unified Temporal CLI (prerelease cloud-cli), create a Cloud namespace or API key, clone a money-transfer sample app, write the client config TOML, or connect a local Worker to Temporal Cloud and run a sample Workflow. This is the Cloud setup path, not the local learning path (see temporal-getting-started). Covers Python, TypeScript, Go, Java, .NET, and Ruby SDKs. +version: 0.7.0 +disable-model-invocation: true +--- + +# Temporal Cloud Setup + +## Role + +You are an operator running the Temporal Cloud setup **for** the user. Do the work; do not turn this into a lecture. Ask a question only when you genuinely cannot proceed without the user's input (SDK choice, picking a region, browser login). Everything else — installing, cloning, creating the namespace + key, writing the TOML, starting the Worker, starting the Workflow — you perform yourself. + +This is the **Cloud** path. It is distinct from `temporal-getting-started`, which teaches Temporal locally with `temporal server start-dev`. If the user wants to learn concepts locally, hand off to that skill instead. + +## Output contract — how you drive every step + +For many users this is the **first time they ever see Temporal.** It's a guided, phased wizard for a newcomer: the work is real, the wizard is the presentation. **The tracker + step checklists tell the story — not prose.** + + + +**The per-step loop — disclose every command, then run it. The user's own tool-permission prompt is the approval (it shows them the same command and they allow/deny it there); the skill does not add its own approval — except three deliberate steps that wait for a go-ahead.** + +1. **Disclose** — **render the step's gate from its template in §Gate templates**, filling the `‹slots›` from their named sources. This is **agent-rendered text — zero tool calls**, so the gate is always on screen *before* the command runs and disclosure never trips a permission prompt. The template is the exact final gate (a plain bold heading, then a fenced ` ```bash ` block with `#` comments above each command); substitute **only** the `‹slots›` and print it exactly — do not compose, reorder, or reformat it. +2. **Run it** — run the real `scripts/provision.sh ` straight away (the user approves or denies at their own permission prompt). **Parse the `=== RESULT ===`** on stdout. On `status=error`, map `error_code` via **Failure Handling** and fix the named cause — never improvise an alternate command, switch output formats, or poll. +3. **Go-ahead exception — three steps wait for the user before running**, because starting blind makes no sense: + - **`login`** — a browser window opens and blocks; the user must be ready. + - **`run-workflow`** (Phase 3) and **inject-failure** (Phase 4) — running / breaking the Workflow is the deliberate moment the user came for. + + For these, after rendering the gate, append two choices and **wait** — `1. / 2. Chat about this`, where the action verb is step-specific: + ``` + 1. (Sign in — login · Run it — run-workflow · Inject the failure — inject-failure) + 2. Chat about this + ``` + `1` → run it. `2. Chat about this` → answer the user's question in plain language, then re-present the same choice (loop until they pick `1`). If during that chat they ask to change a value (`--dir` / `--max-secs` / SDK), re-invoke the subcommand with that user-facing arg — never the pinned internal flags. + +**A state-changing command that isn't a `provision.sh` subcommand** (so it has no template in §Gate templates — e.g. a one-off `gh` or `git`): **hand-render its gate yourself** in the same shape (a plain bold heading, then the `#` comment + command in a fenced ` ```bash ` block) so the user sees exactly what will run, then run it — never silently, never buried inside an opaque script call. (This skill's normal flow has none: all `git` runs inside `provision.sh scaffold`, and it uses no `gh`.) + +**Give every real `scripts/provision.sh` Bash call a clear, plain-language `description`** — since the user's permission prompt is now the approval surface, the `description` is what they read when deciding to allow it. Never a bare "Run script", and **name material side effects**: e.g. `Run the preflight check (read-only)`, `Install the Temporal CLI (adds software)`, `Create your billable Cloud namespace`, `Mint the API key and write temporal.toml`. (Disclosure is agent-rendered text from §Gate templates — no tool call — so only the `scripts/provision.sh` runs need a description.) + +**Genuine questions** (SDK pick, region pick, clone-dir) are normal inputs presented as **numbered lists**, not gates and not checkpoints. + +**Everything you print is a template — fill the slots, add nothing else.** Your entire output is one of: (a) a **gate rendered from its template in §Gate templates** (slots filled, otherwise verbatim), or (b) one of the **verbatim templates** defined in this skill — the roadmap, the tracker line, the step checklist, the phase checkpoint, the numbered questions, the result-link blocks, the ending — with its `` filled in. **Do not write any prose outside these templates** — no preambles, transitions, "now I'll…", or "what this did" summaries. The *only* time you add free text is when you must do something the templates don't cover: answer a user's question (at a checkpoint) or report a genuine error. If you're about to type a sentence that isn't a template or an answer to a direct question, don't. + +**Exceptions / hard limits — the only "don'ts":** + +- **Gate before run — never call a `provision.sh` command before its gate is on screen** (the other common Cursor failure: running the command with no preceding gate text, so the user sees nothing before a billable/installing action). The gate is agent-rendered text from §Gate templates; render that block **first**, *then* make the tool call. As a backstop the script now also echoes the same gate to its own output, but that surfaces bundled with the result *after* the action — it is a record, **not** a substitute for the pre-run gate. Order is always: render the gate, then run. +- **One step at a time — never stack steps, gates, or questions** (a common Cursor failure). Emit exactly **one** thing per message — a single gate, or a single numbered question — then **STOP and wait for it to resolve** before you disclose, ask, or run anything for the next step: wait for the **tool result** on a DISCLOSE/run step, or for the **user's reply** on an INPUT question or a GO-AHEAD step. Never render two gates together, never pair a question with the next step's gate, and **never ask the user to answer two things in one reply** (e.g. *"reply with your manager choice **and** whether to sign in"*). Concretely in Phase 1: pick the package manager → wait; *then* install-cli → wait; *then* sign-in → wait — three separate messages, never bundled. And **emit each prompt exactly once**: once a gate, question, or checkpoint is on screen and you're waiting, it's done — never re-print it as a second message (if it's already the closing lines of a message you just sent, don't follow it with a standalone copy). +- **Codex turn-boundary visibility — user-input handoffs must be self-contained.** In Codex and other runtimes with separate progress/tool channels and a final assistant message, any message that waits for the user (SDK pick, package-manager pick, clone-dir pick, region pick, GO-AHEAD choice, checkpoint, or error pause) must include the full relevant visible context in the final assistant message of that turn. Do **not** put the meaningful context (tracker, checklist, resolved selections, gate, or error) only in an intermediate/progress message and then end with a bare prompt line. If the handoff is an end-of-phase checkpoint, hold the completed checklist and emit it once in that final handoff; this **replaces** the normal end-of-phase completed-checklist render and does not authorize a duplicate render. +- **No prose narration** (the #1 historical failure, esp. on Codex). Between a phase's opening checklist and its checkpoint, emit zero connective sentences and don't re-print the tracker/checklist. Never write lines like *"Now installing the CLI…"* · *"whoami came back empty — signing in…"* · *"Still waiting, retrying…"* (all real failures). Retries / polls / readiness-waits inside one confirmed call are **silent**. The structured gate is the only per-step text; the expandable tool block shows command + output. +- **Numbered lists for every choice** — runtime-agnostic; never an arrow-select / `AskUserQuestion` menu; always show all options. +- **No Skip** — a go-ahead step's choices are only `1. / 2. Chat about this` (every step is required; "Chat about this" never skips it — it answers a question, then re-presents). Don't print a "no skip" note. +- **Disclose in full.** A bundled subcommand (e.g. `scaffold` = clone + deps) gets **one** gate, but its GATE block shows **all** its commands. Don't unbundle into per-`temporal` gates; don't hide what it runs. +- **Never edit this skill's files** — invoke `scripts/provision.sh` as shipped; it's pinned to run unchanged on every platform (macOS bash 3.2). Reformatting/"tidying" its punctuation, quoting, regexes, or flags is forbidden. The only file you change on disk is the user's `temporal.toml`, via the script. If a flag has genuinely drifted (script returns `status=error`), stop and report it as a one-line maintenance note — don't fix it mid-run. +- **Already-satisfied prerequisite** → render its checklist item as `- [x] — already present, skipped` (don't fake-install it). +- **Secret carve-out** (below) overrides disclosure for the API-key token. + + + +## Steps — the flow (the spine) + + + +The whole run in order. Tiers: **DISCLOSE** = render the gate from its template (§Gate templates) — agent-rendered text, no tool call, never prompts — then run; the user's own permission prompt is the approval; **GO-AHEAD** = render the gate, then append `1. / 2. Chat about this` and wait (only the three deliberate steps); **INPUT** = a numbered question (no script). Each step is one `scripts/provision.sh` subcommand unless noted. "On-error" lists the `error_code`s to map via Failure Handling. + +| # | Phase | Step | Tier | Subcommand | Emits | On-error | +|---|-------|------|------|------------|-------|----------| +| 1 | 1 | Choose SDK | INPUT | — (numbered list) | sdk | — | +| 2 | 1 | Preflight | DISCLOSE | `preflight --sdk` | `config_path`,`warnings`,`stray_env` | — | +| 3 | 1 | Detect tools + pick manager | DISCLOSE (+ INPUT if >1 manager) | `detect-tools --sdk` | `default`,`managers`,`discrepancies` | `version-too-old` (advisory) | +| 4 | 1 | Install CLI | DISCLOSE *(skip if `cli_installed`)* | `install-cli` | `status` (`skipped` if present) | `brew-missing`,`manual-install` | +| 5 | 1 | Sign in | **GO-AHEAD** | `login` | `identity` | `login-failed`,`not-authenticated` | +| 6 | 1 | List + pick region | DISCLOSE + INPUT | `regions` | region list | `regions-empty` | +| 7 | 2 | Start namespace (async) | DISCLOSE | `start-namespace --sdk --region` | `namespace_name` | `create-rejected` | +| 8 | 2 | Choose clone dir | INPUT | — (1=default / 2=Edit) | dir | — | +| 9 | 2 | Scaffold the app | DISCLOSE | `scaffold --sdk [--manager] [--dir]` | `repo_path`,`manager` | `clone-failed`,`unknown-sdk`,`manager-not-found`,`unsupported-manager` | +| 10 | 2 | Await namespace (join) | DISCLOSE | `await-namespace --name` | `namespace_handle`,`address` | `namespace-timeout`,`namespace-not-provisioning`,`handle-not-found` | +| 11 | 2 | Create key + save config | DISCLOSE | `create-key --handle --address` | `key_id` (token never printed) | `key-empty`,`no-json-parser`,`manual-key-needed` | +| 12 | 2 | Verify config | DISCLOSE | `verify-config` | — | `profile-missing` | +| 13 | 3 | Await auth | DISCLOSE | `await-auth` | `auth_ready` | `auth-timeout` | +| 14 | 3 | Run the Workflow | **GO-AHEAD** | `run-workflow --sdk --dir` | `workflow_status`,`workflow_id`,`run_id` | `worker-unauthorized`,`worker-not-polling`,`worker-start-failed`,`workflow-failed`,`workflow-not-submitted`,`workflow-timeout`,`precompile-failed` | +| 15 | 4 | Inject failure + recover | **GO-AHEAD** | `run-workflow … --demo-failure transient` | same as 14 | same as 14 | + +Phase bodies below add only the human nuance the table can't (region-pick guardrails, KeyId-vs-secret labeling, the result links). The exact command of any step comes from its gate — render it from the `‹sub›` template in §Gate templates (slots filled), don't hand-write it. + + + +### Secret-handling carve-out (overrides command disclosure) + +The output contract says disclose the real command. **The API-key steps are the exception.** The `eyJ…` token must never be reprinted, logged, rendered in a diff, or passed as an argv (a rendered diff is the one exposure that leaves the local machine). For the key-capture and TOML-write actions: + +- Show the friendly label and a **redacted** form of the command — e.g. `api_key = "eyJ…(captured, not shown)"`. +- Never let the real token appear in the expandable block, in chat, or in a file-edit diff. +- The **KeyId** (e.g. `JW4LO…`) is *not* secret and may be shown. See Phase 2 for the KeyId-vs-secret distinction. +- **Never read, `cat`, `grep`, or open `temporal.toml` (or any key-capture file) with the Read/Edit/Update tool.** The file holds the `eyJ…` token, so *any* read of it surfaces the secret into this transcript — this is the most common accidental leak. To confirm the profile, use **only** `scripts/provision.sh verify-config` (it lists profile *names*, never the key value). +- **Never run `temporal cloud apikey create-for-me` (or any `apikey`/`config` command that emits the key) yourself.** Only `scripts/provision.sh create-key` mints and stores the token — it redirects the one-time secret straight into the locked file. Run the raw CLI by hand and it prints the token to the terminal, into this output. + +## Execution model — drive the bundled script, don't hand-roll the CLI + +The variance-prone work — installing the CLI, signing in, listing regions, creating the namespace, minting the API key, and writing the client-config TOML — is owned by a bundled script: **`scripts/provision.sh`**. **Invoke it and parse its result block; do not reassemble these `temporal cloud` commands yourself.** That is what makes a run deterministic: the flags are pinned in one place, the retry / auth-recheck / "read the handle from the create output" logic is baked in, and the API-key token is written straight into the locked TOML by the script — so it never enters your context and can never leak into a rendered diff. + +Each operation prints one delimited block on **stdout** — parse *that*, not the prose: + +``` +=== RESULT === +status=ok # ok | error | skipped += # operation-specific, e.g. namespace_handle=…, address=…, key_id=… +=== END === +``` + +Human-readable progress goes to **stderr** (it shows in the expandable tool block — the teaching surface). On `status=error` the block carries `error_code` + `message`: map the code via **Failure Handling** below and fix the cause — **do not improvise an alternate command, switch output formats, or poll.** + +The flow steps — subcommand, tier, and error codes — are the **Steps spine table above** (single source of truth). The RESULT keys each emits: + +- `preflight` → `os`, `config_path`, `cli_installed` (drives the Install-CLI skip), `warnings`, `stray_env` +- `detect-tools` → `default`, `managers`, `versions`, `discrepancies` +- `install-cli` → `status` (`skipped` if present) · `login` → `identity` · `regions` → raw list on stderr (you recommend, user picks) +- `start-namespace` → `namespace_name` · `scaffold` → `repo_path`, `manager` · `await-namespace` → `namespace_handle`, `address` +- `create-key` → `key_id` (token never printed) · `verify-config` → profile names only · `await-auth` → `auth_ready` +- `run-workflow` → `workflow_status` (`COMPLETED`), `workflow_id`, `run_id`, `task_queue` (add `--demo-failure transient` for Phase 4) + +**Utility subcommands (not in the main flow):** `preview [args]` (emits the `=== GATE ===` block + `cmd_N` + resolved params; side-effect-free — **maintenance/testing only, not used in the flow**: gates are rendered from §Gate templates, never by calling a script) · `provision-and-scaffold` (the older bundled namespace+clone+deps call — superseded by start/scaffold/await, kept as a fallback) · `install-deps` (re-install / switch manager) · `clone` (clone only) · `repair-config` (strip duplicate `cloud-setup` blocks; keeps `default`) · `cleanup-info` (prints the teardown commands, never runs them). + +**Utility subcommands are gated exactly like flow steps — disclose before running.** "Not in the main flow" means *don't run them as routine steps*, **not** that they skip disclosure: if you ever invoke one (`install-deps` to switch a manager, `repair-config` to fix a duplicate profile, `clone`, etc.), render its gate first (derive it from the `scaffold`/`install-cmd` shapes in §Gate templates — these utilities have no dedicated template). **And don't improvise them into the flow:** the main steps already cover the work (`scaffold` clones *and* installs dependencies — never add an extra `install-deps` "to confirm deps resolve", and never narrate doing so). + +The script is the **single source of truth for CLI flags**, and it is **read-only during a run** (read-only script): invoke it as shipped, never edit it. If it returns `status=error` because a prerelease flag has genuinely drifted, **stop and report that to the user as a one-line maintenance note** — fixing the script is a separate, deliberate task for a human, not something to do mid-setup. The wizard layer (tracker, checklists, checkpoints, the no-narration rules above) is still yours; only the imperative CLI work lives in the script. + +## Per-command gate — disclose, then run + +This setup runs real commands that **create billable Cloud resources and install software on the user's machine**. Earlier versions ran the whole bundle with no visible disclosure, which felt "too magic." So **before each `provision.sh` command, show what it will do** — render its gate. The user's own tool-permission prompt is where they approve or deny (it shows the same command via your `description`); the skill doesn't stack its own approval on top. The exception is the three **go-ahead** steps (`login`, `run-workflow`, inject-failure), which additionally wait for a go-ahead (`1. / 2. Chat about this`). This is disclosure + a light control, not narration — keep it tight. + +**Deterministic backstop (don't rely on it):** every effectful `provision.sh` subcommand now also echoes its own gate to stderr (the tool block) *before* it acts, so the run is self-documenting even if you forget the chat-side gate. This is a safety net — it surfaces bundled with the result, *after* the action — so it never replaces rendering the gate first. Always render the §Gate-template gate, then run. (Disable only for tests via `TCLOUD_DISCLOSE=0`.) + +**The gate — render it from the matching template in §Gate templates; never run a script to build it.** Each step has a verbatim template (a plain bold heading, then a fenced ` ```bash ` block with `#` comments above each command); fill **only** its `‹slots›` from their named sources and **print it exactly** — do not reassemble or reformat it (hand-assembling formatting is where it kept breaking: dropped fences, comment-only, glued rules). Rendering is **agent text — zero tool calls**, so disclosure never trips a permission prompt; only the effectful `scripts/provision.sh` run does. For a **disclose** step, render the gate and run. For a **go-ahead** step, append the numbered choices **stacked one per line** and wait. The gate looks like this (a disclose step — render it, then run): + +```` +**Installing the Temporal CLI** + +```bash +# install the prerelease Temporal CLI via Homebrew (adds software to your machine) +brew install temporalio/prerelease/temporal-cloud +``` +```` + +A multi-step subcommand shows each underlying command, one per step — a terse `#` note **followed by the actual command** (never a comment on its own). Still a disclose step — render, then run: + +```` +**Creating your namespace & downloading the sample app** + +```bash +# 1. create your Cloud namespace — billable; provisions on Temporal's servers (~a few min) +temporal cloud namespace create --name --region aws-us-east-1 \ + --api-key-auth-enabled --retention-days 30 --auto-confirm + +# 2. clone the Cloud-ready sample +git clone --branch money-transfer-project-cloud-setup --single-branch \ + money-transfer-project-template-python + +# 3. install dependencies (pip) +cd money-transfer-project-template-python \ + && python3 -m venv env && source env/bin/activate \ + && python -m pip install -q temporalio +``` +```` + +The run step is a **go-ahead** step — it shows the real Worker + starter commands (**the commands themselves, not just their `#` labels**) and waits for the choice: + +```` +**Run your first Workflow** + +```bash +# Worker — runs in the background, polls the task queue, stopped when done +cd money-transfer-project-template-python && source env/bin/activate && python run_worker.py + +# starter — submits the Workflow, waits for it to reach COMPLETED, then exits +cd money-transfer-project-template-python && WORKFLOW_ID=money-transfer-demo python run_workflow.py +``` + +1. Run it +2. Chat about this +```` + +**A few rules these examples encode** (everything else is in the output contract above — don't restate it): + +- **A `#` comment above every command — never a comment alone, never a bare command.** The real commands must appear (a reported failure: the run step once showed only `# Worker` / `# starter` with the commands missing). Keep each comment to a few words; it's both a label and a one-line lesson for a newcomer. +- **Name material side effects in the relevant comment** — `# … - billable`, `# … (adds software to your machine)`, `# mint key + write the cloud-setup profile to temporal.toml`. The §Gate templates already encode this; render them verbatim. +- **`create-key` secret carve-out:** its GATE block shows the mint command **without** the token (captured straight into the locked TOML) — render as-is; never a token, never a redacted diff. + +## Gate templates + +These are the **verbatim source** for every step's gate. The disclosure is **agent-rendered text — zero tool calls** (no `scripts/preview.sh`, so nothing prompts before the user even sees the command). Only the effectful `scripts/provision.sh ` run prompts. + +**Hard rule — render the matching template verbatim.** Substitute **only** the `‹slots›`, never add/drop/reorder/reformat lines or fences; keep every static character (headings, `#` comments, the ` ```bash ` fence, spacing) byte-for-byte. Each `‹slot›`'s value comes **only** from its named source in "Filling the slots" below — never from memory, never improvised. The result is exactly what `scripts/preview.sh ` used to print between its `=== GATE ===`…`=== END GATE ===` markers. + +### Filling the slots + +| Slot | Source | +|------|--------| +| `‹sdk›` | the user's SDK pick (Phase 1) | +| `‹region›` | the user's region pick (Phase 1, from the `regions` list) | +| `‹manager›` | `detect-tools` RESULT `default`, or the user's override when they pick a non-default manager | +| `‹clone-dir›` | the user's clone-dir pick (Phase 2); default = the repo basename for `‹sdk›` (see SDK command reference) | +| `‹namespace-name›` | `start-namespace` RESULT `namespace_name` | +| `‹namespace-handle›` | `await-namespace` RESULT `namespace_handle` | +| `‹address›` | `await-namespace` RESULT `address` | +| `‹repo-url›` | SDK command reference, keyed by `‹sdk›` | +| `‹install-cmd›` | SDK command reference, keyed by `‹sdk›`/`‹manager›` | +| `‹worker-cmd›` | SDK command reference, keyed by `‹sdk›` | +| `‹starter-cmd›` | SDK command reference, keyed by `‹sdk›` | +| `‹task-queue›` | SDK command reference, keyed by `‹sdk›` | +| `‹runtime›` | SDK command reference, keyed by `‹sdk›` (the version-probe binary) | +| `‹probe-bins›` | SDK command reference, keyed by `‹sdk›` (the manager binaries to look for) | + +`‹key-id›` is **never** shown in any gate — it appears only in the final summary (Phase 2 checklist). There is **no token slot**: the `eyJ…` token never appears in a template. + +### SDK command reference + +Source of truth = `scripts/provision.sh`. Keyed by `‹sdk›` (and `‹manager›` where it varies): + +| `‹sdk›` | `‹repo-url›` | `‹runtime›` | `‹task-queue›` | +|---------|-------------|-------------|----------------| +| python | https://github.com/temporalio/money-transfer-project-template-python | python3 | TRANSFER_MONEY_TASK_QUEUE | +| go | https://github.com/temporalio/money-transfer-project-template-go | go | TRANSFER_MONEY_TASK_QUEUE | +| ts | https://github.com/temporalio/money-transfer-project-template-ts | node | money-transfer | +| java | https://github.com/temporalio/money-transfer-project-java | java | MONEY_TRANSFER_TASK_QUEUE | +| dotnet | https://github.com/temporalio/money-transfer-project-template-dotnet | dotnet | MONEY_TRANSFER_TASK_QUEUE | +| ruby | https://github.com/temporalio/money-transfer-project-template-ruby | ruby | money-transfer | + +`‹clone-dir›` default (repo basename) = the trailing path segment of `‹repo-url›` (e.g. python → `money-transfer-project-template-python`, java → `money-transfer-project-java`). + +**Managers (`default` first) + `‹probe-bins›` (the binaries `detect-tools` looks for):** + +| `‹sdk›` | managers | `‹probe-bins›` | +|---------|----------|----------------| +| python | pip (default), uv | python3 uv | +| ts | npm (default), pnpm, yarn | npm pnpm yarn | +| go | go | go | +| java | maven | mvn | +| dotnet | dotnet | dotnet | +| ruby | bundler | bundle | + +(manager → its probe binary: pip→python3, uv→uv, npm→npm, pnpm→pnpm, yarn→yarn, go→go, maven→mvn, dotnet→dotnet, bundler→bundle. `‹probe-bins›` for an SDK is the space-joined probe binaries of all its managers, in the order above.) + +**`‹install-cmd›`** (keyed by `‹sdk›`/`‹manager›`): + +| `‹sdk›`/`‹manager›` | `‹install-cmd›` | +|---------------------|-----------------| +| python/pip | `python3 -m venv env && . env/bin/activate && python -m pip install -q temporalio` | +| python/uv | `uv venv env && . env/bin/activate && uv pip install -q temporalio` | +| ts/npm | `npm install` | +| ts/pnpm | `pnpm install` | +| ts/yarn | `yarn install` | +| go/go | `go mod download` | +| java/maven | `mvn -q -DskipTests dependency:resolve` | +| dotnet/dotnet | `dotnet restore` | +| ruby/bundler | `bundle install` | + +**`‹worker-cmd›` / `‹starter-cmd›`** (keyed by `‹sdk›`): + +| `‹sdk›` | `‹worker-cmd›` | `‹starter-cmd›` | +|---------|----------------|-----------------| +| python | `source env/bin/activate && python run_worker.py` | `source env/bin/activate && python run_workflow.py` | +| go | `go run worker/main.go` | `go run start/main.go` | +| ts | `npm run worker` | `npm run client` | +| java | `mvn -q compile exec:java -Dexec.mainClass=moneytransferapp.MoneyTransferWorker -Dorg.slf4j.simpleLogger.defaultLogLevel=warn` | `mvn -q compile exec:java -Dexec.mainClass=moneytransferapp.TransferApp -Dorg.slf4j.simpleLogger.defaultLogLevel=warn` | +| dotnet | `dotnet run --project MoneyTransferWorker` | `dotnet run --project MoneyTransferClient` | +| ruby | `ruby worker.rb` | `ruby starter.rb` | + +The scaffold install-comment also varies by where deps land — keep the comment exactly as the template shows for that `‹sdk›`/`‹manager›` (python/ts say "inside the repo"; go/java/dotnet/ruby say "GLOBAL, outside the repo …"). The worked example and templates below carry the right wording per SDK; for non-python SDKs use the install comment from `scripts/preview.sh scaffold --sdk ‹sdk›` if you ever need to re-verify it (maintenance only). + +### Templates + +**Phase 1 — `preflight`** (static): + +```` +**Checking your environment** + +```bash +# check git / jq / brew are available (read-only, local) +command -v git jq brew + +# flag any stray TEMPORAL_* env vars that would override your saved config +env | grep '^TEMPORAL_' || true +``` +```` + +**Phase 1 — `detect-tools`:** + +```` +**Detecting your local tools** + +```bash +# detect which package managers are installed for ‹sdk› (read-only, local) +command -v ‹probe-bins› + +# read each tool's version to flag anything below the minimum +‹runtime› --version +``` +```` + +**Phase 1 — `install-cli` (not installed):** + +```` +**Installing the Temporal CLI** + +```bash +# install the Temporal CLI via Homebrew (adds software) +brew install temporalio/prerelease/temporal-cloud +``` +```` + +**Phase 1 — `install-cli` (already installed — render this variant instead when the CLI is present):** + +```` +**Temporal CLI already installed - nothing to do** + +```bash +# the Temporal CLI is already on your machine, so this step is skipped (no install, no update) +temporal cloud version +``` +```` + +**Phase 1 — `login`** (static): + +```` +**Sign in to Temporal Cloud** + +```bash +# open a browser to sign in (blocks until you finish) +temporal cloud login + +# confirm the signed-in identity +temporal cloud whoami +``` +```` + +**Phase 1 — `regions`** (static): + +```` +**Listing your Cloud regions** + +```bash +# list the regions your account can use +temporal cloud region list +``` +```` + +**Phase 2 — `start-namespace`:** + +```` +**Creating your Cloud namespace** + +```bash +# create your Cloud namespace - billable; submits async, provisions server-side (~a few min) +temporal cloud namespace create --name --region ‹region› --api-key-auth-enabled --retention-days 30 --auto-confirm --async +``` +```` + +**Phase 2 — `create-namespace`** (the synchronous variant — rare; the flow uses `start-namespace` + `await-namespace`): + +```` +**Creating your Cloud namespace** + +```bash +# create your Cloud namespace - billable; provisions server-side (~a few min) +temporal cloud namespace create --name --region ‹region› --api-key-auth-enabled --retention-days 30 --auto-confirm +``` +```` + +**Phase 2 — `scaffold` (clone + deps):** + +```` +**Downloading the sample app (clone + dependencies)** + +```bash +# clone the Cloud-ready sample +git clone --branch money-transfer-project-cloud-setup --single-branch ‹repo-url› ‹clone-dir› + +# install dependencies with ‹manager› ‹install-location-note› +(cd ‹clone-dir› && ‹install-cmd›) +``` +```` + +`‹install-location-note›` per SDK (keep verbatim): python = `into a local venv (env/) inside the repo` · ts = `into node_modules inside the repo` · go = `into the shared Go module cache - GLOBAL, outside the repo (~/go/pkg/mod)` · java = `into the shared Maven cache - GLOBAL, outside the repo (~/.m2)` · dotnet = `into the global NuGet cache - GLOBAL, outside the repo (~/.nuget/packages)` · ruby = `into globally-installed gems - GLOBAL, outside the repo`. + +**Phase 2 — `await-namespace`** (static): + +```` +**Waiting for the namespace to provision** + +```bash +# poll until the namespace is ACTIVE — provisioning usually takes ~a few minutes (bounded) +temporal cloud namespace list --name -o json +``` +```` + +**Phase 2 — `create-key`** (secret carve-out — the token is captured to a 0600 file, never shown; **never** add a token slot): + +```` +**Creating your API key and saving the config** + +```bash +# mint the key + write the cloud-setup profile to temporal.toml (token captured to a 0600 file, never printed) +temporal cloud apikey create-for-me --display-name money-transfer-cloud-setup- --expiry-duration 25h --auto-confirm -o json +``` +```` + +**Phase 2 — `verify-config`** (static): + +```` +**Verifying the saved config** + +```bash +# list the cloud-setup profile fields (api_key redacted, never shown) +temporal --profile cloud-setup config list +``` +```` + +**Phase 3 — `await-auth`** (static): + +```` +**Waiting for the API key to be accepted** + +```bash +# poll an authorized call until the new key is accepted (bounded ~90s) +temporal --profile cloud-setup workflow list --limit 1 +``` +```` + +**Phase 3 — `run-workflow` (clean run):** + +```` +**Run your first Workflow** + +```bash +# Worker - runs in the background, polls the task queue, stopped when done +(cd ‹clone-dir› && ‹worker-cmd›) + +# starter - submits the Workflow, waits for COMPLETED, exits (the run can take a minute or two) +(cd ‹clone-dir› && WORKFLOW_ID=money-transfer-demo ‹starter-cmd›) +``` +```` + +**Phase 4 — `run-workflow … --demo-failure transient` (inject + recover):** + +```` +**Run the recovery Workflow (inject a failure)** + +```bash +# Worker - runs in the background, polls the task queue, stopped when done +(cd ‹clone-dir› && DEMO_FAILURE=transient ‹worker-cmd›) + +# starter - submits the Workflow, waits for COMPLETED, exits (the run can take a minute or two) +(cd ‹clone-dir› && WORKFLOW_ID=money-transfer-demo-recovery ‹starter-cmd›) +``` +```` + +(Utility subcommands — `clone`, `install-deps`, `provision-and-scaffold`, `repair-config` — are **rare** and have no dedicated template; derive their gate from the `scaffold`/`install-cmd` shapes above if you ever invoke one.) + +### Worked example — the `scaffold` gate for Java, fully filled + +`‹sdk›` = java → `‹repo-url›` = `https://github.com/temporalio/money-transfer-project-java`, `‹clone-dir›` (default) = `money-transfer-project-java`, `‹manager›` = `maven`, `‹install-cmd›` = `mvn -q -DskipTests dependency:resolve`, `‹install-location-note›` = `into the shared Maven cache - GLOBAL, outside the repo (~/.m2)`. The rendered gate: + +```` +**Downloading the sample app (clone + dependencies)** + +```bash +# clone the Cloud-ready sample +git clone --branch money-transfer-project-cloud-setup --single-branch https://github.com/temporalio/money-transfer-project-java money-transfer-project-java + +# install dependencies with maven into the shared Maven cache - GLOBAL, outside the repo (~/.m2) +(cd money-transfer-project-java && mvn -q -DskipTests dependency:resolve) +``` +```` + +## Start — show the plan, then begin Phase 1 + +This skill begins when the user invokes it (e.g. `/temporal-cloud-setup`) or asks to set up Temporal Cloud. On start, print the roadmap, then go straight into Phase 1 (whose first step is choosing the SDK). Do **not** narrate. + +Print this opening block verbatim — the two ⚠️ notices first (adjacent), then the plan and the profile line: + +``` +> ⚠️ **Experimental:** this setup skill is experimental and under active development. +> ⚠️ **Heads-up:** this creates real resources in your Temporal Cloud account — a namespace and an API key — which may incur cost. + +**Let's get you set up on Temporal Cloud.** Four phases, end to end. + +1. **Get set up** — install the CLI, sign in, and choose your SDK + region +2. **Download the sample app & create your API key** — create your Cloud namespace and clone the money-transfer app (already wired for Cloud) in parallel, then mint your API key +3. **Run your first Workflow** — start the Worker and run the money-transfer Workflow +4. **See Durable Execution** — break the transfer on purpose and watch Temporal recover it + +I'll show each command before running it — and depending on your setup, your tool may ask you to approve it first. + +Setting up for: ** · SDK pending** +``` + +The ⚠️ Heads-up in that block is a notice, not a blocking gate — continue unless the user objects. + +**Selections are collected up front, in Phase 1, while the user is most engaged** — SDK first (no prerequisite), then region right after sign-in (the live region list requires being logged in). Do not begin cloning before the SDK answer — the SDK selects which repo is cloned. + +## Phase output envelope + +This is the **single source of truth for per-phase formatting** — the phase bodies below supply only *content* (the intent sentence and the steps); this envelope supplies the *format*. Render every phase in this fixed order: + +1. **Tracker line** at the top, marker advanced (see below). +2. **Intent sentence** — one short line on what this phase sets up and why it matters (gloss any Temporal term). No more than one line. +3. **Step checklist — once, all unchecked** (the phase plan). Then run each step (real tool call with a friendly `description`, or a genuine question) **without re-printing the checklist or tracker between steps**. Render an already-present prerequisite as `[x] … already present, skipped`. **No** `**What this did:**` summary and **no** `↪ Learn more:` link during the run. +4. **End-of-phase: completed checklist + checkpoint** — print the checklist once more with **every box checked**, show `**Phase N complete ✅**`, and **close that same message** with the numbered checkpoint prompt (its content is detailed in the next section — it belongs to *this* message, it is not a second message). In Codex-style runtimes, this entire block must be the final assistant message of the turn when you pause for the user; do not print it earlier as progress and then repeat or fragment it in the final response. No checkpoint after Phase 4 — go straight to the Ending. + +The tracker is just the four phase markers and the phase counter — **no leading label** (don't prefix it with "Setup" or anything before the first marker). Legend: completed = `✅`, current = `🔵`, upcoming = `⚪` (a white dot — same filled-circle style as the blue current dot). **Bold the current step's name** (the one with the blue dot). Reprint it at the top of each phase, advancing one marker: + +``` +🔵 **Set up** · ⚪ App & API key · ⚪ Run · ⚪ Recover · phase 1/4 +✅ Set up · 🔵 **App & API key** · ⚪ Run · ⚪ Recover · phase 2/4 +✅ Set up · ✅ App & API key · 🔵 **Run** · ⚪ Recover · phase 3/4 +✅ Set up · ✅ App & API key · ✅ Run · 🔵 **Recover** · phase 4/4 +``` + +(Glyph notes: `⚪` white dot = upcoming and `🔵` blue dot = current — same filled-circle style, so the row reads as one consistent set. `✅` = the reliably-green completion mark; a green *circle*-with-check isn't a dependable cross-platform glyph, so stick with `✅`.) + +## After-phase checkpoint (after Phases 1–3) + +This documents the checkpoint prompt that the envelope's step 4 already emits as the closing lines of the completed-checklist message — **it is described here, not a second message to send.** It hands control back via a numbered prompt (not an arrow-select — numbered works on every runtime). **Do not explain anything proactively**; the checklist already told the story. At a turn boundary, the checkpoint handoff must be self-contained and emitted exactly once: tracker/checklist context + `**Phase N complete ✅**` + this prompt together in the final assistant message. The prompt is: + +``` +1. Continue +2. I have a question about this phase + +Choose a number, or write your response. +``` + +- If they pick **2 (question):** give a short, plain-language explanation of what the phase just did, answer their typed question in newcomer-friendly language, then re-present the numbered prompt — `1. Continue` / `2. I have another question` / `Choose a number, or write your response.` — and loop until they proceed. +- Explanation is **on-demand only** — it appears solely when they pick the question option. +- A checkpoint follows Phases 1–3. **No checkpoint after Phase 4** — go straight to the Ending. +- Genuine blocking input (SDK pick, browser sign-in, region pick) is normal work, **not** this checkpoint (genuine input, not a checkpoint). + +--- + +# The phases + +```text +Phase 1 — Get set up: choose SDK · install CLI · sign in · choose region +Phase 2 — App & API key: create namespace + clone sample + install deps (parallel) · create API key · write config TOML +Phase 3 — Run your first Workflow: start the Worker · run the Workflow (success) +Phase 4 — See Durable Execution: inject a failure · watch Temporal retry & recover ──► then END +``` + + + +## Phase 1 — Get set up + +*Intent: get the tools on your machine, sign you in, and capture your choices — everything the rest of the run needs.* + +Step checklist: `SDK chosen` · `Tools detected` · `CLI installed` · `Signed in` · `Region chosen`. + +**Step — Choose your SDK** (genuine input, not a checkpoint): present the six as a **numbered list** (numbered list, runtime-agnostic) — Python, Go, TypeScript, Java, .NET, Ruby — and take a typed name/number. This selects which repo is cloned and the language of the local app. Echo the resolved profile line (`Setting up for: macOS · Python SDK`) once answered. + +Right after the SDK pick, the **preflight** check (DISCLOSE — render, then run): render its gate from the `preflight` template (§Gate templates), then run `scripts/provision.sh preflight --sdk `. Note its `cli_installed` flag — it drives whether the Install-CLI step below runs or is skipped. If its `stray_env` lists any `TEMPORAL_*` vars, tell the user they override the saved profile and ask them to unset them before continuing; surface other `warnings` (e.g. `brew-missing`, `no-json-parser`) only if they block a later step. + +**Step — Detect local tools + choose your package manager** (DISCLOSE — render, then run). Render its gate from the `detect-tools` template (§Gate templates), then run `scripts/provision.sh detect-tools --sdk ` and read its RESULT. This adapts the setup to the user's machine, and it surfaces tooling problems **early** (here in Phase 1) instead of deep in Phase 2/3. + +- **Package manager (Python & TypeScript only).** `managers` is the set of package managers **this sample supports** for the chosen SDK. When it lists more than one, ask **which of the sample-supported managers to use** — frame it that way ("the sample supports these — which should we use?"), **not** as "here's what's installed on your machine." Present them as a **numbered list** (runtime-agnostic) with the `default` marked — e.g. Python `1. pip (default)` / `2. uv`; TypeScript `1. npm (default)` / `2. pnpm` / `3. yarn`. The user confirms the default or overrides; carry the choice into Phase 2 as `scaffold --manager `. **This is a question — ask it alone and wait for the answer; do not disclose install-cli or sign-in in the same message** (see "One step at a time"). For **Go / Java / .NET / Ruby** the sample has a single toolchain — state it (`Using: maven`) and **skip the prompt**. +- **Surface `discrepancies` HERE (fail-early), plain language:** + - `version-too-old:@(min)` — an **advisory** warning with remediation (e.g. *"Node 16 detected; 18+ recommended — consider upgrading, but I can proceed"*). Not a hard stop. + - `tool-missing:` / `manager-not-found:` — the runtime or chosen manager isn't installed. Offer another **sample-supported** manager from `managers`, or ask the user to install the tool, then re-run `detect-tools`. +- The default proposal is **deterministic** — the same machine yields the same default every run; nothing is persisted (no state file). + +**Step — Install the unified Temporal CLI** (DISCLOSE — render, then run, *unless already present*). It ships the `temporal cloud` command group (binary `temporal-cloud`). **Branch on preflight's `cli_installed`** (it used the same `temporal cloud` probe the installer does, so it's authoritative — don't re-check by calling `install-cli` just to confirm): + +- **`cli_installed=true` → skip the step entirely.** Render the checklist item as `- [x] Temporal CLI — already present, skipped` and do **not** render the install gate or call `install-cli`. No gate, no tool call — marking it skipped *is* the step (per the "already-satisfied prerequisite" rule). We deliberately do **not** auto-update a working CLI (don't change a working install out from under the user). +- **`cli_installed=false` → install it.** Render its gate from the `install-cli` template (§Gate templates), then run `scripts/provision.sh install-cli`. It installs via the `temporalio/prerelease` Homebrew tap on macOS, and returns `error_code=brew-missing` / `manual-install` with the fallback URL if it can't — do not auto-install Homebrew; relay the message and wait. (`install-cli` still self-checks presence and emits `status=skipped` as a backstop, but in the normal flow you won't reach it when the CLI is already there.) +- (No prerelease disclaimer here — the ⚠️ notice at the top of the run already covers that.) + +This CLI is separate from any local `temporal server start-dev`. The Cloud path does not start a local server. + +**Step — Sign in** (**GO-AHEAD** — a browser opens, so wait for the user). Render its gate from the `login` template (§Gate templates), then present `1. Sign in / 2. Chat about this` and wait. On `2`, answer the question and re-present. On `1`, run `scripts/provision.sh login` — it runs `temporal cloud login` (which opens a browser on the user's machine and blocks until they finish) and then confirms with `whoami`. **Do not ask the user to run the command themselves** (no `! temporal cloud login` hand-off); you run the script, the user just completes the browser prompt. Tell them to complete the sign-in in the browser. On success the result block carries `identity`; on `error_code=login-failed`/`not-authenticated`, ask them to finish the browser login and re-run. If they're not part of a Cloud account yet, point them to https://temporal.io/get-cloud and pause. + +**Step — Choose your region** (DISCLOSE — render, then run): render its gate from the `regions` template (§Gate templates), then run `scripts/provision.sh regions` (you're authenticated now). It prints the live region list; present it as a **numbered list** (runtime-agnostic), then apply these picking rules: + +- **Recommend the nearest** — infer it from the system timezone/locale (e.g. `America/New_York` → suggest `aws-us-east-1`) and mark it `(recommended)`, but the user still picks. +- **Never accept a region from memory and never auto-select** — a valid-but-wrong region creates a persistent, billable namespace in the wrong place (and has set off internal alerts). Use the exact identifier the user picks (shape `-`); it's passed to the create step next. +- **Fail fast — only accept a region that appears in the `regions` output** (it reflects what your account can actually use). If the user names one that isn't listed, don't pass it to create (a region your account lacks access to is the classic "it spun for minutes" trap) — re-show the list and have them pick a listed value. +- **Steer away from `unsupported_regions`** — the `regions` RESULT may list `unsupported_regions` (regions whose provider reads `UNKNOWN`, e.g. `azure-centralus`). On such accounts those **accept a namespace create but never provision it** — a phantom that stalls the next phase. Mark any listed there as `(may not be available on your account)`, **don't recommend it**, and steer to an AWS/GCP region. If the user insists, warn it may never provision (you'll catch it fast — see `namespace-not-provisioning`). +- **If the create is still rejected** for the region, see Failure Handling (`create-rejected`) — re-list and re-pick, never silently retry the same region. + +**If the user asks at the checkpoint, explain (plain language):** installed the Temporal CLI, signed you in, and recorded your SDK + region. Your namespace itself gets created in the next phase — in parallel with setting up your app. (Docs: https://docs.temporal.io/cli) + +## Phase 2 — App & API key + +*Intent: create your Cloud namespace and download the sample app (clone + dependencies) in parallel, then mint your API key and save the connection config — everything needed to run Workflows on Cloud.* + +Step checklist: `Namespace active` · `Sample cloned` · `Dependencies installed` · `API key created` · `Config saved`. + +The namespace and the app are set up as **three separate, individually-gated steps** — so the **billable** namespace gets its own explicit approval, distinct from the benign clone. The namespace provisions on Temporal's servers (`--async`) **while** the app is cloned, so the parallelism is preserved without any background process having to survive across steps (identical on Claude Code, Codex, Cursor). + +**Step — Start your namespace** (`start-namespace`, DISCLOSE — its own gate, then run; the user's permission prompt approves this **billable** create — make the `description` say so): + +- **Fire-and-forget:** submits the create with `--async`, returns immediately, and provisions server-side while you download the sample app. +- **Read `namespace_name`** from the result — you pass it to `await-namespace` below. The handle isn't known yet (it resolves once provisioning completes). +- **Errors:** `create-rejected` (region/name). + +**Step — Ask where to clone** (genuine input — **always ask, don't silently default**). Present a two-option numbered choice where **option 1 is the default clone path itself** (so the user sees and confirms the real path) and **option 2 is "Somewhere else"**: + +``` +Where should I clone the sample app? +1. ./money-transfer-project-template-python +2. Somewhere else +``` + +- **1** → clone to that path (omit `--dir`, or pass it explicitly — same result). +- **2 (Somewhere else)** → ask for the path, then pass it as `--dir`. + +Show the concrete default for the chosen SDK in option 1 (e.g. `./money-transfer-project-template-ts` for TypeScript, `./money-transfer-project-template-go` for Go). Pass the chosen path as `--dir` to `scaffold` below. + +**Step — Download the sample app** (`scaffold`, DISCLOSE — its own gate, then run, separate from the namespace): + +- **Clones the cloud-ready sample + installs dependencies** — runs **while the namespace provisions**, threading the Phase 1 manager choice and the clone dir just chosen. +- **`--manager`/`--dir` are validated fail-fast before the clone** — a bad/uninstalled manager errors immediately (`manager-not-found` / `unsupported-manager`). +- **Read `repo_path` + `manager`.** Other errors: `clone-failed`, `unknown-sdk`. +- The cloned branch ships wired for Cloud — **no connection code to edit.** + +**Step — Wait for the namespace** (`await-namespace --name `, DISCLOSE — render, then run): render its gate from the `await-namespace` template (§Gate templates), then run `scripts/provision.sh await-namespace --name `. Polls (via the exact `namespace list --name` filter) until the namespace is **ACTIVE**, then reads the handle + endpoint from that result. Read **`namespace_handle`** + **`address`**; carry both into the key step below. Errors: `namespace-timeout` (appeared but slow — re-run), `namespace-not-provisioning` (never appeared — bad region, re-run `start-namespace` elsewhere), `handle-not-found`. + +For reference, the per-SDK repo mapping (the script selects the right one): + +| SDK | Repository (branch `money-transfer-project-cloud-setup`) | +|------------|------------| +| Python | `https://github.com/temporalio/money-transfer-project-template-python` | +| Go | `https://github.com/temporalio/money-transfer-project-template-go` | +| TypeScript | `https://github.com/temporalio/money-transfer-project-template-ts` | +| Java | `https://github.com/temporalio/money-transfer-project-java` | +| .NET | `https://github.com/temporalio/money-transfer-project-template-dotnet` | +| Ruby | `https://github.com/temporalio/money-transfer-project-template-ruby` | + +**How it connects (no edit needed):** all six SDKs load the **`cloud-setup`** profile from `temporal.toml` (env-config). The key step below writes it; nothing else is needed at run time. + +**After `await-namespace` returns, go straight to the `create-key` step — no *phase checkpoint* between them, and don't re-print the checklist or tracker. But `create-key` still gets its own gate — its command is disclosed, then run (the user's permission prompt approves it, like every step); it mints your key *and* writes/replaces the `cloud-setup` profile in `temporal.toml`. The next checklist render is the completed one at the end of the phase.** + +**Step — Create the key and save the config** (`create-key --handle --address
`, DISCLOSE — render, then run, using the values from the `await-namespace` step above). In one deterministic, secret-safe operation the script: + +- **re-verifies auth** (`whoami`, re-prompting login if expired); +- **mints the key** with the pinned flags; +- **captures it via `-o json` into a `0600` temp file** so the `eyJ…` token never reaches stdout, your context, or a rendered diff; +- **writes a named `cloud-setup` profile** into the shared `temporal.toml` (address, namespace, api_key) **without touching `[profile.default]`** or its login session; +- **`chmod 600`s the file** and deletes the temp capture. + +It returns only the **non-secret** `key_id` and the `config_path` — never the token. Then verify the profile (DISCLOSE — render, then run): render its gate from the `verify-config` template (§Gate templates), then run `scripts/provision.sh verify-config` to confirm the profile loads (it never prints the api_key value). + +This is the **secret-handling carve-out** — render the action's label without the token (e.g. `api_key = "eyJ…(captured, not shown)"`); never reprint, log, argv-pass, or commit it. + +- On `error_code=key-empty`/`not-authenticated`: an expired login — let the script re-prompt and retry once; don't switch output formats or poll. +- On `error_code=no-json-parser`: install `jq` or `python3`, then re-run (the safe capture needs one). +- On `error_code=manual-key-needed`: automatic capture failed and there was no terminal to paste into. Ask the **user** to paste the one-time key and re-run `create-key` from a context with a terminal — the script reads the paste *hidden*, straight into the locked file. **Never** have the user paste the key into the chat, and never paste it yourself. +- The secret is shown only once and cannot be retrieved later; if it's truly lost, mint a new key (re-run this step) — don't try to recover the old value. + +**KeyId vs. secret — not the same thing.** The `key_id` the script returns (e.g. `JW4LO…`) is a non-secret *identifier* — the handle used to revoke the key later (`temporal cloud apikey delete --key-id `); fine to show. **Only the `eyJ…` token is the credential** — never reprint, log, or commit it. + +**When you check off "API key created," label the `key_id` so a newcomer can't mistake it for the secret.** A bare 32-char `key_id` on its own reads like a leaked key and alarms people. Render it with an explicit non-secret tag, and make the config line state the secret was stored (not shown) — e.g.: + +``` +- [x] API key created — key id JW4LO… (non-secret identifier; used to revoke the key) +- [x] Config saved — ~/Library/Application Support/temporalio/temporal.toml (chmod 600; secret stored, not shown) +``` + +Never render the `key_id` bare and unlabeled, and never put the `eyJ…` token on either line. + +**Env vars override the profile.** Env-config gives `TEMPORAL_*` env vars **higher precedence** than the TOML profile — the Phase 1 preflight flags any stray `TEMPORAL_ADDRESS`/`TEMPORAL_NAMESPACE`/`TEMPORAL_API_KEY`. If `stray_env` was non-empty, make sure they're unset in the run shell or they'll override the saved profile at run time. + +**If the user asks at the checkpoint, explain (plain language):** created your Cloud namespace and, in parallel, cloned a small money-transfer app in your language and installed its dependencies. Then minted an API key for your namespace (``) and saved the connection — address, namespace, key — into a locked `cloud-setup` profile your app reads at run time. (Docs: https://docs.temporal.io/develop · https://docs.temporal.io/cloud/api-keys) + +## Phase 3 — Run your first Workflow + +*Intent: prove the whole setup works by running a real Workflow on Temporal Cloud.* + +Step checklist: `Worker running` · `Workflow completed`. + +The app connects from config and Phase 2 supplied the credentials — this phase is **run-only**. All SDKs read the `cloud-setup` profile from `temporal.toml`; confirm it's present with `scripts/provision.sh verify-config` if you haven't already. + +**Connect-readiness is handled for you — do not hand-roll the Worker.** The Worker start, the wait-until-it's-polling, the starter, and the Worker teardown all live in **one deterministic call** (`run-workflow`), so you never launch `nohup … &` / `ps` / `pgrep` yourself — that is exactly the flaky, noisy step this replaces. Two things still matter: + +1. A freshly-minted API key isn't accepted by the data plane *immediately* — wait on `await-auth` first. +2. If the connect still fails, the namespace endpoint (`.tmprl.cloud:7233`) is correct — do **not** switch to a regional endpoint, re-mint the key, or edit the profile (per temporalio/documentation#4733). The failure is readiness, not the endpoint. + +Run two script calls, both using `repo_path` from Phase 2 (the script handles per-SDK run commands and Python venv activation internally — you don't): + +1. **Wait for auth** (`await-auth`, DISCLOSE — render, then run): render its gate from the `await-auth` template (§Gate templates), then run `scripts/provision.sh await-auth` and wait for `auth_ready=true`. +2. **Run the Workflow** (`run-workflow --sdk --dir `, **GO-AHEAD** — the deliberate moment): render its gate from the `run-workflow` (clean run) template (§Gate templates), append `1. Run it / 2. Chat about this` (on `2`, answer, then re-present), and on `Run it` one synchronous call starts the Worker, waits until it's polling (Temporal's API, not the OS process table), runs the starter, and stops the Worker. Wait for `workflow_status=COMPLETED`; it emits the run's **`workflow_id`** + **`run_id`** (don't run `workflow list` yourself). On `worker-unauthorized`, re-run `await-auth` then `run-workflow` (see Failure Handling). +3. **Show the success link, then confirm the win.** `run-workflow` already verified `COMPLETED`. Surface the run's **timeline** page on its own bare line (bare URL so the terminal auto-linkifies it — no backticks/fence), using the `workflow_id` and `run_id` from the RESULT block: + + **View your completed Workflow on Temporal Cloud:** + + https://cloud.temporal.io/namespaces//workflows///timeline + + That's your first Workflow on the Cloud — no separate `workflow describe` needed. + + *Fallback — only if `run_id` came back `unknown` (the data plane was briefly lagging): show the Workflow **detail** page instead, rendered exactly like the link above — bold lead-in, then a bare URL alone on its own line (no backticks) so it auto-linkifies:* + + **View your completed Workflow on Temporal Cloud:** + + https://cloud.temporal.io/namespaces//workflows/ + + *Never fall back to the bare `…/workflows` namespace list.* + +At the **Phase 3 checkpoint**, lead with the win — `**Phase 3 complete ✅ — your first Workflow ran clean.**` — then the standard numbered prompt (`1. Continue` / `2. I have a question about this phase`). Phase 4 itself frames and triggers the failure injection, so this checkpoint stays a plain **Continue**. + +**If the user asks at the checkpoint, explain (plain language):** started a Worker that polls your Cloud namespace, ran the money-transfer Workflow against Temporal Cloud, and confirmed it reached `COMPLETED` — your first Workflow on the Cloud. (Docs: https://docs.temporal.io/workflows) + +## Phase 4 — See Durable Execution (inject a failure) + +*Intent: break the transfer on purpose and watch Temporal retry and recover it — the durable-execution payoff. This phase is **required**; the user reaches it via **Continue** at the Phase 3 checkpoint, then triggers the break via the prompt below.* + +Step checklist: `Failure injected` · `Recovered & completed`. + +**Open by framing the break, then let the user trigger it.** After the tracker + intent + unchecked checklist, explain in one or two plain sentences what's about to happen — *we'll run the same transfer again, but force the deposit to fail on its first attempts, then watch Temporal automatically retry until it succeeds* — then present a numbered prompt so the user actively triggers it: + +``` +1. Inject the failure +2. Chat about this +``` + +On `1`, run the inject step below; on `2`, answer the question and re-present. This is a genuine engagement point (genuine input, not a checkpoint), not a checkpoint. + +> The `DEMO_FAILURE` toggle is shipped on the `money-transfer-project-cloud-setup` branches (the deposit activity reads it), verified end-to-end on real Cloud for all six supported SDKs. + +**Step — Inject the failure.** The `1. Inject the failure` choice above is this step's go-ahead — so disclose, then run (don't add a second go-ahead prompt): + +- **Disclose:** render its gate from the `run-workflow` (inject + recover) template (§Gate templates). +- **Run:** `scripts/provision.sh run-workflow --sdk --dir --demo-failure transient`. Starts the Worker with `DEMO_FAILURE=transient` (the deposit activity fails its first attempts, then succeeds), runs **the same starter command — the sample's source is never edited** (it reads its Workflow ID from the environment), and stops the Worker when done. +- **Distinct Workflow ID:** this run is named `money-transfer-demo-recovery` (vs the clean run's `money-transfer-demo`), so it appears as a **separate Workflow** in Cloud whose history shows the failure-and-recovery. +- **Wait for `workflow_status=COMPLETED`** — the retry recovered it. **Keep this call's `workflow_id` + `run_id`**; they identify the recovery run the Ending link points at. + +**Step — Show the recovery.** Keep it to **one line**: the deposit failed on purpose, Temporal retried it automatically, and the Workflow still reached `COMPLETED` (the withdrawal never re-ran) — then send them to the dashboard to see it. Don't walk through the worker logs or event history line-by-line; the dashboard CTA carries the detail. + +*(Variant — advanced, manual.)* `DEMO_FAILURE=permanent` makes the deposit fail **non-retryably**, so the **`refund`** compensation runs — the saga/rollback story. Two caveats: + +- **Run it by hand, not through `run-workflow`** — `run-workflow` expects `COMPLETED` and would report a non-zero starter as `workflow-failed`. +- **Terminal state differs by SDK** — narrate what the history actually shows, don't assert one outcome: + - Python / Go / TypeScript / .NET → **`FAILED`** (the original error propagates after the refund). + - Java / Ruby → **`COMPLETED`** (their saga returns after compensating). + +There is **no checkpoint after Phase 4** — go straight to the Ending, whose Cloud-UI link is the recovered run's timeline page (`money-transfer-demo-recovery`). + +**If the user asks (plain language):** we made the deposit fail on purpose; Temporal retried it automatically and the Workflow still finished correctly — no lost state, no manual recovery. (Docs: https://docs.temporal.io/encyclopedia/retry-policies) + + + +## Ending the Skill + +Once Phase 4 has shown the recovery, the setup is done. **Close with a short summary and the recovery link — keep it tight, no big recap table.** Do exactly this: + +1. **No Worker should still be running** — `run-workflow` stops its Worker when it returns, so Phases 3 and 4 leave nothing polling Cloud. Only stop a process by hand if you ran the advanced manual `DEMO_FAILURE=permanent` variant. +2. **Show the recovered Workflow, then close with the summary.** First the Cloud UI link (bold lead-in, bare URL alone on its own line, no backticks) — the **recovery run's timeline** page, using the `workflow_id`/`run_id` Phase 4 emitted: + + **View your recovered Workflow on Temporal Cloud:** + + https://cloud.temporal.io/namespaces//workflows///timeline + + *Fallback — only if `run_id` is `unknown`: show the Workflow **detail** page the same way — bold lead-in, bare URL on its own line:* + + **View your recovered Workflow on Temporal Cloud:** + + https://cloud.temporal.io/namespaces//workflows/ + + *— never the bare `…/workflows` list.* + + Then close with a short summary (a few plain sentences, no table) that makes the durable-execution payoff concrete — for example: + + > 🎉 You're set up on Temporal Cloud — and you just watched **Durable Execution** in action. Your money-transfer Workflow ran on real Cloud infrastructure, and when the deposit failed on purpose, Temporal automatically retried it until it succeeded — the transfer still completed, with no lost state and not a line of retry code from you. That's the whole idea: you write the business logic; Temporal makes it survive failures and run to completion. + +3. **One-line note:** the API key is saved in `temporal.toml` (give the path, `chmod 600`) and **auto-expires in ~25 hours** — don't share raw terminal logs and never commit the TOML. + +The summary above is the payoff — keep it to that one short recap. The only calls-to-action are the **two Workflow timeline links** — the completed run (Phase 3) and the recovered run (here at the end). Don't loop back, re-run, keep teaching, offer teardown, or suggest other next steps — a successful completion is the terminal state. + +## Failure Handling + + + +Stop and surface the problem (don't silently retry destructive or auth steps), staying in the calm output style — report the problem and fix plainly, without exposing reasoning. The script ops fail loudly with `status=error` + an `error_code`; **fix the cause the code names and re-run the same op — never improvise an alternate command, switch output formats, or poll.** Map the codes: + +- **`brew-missing` / `manual-install`** (install-cli) → relay the script's message: install Homebrew from https://brew.sh, or download `temporal-cloud` from the releases page and put it on `PATH`. Do **not** auto-install Homebrew. Re-run install-cli. +- **`login-failed` / `not-authenticated`** (login, or any later op) → the browser sign-in didn't complete or the session expired. Ask the user to finish/redo the browser login, then re-run the op (create-namespace / create-key re-check auth themselves). +- **`regions-empty`** → confirm auth (`login`), then re-run `regions`. Don't hand-prefix or guess a region. +- **`create-rejected`** (start-namespace) → the namespace create was rejected on submit, usually region or name format; re-list regions, have the user pick an exact provider-prefixed value, then re-run `start-namespace`. Attribute this as a **namespace** failure, never as a downstream key error. +- **`namespace-timeout`** (await-namespace) → the namespace **appeared** (ACTIVATING) but didn't reach **ACTIVE** within the bound (`NS_AWAIT_MAX_SECS`, default 600s). It's provisioning lag, not a misconfig: re-run `await-namespace` (it resumes polling the exact `namespace list --name` filter until the namespace is ACTIVE); raise the bound with `NS_AWAIT_MAX_SECS=N` if needed. Don't switch to `namespace get`/other formats. (Waiting for ACTIVE here is what keeps `create-key`/`await-auth` from connecting to an endpoint that isn't serving yet — the cause of a "no children to pick from" stall.) +- **`namespace-not-provisioning`** (await-namespace / provision-and-scaffold) → the create was accepted but the namespace **never appeared** in the list within the phantom-grace window (`NS_PHANTOM_GRACE_SECS`, default 75s) — i.e. it's not provisioning at all, vs. just slow. Almost always an **unavailable region** (e.g. `azure-centralus`, whose provider reads `UNKNOWN` — see the region step's `unsupported_regions`). Don't re-run `await-namespace` on the same name; **re-run `start-namespace` with an AWS/GCP region**. +- **`handle-not-found`** (provision-and-scaffold) → the namespace was created but didn't reach **ACTIVE** within the retry bound (still provisioning). Re-run `provision-and-scaffold` or `await-namespace` to resume the wait. **Never** decode the API-key token or hunt the filesystem/config for the account-id — the exact `namespace list --name` filter is the source. +- **`clone-failed` / `unknown-sdk`** (scaffold) → the sample clone failed (network/repo) or the SDK has no repo mapping; confirm the SDK + connectivity, then re-run `scaffold`. (Independent of the namespace, which is already provisioning.) +- **`manager-not-found`** (scaffold / install-deps) → the chosen package manager isn't installed on this machine. Offer an **available** manager from the latest `detect-tools` `managers` list, or ask the user to install the missing one, then re-run with that `--manager`. Caught **before** the clone, so nothing was set up. +- **`unsupported-manager`** (scaffold / install-deps) → that manager isn't valid for this SDK's sample (e.g. `poetry` for Python, whose sample ships no `pyproject.toml`). Pick one of the supported managers named in the error / `detect-tools` and re-run. +- **`version-too-old`** (detect-tools `discrepancies`, **advisory**) → not a hard error and never blocks the run. Relay the remediation (upgrade the tool to the noted minimum) but you may proceed — the sample usually still works on the older version. +- **`no-json-parser`** (create-key) → install `jq` or `python3` (needed to capture the token safely), then re-run create-key. +- **`key-empty` / `key-create-failed`** (create-key) → almost always an expired login (not an output-format problem); the script re-checks `whoami` — redo the browser login if prompted, then re-run create-key once. +- **`config-write-failed`** (create-key) → the profile couldn't be written to `temporal.toml` (read-only dir or full disk); the key was minted but not saved. Fix directory permissions / free disk, then re-run create-key (it mints a fresh key and writes a clean profile). +- **API key lost** (only shown once) → re-run create-key to mint a fresh one (it rewrites the profile); don't try to recover the old value. +- **`temporal.toml` unparseable / duplicate `[profile.cloud-setup]` blocks** (e.g. from earlier partial runs) → run `scripts/provision.sh repair-config` (strips every `cloud-setup` block via awk, keeps `[profile.default]`, never reads the file into context), then re-run `create-key` to write one fresh profile. **Do not hand-edit, `cat`, or `awk` the file yourself** (the read-only-script rule and the secret carve-out) — `create-key` also strips any existing/duplicate `cloud-setup` blocks before writing, so it self-heals too. +- **`worker-unauthorized`** (run-workflow) → the Worker hit an auth error before it could poll — the just-minted key isn't accepted yet. Re-run `await-auth` (wait for `auth_ready=true`), then re-run `run-workflow`. Do **not** switch endpoints, re-mint the key, or edit the profile (it's readiness, not config). +- **`precompile-failed`** (run-workflow) → the Maven/dotnet build step failed before the Worker started. Java and .NET are pre-compiled once so the timing windows cover only Temporal operations. Check the output above; usually a missing Java/Maven/.NET SDK installation or a network issue fetching dependencies on the first build. +- **`worker-not-polling`** (run-workflow) → the Worker started but never registered as a poller within the bound (`WORKER_READY_MAX_SECS`, default 120s). Usually deps weren't installed or the Worker process crashed early. Confirm `scaffold` finished and `--dir` is the real `repo_path`, then re-run; raise the bound with `WORKER_READY_MAX_SECS=N` if needed. The script prints the Worker log tail to help. +- **`worker-start-failed`** (run-workflow) → the Worker process exited before polling (missing deps/venv, wrong dir, or a sample-app error in the log tail). Confirm deps installed and `--dir` is correct, then re-run. +- **`workflow-failed`** (run-workflow) → the starter exited non-zero / the Workflow didn't reach `COMPLETED`. Read the printed log tail: if it's an auth error, run `await-auth` and retry; otherwise surface the sample-app error. (Expected `FAILED` for the manual `DEMO_FAILURE=permanent` variant is **not** run through `run-workflow`.) +- **`workflow-not-submitted`** (run-workflow) → the starter exited **0 but never submitted a Workflow** within the settle window (`NOWF_SETTLE_SECS`, default 15s). Some sample clients catch their own connect/start error and still exit 0 (the .NET starter does this), so a clean exit code can hide a failed start. Read the printed log tail — it's almost always an auth/connection error: run `await-auth` (wait for `auth_ready=true`) and re-run `run-workflow`; if the profile points at a namespace that isn't ACTIVE, re-check `await-namespace` first. +- **`workflow-timeout`** (run-workflow) → the starter didn't finish within `--max-secs` (default 180s). The Cloud workflow is automatically terminated on timeout so it doesn't stay Running with no worker. If it still fires, raise with `--max-secs N` and re-run. +- **`Request unauthorized` / `Unavailable` on the first connect (right after setup)** → almost always **post-provision readiness**, not a misconfig: the just-created namespace + key need a moment to become connectable, and the parallel flow shortens that gap. **Wait ~10–15s and retry the connect, up to ~3 times.** Do **not** switch the address to a regional endpoint, re-mint the key, or rewrite the profile. The namespace endpoint (`.tmprl.cloud:7233`) is the correct, Temporal-recommended endpoint for API keys (temporalio/documentation#4733); regional is **not** the fix. If it still fails after retries, that's a Temporal Cloud issue to escalate — not a reason to change the skill's endpoint. +- **`auth-timeout`** (await-auth) → the new API key still isn't accepted after the bound (`AUTH_READY_MAX_SECS`, default 90s). It's propagation, not config: wait longer and re-run `await-auth`; if it never clears, re-run `create-key` to mint a fresh key. Don't switch endpoints or edit the profile. +- **TLS / auth errors at connect** → confirm the profile's `[profile.cloud-setup.tls]` has `disabled = false` (TLS on), the address is the **namespace endpoint** (`.tmprl.cloud:7233`), and the `api_key` is set; re-run `verify-config`. (`create-key` now writes `disabled = false` explicitly.) +- **`temporal cloud …` commands suddenly fail auth (after deleting/expiring the key)** → a profile carrying an `api_key` overrides the login session. Remove the `[profile.cloud-setup]` block, or pass `--disable-config-file`. The setup always lives in the **named** `cloud-setup` profile (never `default`), so management commands keep using the login session. + + + +## Files + +- `scripts/provision.sh` — **the deterministic executor.** Owns preflight / **detect-tools** / **preview** / install / login / regions / namespace-create / **install-deps (manager-parameterized)** / key-mint+config-write / verify / await-auth / **run-workflow (Worker + starter)** / clone / repair-config / cleanup-info. Invoke it and parse its `=== RESULT ===` block (see "Execution model" above); it is the single source of truth for the pinned CLI flags, the per-SDK run commands, **and the per-(SDK,manager) install matrix + minimum-version table**. Pure bash, portable across Claude Code, Codex, and Cursor. **Read-only during a run — invoke it, never edit it (read-only script).** +- `scripts/preview.sh` — **read-only disclosure shim, maintenance/testing only.** A ~3-line forwarder to `provision.sh preview `. The flow no longer calls it: gates are **agent-rendered from §Gate templates** (zero tool calls, so disclosure never prompts on any host). Kept so `provision.sh preview` output stays verifiable (the drift-guard test binds the templates to it). Side-effect-free; never edit it. +- `scripts/tests/run-tests.sh` — offline, stubbed tests for the adaptation work (detection defaults, side-effect-free preview, per-manager install, version discrepancies, ASCII RESULT, no secrets). Dev-only; run under `/bin/bash` (3.2). Not part of a setup run. +- `references/unified-cli.md` — background on the prerelease CLI and the client-config TOML: `login`/`whoami`, `region list`, `namespace create`, `apikey create-for-me`, file locations, and the auth-override gotcha. The script encodes these; read the reference when a flag drifts and you need to update the script. +- `references/sdk-cloud.md` — per-SDK table: repo + cloud branch, task-queue name, how each connects (`cloud-setup` profile), and worker/starter run commands. No connection edits — the branch is pre-wired. diff --git a/plugins/temporal/skills/temporal-cloud-setup/agents/openai.yaml b/plugins/temporal/skills/temporal-cloud-setup/agents/openai.yaml new file mode 100644 index 0000000..da170e0 --- /dev/null +++ b/plugins/temporal/skills/temporal-cloud-setup/agents/openai.yaml @@ -0,0 +1,2 @@ +policy: + allow_implicit_invocation: false \ No newline at end of file diff --git a/plugins/temporal/skills/temporal-cloud-setup/references/sdk-cloud.md b/plugins/temporal/skills/temporal-cloud-setup/references/sdk-cloud.md new file mode 100644 index 0000000..7c52754 --- /dev/null +++ b/plugins/temporal/skills/temporal-cloud-setup/references/sdk-cloud.md @@ -0,0 +1,130 @@ +# Per-SDK: Cloud-ready Sample Repo, Branch, Task Queue, and How It Connects + +For the chosen SDK: clone the **`money-transfer-project-cloud-setup`** branch, install deps, then run the Worker and the starter. **The branch is pre-wired for Cloud — there is no connection edit.** + +> **Note:** the clone + deps install are done by `scripts/provision.sh provision-and-scaffold` (PE-68), and the Worker + starter run is done by `scripts/provision.sh run-workflow --sdk --dir ` (PE-70) — a single synchronous call that starts the Worker, waits until it's polling (Temporal API, not `ps`/`pgrep`), runs the starter, and stops the Worker. The per-SDK repo, task queue, and run commands below are the **source of truth the script encodes** — they are reference, not commands you run by hand. + +How each branch connects: **all six SDKs load the named `cloud-setup` profile from `temporal.toml`** (env-config), so the key stays in the locked `0600` file — never in source, argv, or shell history. Step 6 writes that profile; nothing else is needed at run time. *(Verify the SDK's env-config symbol against current docs before relying on it.)* + +All connection values come from the **`cloud-setup` profile** in the TOML written in Step 6 (never `default`): +- `address` = the **namespace endpoint** = the handle + `.tmprl.cloud:7233`, e.g. `.tmprl.cloud:7233` (region-agnostic, works with API keys — **not** the regional `..api.temporal.io:7233`) +- `namespace` = `` +- `api_key` = the one-time key + +TLS is required for Temporal Cloud (the `cloud-setup` profile / env-config carries it). Worker and starter must use the **same task-queue name** (already set in the sample — do not change it). + +## Quick table + +| SDK | Repo (branch `money-transfer-project-cloud-setup`) | Task queue | Connects via | +|-----|------|-----------|--------------| +| Python | `money-transfer-project-template-python` | `TRANSFER_MONEY_TASK_QUEUE` | `cloud-setup` profile (env-config) | +| Go | `money-transfer-project-template-go` | `TRANSFER_MONEY_TASK_QUEUE` | `cloud-setup` profile (env-config) | +| TypeScript | `money-transfer-project-template-ts` | `money-transfer` | `cloud-setup` profile (env-config) | +| Java | `money-transfer-project-java` | `MONEY_TRANSFER_TASK_QUEUE` | `cloud-setup` profile (env-config) | +| .NET | `money-transfer-project-template-dotnet` | `MONEY_TRANSFER_TASK_QUEUE` | `cloud-setup` profile (env-config) | +| Ruby | `money-transfer-project-template-ruby` | `money-transfer` | `cloud-setup` profile (env-config) | + +The per-SDK sections below give **install + run** commands. The connection code they show is **reference only** — the cloud branch already contains it; you do not edit it. + +--- + +## Python + +Install: `python3 -m venv env && source env/bin/activate && python -m pip install temporalio` + +Connects via the `cloud-setup` profile (already in the branch — no edit): `ClientConfigProfile.load("cloud-setup")` → `Client.connect(**profile.to_client_connect_config())` (`temporalio.envconfig`), in `run_worker.py` and `run_workflow.py`. + +Run (activate the venv in each shell): `source env/bin/activate && python run_worker.py` (Worker) then `source env/bin/activate && python run_workflow.py` (starter). + +--- + +## Go + +Install: `go version` (deps via `go.mod`). + +Connects via the `cloud-setup` profile (already in the branch — no edit): `envconfig.LoadClientOptions(LoadClientOptionsRequest{ConfigFileProfile:"cloud-setup"})` → `client.Options` (`go.temporal.io/sdk/contrib/envconfig`), in `worker/main.go` and `start/main.go`. + +Run: `go run worker/main.go` then `go run start/main.go`. + +--- + +## TypeScript + +Install: `npm install`. + +Connects via the `cloud-setup` profile (already in the branch — no edit): `loadClientConnectConfig({profile:'cloud-setup'})` (`@temporalio/envconfig`) drives both `NativeConnection` (worker, `src/worker.ts`) and `Connection` (client, `src/client.ts`). + +Run: `npm run worker` (= `ts-node src/worker.ts`) then `npm run client` (= `ts-node src/client.ts`). (Verified against the `money-transfer-project-cloud-setup` branch's `package.json`.) + +--- + +## Java + +Build: Maven (`pom.xml`); the run targets compile on demand. + +Connects via the `cloud-setup` profile (already in the branch — no edit): `ClientConfigProfile.load(...setConfigFileProfile("cloud-setup"))` → `toWorkflowServiceStubsOptions()` / `toWorkflowClientOptions()` (`io.temporal.envconfig`), in `MoneyTransferWorker.java` and `TransferApp.java`. + +Run (Maven, verified against the cloud-setup branch's `Makefile`): worker `mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker"` (`make worker`), then starter `mvn compile exec:java -Dexec.mainClass="moneytransferapp.TransferApp"` (`make run`). + +--- + +## .NET + +Install: `dotnet --version`. + +Connects via the `cloud-setup` profile (already in the branch — no edit): `ClientEnvConfig.LoadClientConnectOptions(new ProfileLoadOptions{Profile="cloud-setup"})` → `TemporalClientConnectOptions` (`Temporalio.Common.EnvConfig`), in `MoneyTransferWorker/Program.cs` and `MoneyTransferClient/Program.cs`. + +Run: `dotnet run --project MoneyTransferWorker` then `dotnet run --project MoneyTransferClient`. + +--- + +## Ruby + +Install: `bundle install`. + +Connects via the `cloud-setup` profile (already in the branch — no edit): `Temporalio::EnvConfig::ClientConfig.load_client_connect_options(profile:'cloud-setup')` → `Client.connect(*args, **kwargs)`, in `worker.rb` and `starter.rb`. + +Run: `ruby worker.rb` then `ruby starter.rb`. + +--- + +## Package managers & minimum versions (PE-75 adaptation) + +`scripts/provision.sh detect-tools --sdk ` reports which managers are **supported by the sample +AND installed**, picks a deterministic default (first available in preference order; the lockfile's +manager leads when one is committed), and flags version discrepancies. `install_cmd_for` / +`install-deps` in `provision.sh` are the single source of truth for the install commands below. + +**Only Python and TypeScript ship a real manager choice.** This was confirmed by auditing the +`money-transfer-project-cloud-setup` branch of each sample repo (Step 0): the Python sample ships +**no** dependency manifest (`pip install temporalio` only — so `poetry` is not offered), and the +TypeScript sample commits `package-lock.json` (so `npm` leads). The other four SDKs are single-toolchain. + +| SDK | Managers (default first) | Install command (per manager) | +|-----|--------------------------|-------------------------------| +| python | **pip**, uv | pip: `python3 -m venv env && . env/bin/activate && python -m pip install -q temporalio` · uv: `uv venv env && . env/bin/activate && uv pip install -q temporalio` | +| ts | **npm**, pnpm, yarn | npm: `npm install` · pnpm: `pnpm install` · yarn: `yarn install` | +| go | go | `go mod download` | +| java | maven | `mvn -q -DskipTests dependency:resolve` (the run targets also compile on demand) | +| dotnet | dotnet | `dotnet restore` | +| ruby | bundler | `bundle install` | + +**Minimum versions (advisory — a shortfall is a `version-too-old` warning + remediation, never a hard block):** + +| Tool | Min | Tool | Min | +|------|-----|------|-----| +| python3 | 3.8 | uv | 0.1 | +| node | 18 | npm | 8 | +| go | 1.21 | pnpm | 8 | +| java (JDK) | 8 | yarn | 1.22 | +| dotnet | 6.0 | maven | 3.6 | +| ruby | 3.0 | bundler | 2.0 | + +Java reports its version with the legacy `1.8.x` scheme for JDK 8; `detect-tools` maps `1.N` to `N` +so JDK 8 is treated as 8 (not flagged below the JDK-11 line some tools assume — the Temporal Java +SDK supports JDK 8, so 8 is the intended floor). + +## Notes + +- If unsure of the exact env-config symbol for the installed SDK version, check the SDK's current "connect to Temporal Cloud" / env-config docs or the sample repo's README. +- The `address` is the namespace endpoint (`.tmprl.cloud:7233`) — built directly from the handle that `namespace create` returns, **not** the Web UI URL and **not** reconstructed via `namespace get -o json` (which returns empty on this prerelease). `scripts/provision.sh create-namespace` derives and returns it as `address`. \ No newline at end of file diff --git a/plugins/temporal/skills/temporal-cloud-setup/references/unified-cli.md b/plugins/temporal/skills/temporal-cloud-setup/references/unified-cli.md new file mode 100644 index 0000000..8444f39 --- /dev/null +++ b/plugins/temporal/skills/temporal-cloud-setup/references/unified-cli.md @@ -0,0 +1,165 @@ +# Unified Temporal CLI (cloud-cli) + Client Config TOML Reference + +> **The executor is `scripts/provision.sh`, not these raw commands.** During a run the agent invokes that script, which has these flags pinned and the error handling baked in. This file is the **background + source-of-truth** for what the script does: read it to understand the behavior, and when a prerelease flag drifts, update both `scripts/provision.sh` and this file together. Don't hand-run these commands during a normal setup. + +Read this before changing any `temporal cloud ...` or `temporal config ...` command in the script. The `cloud-cli` is in **prerelease**, so command and flag names change. If a command or flag here is not accepted, run the matching `--help` and use what it reports — never guess flags. + +## What this CLI is + +- The unified CLI exposes a `temporal cloud` command group (binary name `temporal-cloud`). It is the prerelease successor to the older `tcld` Cloud CLI; the subcommand shapes mirror `tcld`. +- It is **separate** from a local `temporal server start-dev` workflow. This setup never starts a local server. +- Source / releases: https://github.com/temporalio/cloud-cli + +## Prerelease disclaimer (show before download) + +Surface this to the user before installing, quoted verbatim: + +> **Pre-release:** This extension is offered as a pre-release and is subject to change. Please reach out to Temporal Support if you have questions. + +## Install + +macOS (Homebrew, prerelease tap) — **confirm Homebrew is installed first** (`command -v brew`); if it is absent, do not auto-install it, point the user to https://brew.sh or use the download fallback below: + +```bash +command -v brew && brew install temporalio/prerelease/temporal-cloud +``` + +The tap is `temporalio/prerelease`, but the formula/binary is named `temporal-cloud` (not `cloud-cli`). If `brew install temporalio/prerelease/cloud-cli` fails with a formula-not-found error, use `temporal-cloud` as shown above. + +Other platforms: download the latest archive from +https://github.com/temporalio/cloud-cli/releases/latest , extract, and put the +`temporal-cloud` binary on your `PATH`. Build from source with `make build` (needs Go). + +Verify: + +```bash +temporal cloud version # or: temporal-cloud --version +temporal cloud --help +``` + +## Authenticate + +```bash +temporal cloud login # opens a browser for OAuth +temporal cloud whoami # confirm the authenticated identity +``` + +For non-interactive use, most commands also accept `--api-key `. + +If the user has no Cloud account yet, send them to https://temporal.io/get-cloud first. + +## List regions + +Before creating a namespace, **always** list the available **provider-prefixed** regions and have the user **select from the output** — **do not hand-prefix a bare region, and do not accept a region typed from memory without confirming it against the list** (a remembered region can be valid yet wrong for the account, creating the namespace in the wrong place — which has paged on-call teams): + +```bash +temporal cloud region list # list available regions; `temporal cloud region get ` for one +``` + +Note: in the current prerelease (cloud version 0.0.1) the region commands live under the **`region`** group (`temporal cloud region list` / `region get`) — there is **no** `namespace list-regions` subcommand. Use `--help` to confirm the exact subcommand name in the installed version. Pass to `--region` only a value the user selected from this command's output (shape `-`); never a bare region you prefixed yourself, and never one typed from memory that you haven't confirmed against the list. + +## Create an API-key namespace + +Prerelease CLI — these flags are verified during the start-up check (against https://github.com/temporalio/cloud-cli and `--help`). Use the verified flags directly here; if that check found drift, this block should already be updated to match. As of the current prerelease: + +```bash +temporal cloud namespace create \ + --name quickstartai-- \ + --region - \ + --api-key-auth-enabled \ + --retention-days 30 \ + --auto-confirm +``` + +- Confirmed flag names (these differ from older `tcld`): the namespace name flag is `--name` (not `--namespace`); API-key auth is the boolean `--api-key-auth-enabled` (not `--auth-method api_key`). +- **`--name` takes the bare name only** (e.g. `quickstartai-go-20260617-143205`) — Cloud appends `.` itself (its `--help` even says the name "becomes part of the namespace ID"). The bare name must be **≤39 characters**; the appended account-id does not count toward that limit. +- **`--auto-confirm` is required for unattended runs** (otherwise an interactive confirm prompt hangs the skill). **Never `--auto-confirm` a *delete*.** +- **Region is provider-prefixed**: `us-east-1` → `aws-us-east-1` (and `gcp-...` for GCP). List exact values with the region-list command above. +- **Create synchronously (no `--async`).** It blocks for **a few minutes** (repeated `Operation pending…` lines — normal provisioning, not a hang) until the namespace lands on Cloud. `--async` is still unusable (returns before the namespace is provisioned). Don't abort during the wait. +- **Get the handle from `namespace list -o jsonl`, NOT from the create output.** As of the current prerelease, `create`'s default text output drifted to a **diff format** (`-{}` / `+{"name": …}`) that no longer carries the `.` handle. `namespace list -o jsonl` **works** — the earlier "list/get/-o json return empty" note is stale — and returns `{"Namespaces":[{"namespace":"","spec":{"name":""},"endpoints":{"mtlsGrpcAddress":".tmprl.cloud:7233", …}}, …]}`. `lookup_handle_by_name` matches our `spec.name` → `.namespace`, with an account-id-construct fallback (the account-id is account-stable). Still never decode the API-key token or read config files for the account-id. +- **Detached-but-synchronous (overlap):** `provision-and-scaffold` runs this synchronous create as a background job **within one invocation** while it clones + installs deps in the foreground, then joins and resolves the handle via `lookup_handle_by_name`. Still **never `--async`**; only the *process* is backgrounded, and only inside that single call. +- The `address` for the client config TOML is the **namespace endpoint** = the handle + `.tmprl.cloud:7233`, e.g. `.tmprl.cloud:7233` — build it directly from the handle (no `namespace get` needed). Use this, **not** the regional `grpcAddress` (`..api.temporal.io:7233`) — region-agnostic, works with API keys, survives HA failover (Temporal's recommended endpoint). + +## Create an API key + +Flags verified during the start-up check (against https://github.com/temporalio/cloud-cli and `--help`). As of the current prerelease, the subcommand for the logged-in user is `create-for-me` with these flags. + +> **⚠️ Secret-safety: never run this command without redirecting stdout into a `0600` file.** The response carries the one-time `eyJ…` token; run it bare and the token prints to the terminal (and into any agent transcript). `scripts/provision.sh create-key` is the sanctioned caller — it always redirects. The form below is the *only* form to copy: + +```bash +umask 077 +temporal cloud apikey create-for-me \ + --display-name \ + --description "money-transfer Cloud setup" \ + --expiry-duration 24h \ + --auto-confirm \ + -o json > "$KEYFILE" # never to the terminal +``` + +- Confirmed against the prerelease CLI (differs from older `tcld`): subcommand `apikey create-for-me`; `--display-name` is **required** (not `--name`); expiry is `--expiry-duration` (not `--duration`). A separate `apikey create` may exist for service accounts; use `create-for-me` for the current user. +- `--auto-confirm` is **required** — like `namespace create`, this command prompts interactively; without it a captured run returns exit 0 with empty output and no key created. +- **Re-verify auth first.** The earlier `login` can expire mid-run. Run `temporal cloud whoami` right before this; if it errors/empties, `temporal cloud login` again before retrying. +- **Empty output + exit 0 ⇒ diagnose, don't improvise.** It means a suppressed confirm prompt (add `--auto-confirm`) **or an expired login** (re-check `whoami`/`login`). Fix the cause and re-run once. +- Capture path (verified against cloud version 0.0.1 with fresh auth): `-o json` **works** — redirect it to a `0600` file (`umask 077`), e.g. `... --auto-confirm -o json > "$HOME/.key.json"`. The secret is the **`.token`** field and the non-secret id is **`.keyId`**. In Step 6, read `.token` from that file and write it **directly into the TOML** under `[profile.cloud-setup]` via a script (never `config set --value`, never a rendered diff — both would expose the secret), then `chmod 600` the file and delete the temp capture file. +- `temporal cloud apikey list` may return **empty output even when keys exist** on this prerelease build — do **not** treat that as a failed creation. The authoritative confirmation of key creation is the `create-for-me` response (`asyncOperation.state == STATE_FULFILLED` + a returned `keyId`); the ultimate validity check is the Worker authenticating in Phase 4. +- **The secret is displayed only once.** It cannot be retrieved later — if lost, create a new key. Never echo it into chat or commit it to a repo. + +## Client config TOML + +Temporal SDKs and the CLI share a client-config TOML with named profiles. + +Default file locations: + +| OS | Path | +|---------|------| +| macOS | `$HOME/Library/Application Support/temporalio/temporal.toml` | +| Linux | `~/.config/temporalio/temporal.toml` | +| Windows | `%AppData%\temporalio\temporal.toml` | + +Override with the `TEMPORAL_CONFIG_FILE` environment variable. + +**Use a named `cloud-setup` profile — never write to `[profile.default]`.** The user may already have a `default` profile (local dev, another Cloud namespace) that this would silently overwrite. A named profile lives alongside it and is selected explicitly. + +**Auth-override gotcha:** a profile that carries an `api_key` becomes the CLI's auth source for any command that loads it, **overriding the `temporal cloud login` (browser) session**. Implications: +- Run management commands (`login`, `whoami`, `namespace …`, `apikey …`) **without** `--profile`, so they keep using the login session. Use `--profile cloud-setup` only for data-plane `workflow` commands (which need the key). This is why writing the key to `default` is harmful — it would hijack every command. +- After the key is deleted or expires, any command that loads that profile fails auth against a namespace that may no longer exist. Remove the `[profile.cloud-setup]` block to clean up, or pass `--disable-config-file` to bypass the profile and fall back to the login session. + +Profile structure: + +```toml +[profile.cloud-setup] +address = ".tmprl.cloud:7233" # namespace endpoint +namespace = "" +api_key = "" + +[profile.cloud-setup.tls] +disabled = false # TLS is REQUIRED for Temporal Cloud — set it explicitly. +# An empty [profile.cloud-setup.tls] section left TLS ambiguous and caused +# "unable to connect / tls not set to true" failures; `disabled = false` is unambiguous. +``` + +**Write the whole `[profile.cloud-setup]` block directly into the TOML file — do not use `temporal config set`.** `config set` rewrites the shared file and can strip the `oauth` block out of `[profile.default]` (where the `temporal cloud login` session lives), breaking `temporal cloud whoami` and cleanup. Writing the block yourself touches only `cloud-setup`. Write `address` and `namespace` directly, and the `api_key` the same way — from the captured value via shell redirection (never `config set --value`, which records the secret in shell history and exposes it in `ps`). Then restrict permissions so other local accounts can't read the credential: + +```bash +chmod 600 "$HOME/Library/Application Support/temporalio/temporal.toml" +``` + +Other useful commands (always scope them to the profile): + +```bash +temporal --profile cloud-setup config list # show the profile (do not print api_key to the user) +temporal --profile cloud-setup config get +temporal --profile cloud-setup config delete +``` + +Once the `cloud-setup` profile is set, `temporal --profile cloud-setup workflow list` / `workflow describe` operate against the Cloud namespace. Plain commands without the flag still use the user's `default` — so always pass `--profile cloud-setup`. + +## Cloud UI deeplink + +To send the user to their **specific Workflow run** in the browser (preferred — lands them on the run's history so they can watch it / see the failure-and-recovery), use the run URL: + +``` +https://cloud.temporal.io/namespaces//workflows// +``` + +The bare list URL (`…/workflows`) is a fallback only — surface the run-specific URL when you have the Workflow ID + Run ID (from the starter output or `workflow describe -o json`). `` is the namespace's full handle from `namespace create` — `.`, e.g. `quickstartai-go-20260617-143205.fmrip`. \ No newline at end of file diff --git a/plugins/temporal/skills/temporal-cloud-setup/scripts/preview.sh b/plugins/temporal/skills/temporal-cloud-setup/scripts/preview.sh new file mode 100644 index 0000000..5b0d4b8 --- /dev/null +++ b/plugins/temporal/skills/temporal-cloud-setup/scripts/preview.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Read-only disclosure shim for temporal-cloud-setup. +# +# This forwards verbatim to `provision.sh preview ` and nothing else. +# It exists so the agent can build a step's GATE block (the disclosure the +# user sees BEFORE approving) WITHOUT that disclosure call itself tripping an +# approval prompt: a `Bash(*provision.sh*)` permission rule matches the real, +# effectful subcommands but NOT this file's name. Disclosure must always +# precede confirmation, so the gate-builder must be promptless. +# +# All logic remains in provision.sh — this adds zero behavior. `preview` is +# side-effect-free (no network, no writes), so running it unprompted is safe. +set -euo pipefail +# Resolve our own directory with bash builtins only (no external `dirname`/`cd` +# binary), so the shim works even on a minimal PATH. +dir="${0%/*}"; [ "$dir" = "$0" ] && dir="." +dir="$(cd "$dir" && pwd)" +# Forward with the same bash interpreter running this shim, so we don't depend on +# provision.sh's `#!/usr/bin/env bash` shebang resolving on a minimal PATH. +exec "${BASH:-bash}" "$dir/provision.sh" preview "$@" \ No newline at end of file diff --git a/plugins/temporal/skills/temporal-cloud-setup/scripts/provision.sh b/plugins/temporal/skills/temporal-cloud-setup/scripts/provision.sh new file mode 100644 index 0000000..e428d1f --- /dev/null +++ b/plugins/temporal/skills/temporal-cloud-setup/scripts/provision.sh @@ -0,0 +1,1885 @@ +#!/usr/bin/env bash +# +# provision.sh — deterministic executor for the temporal-cloud-setup skill. +# +# =========================================================================== +# DO NOT EDIT THIS FILE WHILE RUNNING THE SKILL. Invoke it as shipped. +# It is written to work UNCHANGED on every platform (macOS bash 3.2 / BSD +# awk included). Do not reformat its punctuation, quoting, regexes, or flags. +# If a prerelease flag has genuinely drifted, that is a deliberate human +# maintenance edit -- not something an agent does mid-setup. (SKILL.md rule 10.) +# =========================================================================== +# +# WHY THIS EXISTS +# The skill's variance-prone work (create the namespace, mint + capture the +# API key, write the client-config TOML) is the part where an agent tends to +# improvise — polling, switching output formats, hunting the filesystem for an +# account-id, or leaking the secret into a rendered diff. This script makes +# that work identical on every run: fixed flags, baked-in error handling, and +# the secret never leaves the script (it is written straight into the locked +# TOML; only the non-secret KeyId is ever printed). +# +# The agent INVOKES this script and PARSES its structured RESULT block — it +# does not reassemble these commands itself. The wizard UX (phases, tracker, +# checkpoints, narration rules) stays in SKILL.md; the determinism lives here. +# +# PORTABILITY +# Pure bash, self-locating, no Claude/Codex-specific assumptions. Runs the +# same under Claude Code and Codex (both just shell out to it). +# +# OUTPUT CONTRACT +# Human-readable progress goes to STDERR (shown in the agent's tool block). +# The machine-readable result goes to STDOUT as a single delimited block: +# +# === RESULT === +# status=ok # ok | error | skipped +# = # operation-specific (see each subcommand) +# === END === +# +# On failure: status=error plus error_code= and message=. +# The token (eyJ...) is NEVER emitted on stdout, stderr, or in any result key. +# +# CLI FLAGS ARE PINNED HERE (single source of truth) +# These match the flags verified end-to-end against the prerelease CLI and +# documented in references/unified-cli.md. The unified CLI is in PRERELEASE; +# if a flag is rejected, fix it HERE (and in the reference) — one place, not +# per-run in the agent's head. +# +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# ---- output helpers --------------------------------------------------------- + +log() { printf '%s\n' "$*" >&2; } # human progress -> stderr +die() { # structured error -> stdout + local code="$1"; shift + printf '=== RESULT ===\nstatus=error\nerror_code=%s\nmessage=%s\n=== END ===\n' \ + "$code" "$*" + exit 1 +} +result_open() { printf '=== RESULT ===\n'; printf 'status=%s\n' "${1:-ok}"; } +result_kv() { printf '%s=%s\n' "$1" "$2"; } +result_close() { printf '=== END ===\n'; } + +require_cmd() { command -v "$1" >/dev/null 2>&1; } + +# ---- config-file location (TEMPORAL_CONFIG_FILE wins, else per-OS default) --- + +config_path() { + if [ -n "${TEMPORAL_CONFIG_FILE:-}" ]; then + printf '%s' "$TEMPORAL_CONFIG_FILE"; return + fi + case "$(uname -s)" in + Darwin) printf '%s' "$HOME/Library/Application Support/temporalio/temporal.toml" ;; + Linux) printf '%s' "${XDG_CONFIG_HOME:-$HOME/.config}/temporalio/temporal.toml" ;; + *) printf '%s' "$HOME/.config/temporalio/temporal.toml" ;; # best effort + esac +} + +# ---- per-SDK repo table (single source of truth; mirrors references/sdk-cloud.md) +# Branch is always money-transfer-project-cloud-setup (pre-wired for Cloud). + +repo_url_for_sdk() { + case "$1" in + python) printf 'https://github.com/temporalio/money-transfer-project-template-python' ;; + go) printf 'https://github.com/temporalio/money-transfer-project-template-go' ;; + ts|typescript) printf 'https://github.com/temporalio/money-transfer-project-template-ts' ;; + java) printf 'https://github.com/temporalio/money-transfer-project-java' ;; + dotnet|.net|net) printf 'https://github.com/temporalio/money-transfer-project-template-dotnet' ;; + ruby) printf 'https://github.com/temporalio/money-transfer-project-template-ruby' ;; + *) printf '' ;; + esac +} + +# ---- per-SDK run matrix (verified against the money-transfer-project-cloud-setup +# branches; mirrors references/sdk-cloud.md). Worker command is long-running; the +# starter blocks until the Workflow returns its result. These are the SINGLE source +# of truth for the run step so the agent never guesses entrypoints/targets. +worker_cmd_for() { + case "$1" in + python) printf 'source env/bin/activate && python run_worker.py' ;; + go) printf 'go run worker/main.go' ;; + ts|typescript) printf 'npm run worker' ;; + java) printf 'mvn -q compile exec:java -Dexec.mainClass=moneytransferapp.MoneyTransferWorker -Dorg.slf4j.simpleLogger.defaultLogLevel=warn' ;; + dotnet|.net|net) printf 'dotnet run --project MoneyTransferWorker' ;; + ruby) printf 'ruby worker.rb' ;; + *) printf '' ;; + esac +} +starter_cmd_for() { + case "$1" in + python) printf 'source env/bin/activate && python run_workflow.py' ;; + go) printf 'go run start/main.go' ;; + ts|typescript) printf 'npm run client' ;; + java) printf 'mvn -q compile exec:java -Dexec.mainClass=moneytransferapp.TransferApp -Dorg.slf4j.simpleLogger.defaultLogLevel=warn' ;; + dotnet|.net|net) printf 'dotnet run --project MoneyTransferClient' ;; + ruby) printf 'ruby starter.rb' ;; + *) printf '' ;; + esac +} +# task-queue name the worker polls — the readiness probe checks for a poller here. +task_queue_for() { + case "$1" in + python|go) printf 'TRANSFER_MONEY_TASK_QUEUE' ;; + java|dotnet|.net|net) printf 'MONEY_TRANSFER_TASK_QUEUE' ;; + ts|typescript|ruby) printf 'money-transfer' ;; + *) printf '' ;; + esac +} + +# ---- per-(SDK, package-manager) adaptation matrices (PE-75) ------------------ +# Single source of truth for which managers each SDK sample ACTUALLY supports, +# derived from a Step-0 audit of the money-transfer-project-cloud-setup branches +# (see references/sdk-cloud.md). Only python and ts ship a choice; the rest are +# single-manager. We never offer a manager the sample can't honor (e.g. poetry on +# the python sample, which ships no pyproject.toml). + +# managers_for -> space-separated candidate managers in PREFERENCE order +# (default = first one whose binary is on PATH). For SDKs that ship a lockfile the +# lockfile's manager leads, so the default honors the committed lockfile (ts->npm). +managers_for() { + case "$1" in + python) printf 'pip uv' ;; # sample has no manifest; pip default, uv faster override + ts|typescript) printf 'npm pnpm yarn' ;; # package-lock.json committed -> npm leads + go) printf 'go' ;; + java) printf 'maven' ;; # pom.xml only; no mvnw/gradlew/build.gradle + dotnet|.net|net) printf 'dotnet' ;; + ruby) printf 'bundler' ;; + *) printf '' ;; + esac +} + +# runtime_for -> the language-runtime binary whose version we check/warn on. +runtime_for() { + case "$1" in + python) printf 'python3' ;; + ts|typescript) printf 'node' ;; + go) printf 'go' ;; + java) printf 'java' ;; + dotnet|.net|net) printf 'dotnet' ;; + ruby) printf 'ruby' ;; + *) printf '' ;; + esac +} + +# manager_bin -> the executable to look for on PATH for that manager. +manager_bin() { + case "$1" in + pip) printf 'python3' ;; # pip is invoked as `python3 -m pip` + uv) printf 'uv' ;; + npm) printf 'npm' ;; + pnpm) printf 'pnpm' ;; + yarn) printf 'yarn' ;; + go) printf 'go' ;; + maven) printf 'mvn' ;; + dotnet) printf 'dotnet' ;; + bundler) printf 'bundle' ;; + *) printf '' ;; + esac +} + +# min_version_for -> minimum tested version (advisory; a shortfall is a +# WARNING with remediation, never an auto-fix or a hard block). Empty = no minimum. +min_version_for() { + case "$1" in + python3) printf '3.8' ;; + node) printf '18' ;; # Node 16 is EOL; @temporalio/* targets 18+ + go) printf '1.21' ;; + java) printf '8' ;; # Temporal Java SDK supports JDK 8+ (deliberately below the 11 line) + dotnet) printf '6.0' ;; + ruby) printf '3.0' ;; + uv) printf '0.1' ;; + npm) printf '8' ;; + pnpm) printf '8' ;; + yarn) printf '1.22' ;; + maven) printf '3.6' ;; # mvn + bundler) printf '2.0' ;; # bundle + *) printf '' ;; + esac +} + +# install_cmd_for -> the dependency-install command string. +# SINGLE SOURCE OF TRUTH for installs, shared by cmd_install_deps, the internal +# provision-and-scaffold path, and `preview`. Empty = unsupported (sdk,manager) pair. +install_cmd_for() { + local sdk="$1" mgr="$2" + case "$sdk" in + python) + case "$mgr" in + pip) printf 'python3 -m venv env && . env/bin/activate && python -m pip install -q temporalio' ;; + uv) printf 'uv venv env && . env/bin/activate && uv pip install -q temporalio' ;; + *) printf '' ;; + esac ;; + ts|typescript) + case "$mgr" in + npm) printf 'npm install' ;; + pnpm) printf 'pnpm install' ;; + yarn) printf 'yarn install' ;; + *) printf '' ;; + esac ;; + go) [ "$mgr" = go ] && printf 'go mod download' ;; + java) [ "$mgr" = maven ] && printf 'mvn -q -DskipTests dependency:resolve' ;; + dotnet|.net|net) [ "$mgr" = dotnet ] && printf 'dotnet restore' ;; + ruby) [ "$mgr" = bundler ] && printf 'bundle install' ;; + *) printf '' ;; + esac +} + +# install_location_note -> short phrase stating WHERE the install writes, so the +# disclosure is explicit when deps land in a shared GLOBAL cache outside the repo +# (Go/Java/.NET/Ruby) vs. a location local to the clone (Python venv, TS node_modules). +install_location_note() { + case "$1" in + python) printf 'into a local venv (env/) inside the repo' ;; + ts|typescript) printf 'into node_modules inside the repo' ;; + go) printf 'into the shared Go module cache - GLOBAL, outside the repo (~/go/pkg/mod)' ;; + java) printf 'into the shared Maven cache - GLOBAL, outside the repo (~/.m2)' ;; + dotnet|.net|net) printf 'into the global NuGet cache - GLOBAL, outside the repo (~/.nuget/packages)' ;; + ruby) printf 'into globally-installed gems - GLOBAL, outside the repo' ;; + *) printf 'into the repo' ;; + esac +} + +# resolve_pkg_inputs — resolve the package manager and its +# install command for an SDK. Sets globals PKG_MGR / PKG_ICMD / PKG_CANDIDATES and +# returns 0; returns 2 if the SDK has no manager mapping, 3 if the chosen manager isn't +# supported. Uses globals + a return code (not stdout + `die`) on purpose: a `die` inside +# a `$(...)` call would exit only the subshell, so the CALLER maps the code to the error. +resolve_pkg_inputs() { + local sdk="$1" mgr_in="$2" + PKG_CANDIDATES="$(managers_for "$sdk")" + [ -n "$PKG_CANDIDATES" ] || return 2 + PKG_MGR="${mgr_in:-${PKG_CANDIDATES%% *}}" + PKG_ICMD="$(install_cmd_for "$sdk" "$PKG_MGR")" + [ -n "$PKG_ICMD" ] || return 3 + return 0 +} + +# die_pkg — map resolve_pkg_inputs' non-zero return to the right error. +die_pkg() { + case "$1" in + 2) die unknown-sdk "no package-manager mapping for SDK '$2'" ;; + 3) die unsupported-manager "manager '$PKG_MGR' not supported for SDK '$2' (supported: $(printf '%s' "$PKG_CANDIDATES" | tr ' ' ','))" ;; + esac +} + +# ver_norm -> leading dotted-numeric version (e.g. "go1.22.0"->"1.22.0"). +ver_norm() { printf '%s' "$1" | grep -oE '[0-9]+(\.[0-9]+){0,3}' | head -n1; } + +# ver_ge -> exit 0 if HAVE >= WANT (component-wise, bash 3.2-safe). +# Splits on '.' via a saved/restored IFS — NOT a `... | read` pipe, which would +# run read in a subshell and lose the captured components. +ver_ge() { + local have want h1 h2 h3 w1 w2 w3 oldifs + have="$(ver_norm "$1")"; want="$(ver_norm "$2")" + [ -n "$have" ] || return 1 + [ -n "$want" ] || return 0 + oldifs="$IFS"; IFS=. + set -- $have; h1="${1:-0}"; h2="${2:-0}"; h3="${3:-0}" + set -- $want; w1="${1:-0}"; w2="${2:-0}"; w3="${3:-0}" + IFS="$oldifs" + if [ "$h1" -ne "$w1" ]; then [ "$h1" -gt "$w1" ]; return; fi + if [ "$h2" -ne "$w2" ]; then [ "$h2" -gt "$w2" ]; return; fi + [ "$h3" -ge "$w3" ] +} + +# tool_version -> a bare version string, or empty if +# absent/unparseable. Handles the binaries whose --version output is non-standard +# (go, java's legacy 1.8 scheme on stderr). +tool_version() { + local bin="$1" raw v + require_cmd "$bin" || return 0 + case "$bin" in + go) raw="$(go version 2>/dev/null)" ;; + java) raw="$(java -version 2>&1 | head -n1)" ;; # java prints version to stderr + mvn) raw="$(mvn -version 2>/dev/null | head -n1)" ;; + *) raw="$("$bin" --version 2>&1 | head -n1)" ;; + esac + v="$(ver_norm "$raw")" + # Java's legacy "1.8.0_292" scheme: map 1.N -> N so numeric compare treats 8 as 8. + if [ "$bin" = java ]; then + case "$v" in 1.*) v="${v#1.}" ;; esac + fi + printf '%s' "$v" +} + +# ---- JSON field extraction (jq preferred, python3 fallback) ----------------- + +json_field() { # json_field + local file="$1" field="$2" + if require_cmd jq; then + jq -r --arg f "$field" '.[$f] // empty' "$file" 2>/dev/null + elif require_cmd python3; then + python3 -c 'import json,sys +try: + d=json.load(open(sys.argv[1])) + v=d.get(sys.argv[2]) + print(v if v is not None else "") +except Exception: + print("")' "$file" "$field" + else + printf '' + fi +} + +# ============================================================================= +# Subcommands +# ============================================================================= + +cmd_preflight() { + # SDK is optional and only used to echo the repo_url. Accept --sdk (the + # convention every other subcommand uses) and a bare positional for back-compat. + local sdk="" + while [ $# -gt 0 ]; do + case "$1" in + --sdk) sdk="$2"; shift 2 ;; + -*) die bad-args "unknown arg: $1" ;; + *) sdk="$1"; shift ;; + esac + done + log "Preflight checks..." + local os; os="$(uname -s)" + local cfg; cfg="$(config_path)" + + local warnings=() + require_cmd git || warnings+=("git-missing") + if ! require_cmd jq && ! require_cmd python3; then + warnings+=("no-json-parser") # needed to capture the API key safely + fi + if [ "$os" = "Darwin" ] && ! require_cmd brew; then + warnings+=("brew-missing") # only a problem if the CLI also isn't installed + fi + # Stray TEMPORAL_* env vars OVERRIDE the TOML profile (env-config precedence). + local stray=() + [ -n "${TEMPORAL_ADDRESS:-}" ] && stray+=("TEMPORAL_ADDRESS") + [ -n "${TEMPORAL_NAMESPACE:-}" ] && stray+=("TEMPORAL_NAMESPACE") + [ -n "${TEMPORAL_API_KEY:-}" ] && stray+=("TEMPORAL_API_KEY") + + result_open ok + result_kv os "$os" + result_kv config_path "$cfg" + result_kv cli_installed "$(cloud_cli_present && echo true || echo false)" + [ -n "$sdk" ] && result_kv repo_url "$(repo_url_for_sdk "$sdk")" + result_kv warnings "$(IFS=,; echo "${warnings[*]:-}")" + result_kv stray_env "$(IFS=,; echo "${stray[*]:-}")" + result_close +} + +# cmd_detect_tools: PE-75 local-setup adaptation (Pillar 2). For the chosen SDK, +# detect which package managers the sample supports AND are installed, pick a +# deterministic default (first available in preference order), report tool +# versions, and surface discrepancies (missing runtime/manager, version-too-old) +# so the agent can present them EARLY (Phase 1) — not deep in a later phase. +# Side-effect free: only reads versions, never installs or clones. Deterministic: +# same environment -> same default every run (no state file). +cmd_detect_tools() { + local sdk="" + while [ $# -gt 0 ]; do + case "$1" in + --sdk) sdk="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + [ -n "$sdk" ] || die bad-args "--sdk is required" + local candidates; candidates="$(managers_for "$sdk")" + [ -n "$candidates" ] || die unknown-sdk "no package-manager mapping for SDK '$sdk'" + + log "Detecting tools for '$sdk'..." + local runtime rt_ver rt_min available="" default="" versions="" disc="" + runtime="$(runtime_for "$sdk")" + + # Runtime: presence + version vs. minimum. + rt_ver="$(tool_version "$runtime")" + rt_min="$(min_version_for "$runtime")" + if [ -z "$rt_ver" ]; then + disc="${disc:+$disc,}tool-missing:$runtime" + else + versions="${versions:+$versions,}$runtime@$rt_ver" + if [ -n "$rt_min" ] && ! ver_ge "$rt_ver" "$rt_min"; then + disc="${disc:+$disc,}version-too-old:$runtime@$rt_ver(min$rt_min)" + fi + fi + + # Managers: build the available list (preference order) + record versions/discrepancies. + local m bin ver vmin + for m in $candidates; do + bin="$(manager_bin "$m")" + if require_cmd "$bin"; then + available="${available:+$available,}$m" + [ -z "$default" ] && default="$m" + ver="$(tool_version "$bin")" + [ -n "$ver" ] && versions="${versions:+$versions,}$m@$ver" + vmin="$(min_version_for "$m")" + if [ -n "$ver" ] && [ -n "$vmin" ] && ! ver_ge "$ver" "$vmin"; then + disc="${disc:+$disc,}version-too-old:$m@$ver(min$vmin)" + fi + fi + done + # No supported manager installed at all -> name the preferred (first candidate) as missing. + if [ -z "$available" ]; then + disc="${disc:+$disc,}manager-not-found:${candidates%% *}" + fi + + result_open ok + result_kv sdk "$sdk" + result_kv runtime "${rt_ver:+$runtime@$rt_ver}" + result_kv managers "$available" + result_kv default "$default" + result_kv candidates "$(printf '%s' "$candidates" | tr ' ' ',')" + result_kv versions "$versions" + result_kv discrepancies "$disc" + result_close +} + +# cloud_cli_present -> 0 iff the unified CLI's `cloud` command GROUP works. `temporal` +# alone can be the OSS CLI without the cloud group, so test the group, not just the +# binary. Shared by preflight (cli_installed) and install-cli so the two presence +# checks never disagree. (`temporal cloud version` isn't a real subcommand — it prints +# the group's help and exits 0 — but that makes it a fine *presence* probe: it exits +# non-zero only when `temporal` is absent or has no `cloud` group.) +cloud_cli_present() { temporal cloud version >/dev/null 2>&1; } + +# cli_version -> the real CLI version line (e.g. "temporal version 1.7.2 (...)"). +# NB: from `temporal --version`, NOT `temporal cloud version` (the latter prints help, +# which is the "version=The Temporal Cloud CLI provides commands…" bug). +cli_version() { temporal --version 2>/dev/null | head -n1; } + +cmd_install_cli() { + if cloud_cli_present; then + log "Temporal CLI already present — skipping install." + result_open skipped + result_kv version "$(cli_version)" + result_close + return + fi + case "$(uname -s)" in + Darwin) + if require_cmd brew; then + log "Installing temporal-cloud via Homebrew..." + if brew install temporalio/prerelease/temporal-cloud >&2; then + # (b) Verify the install actually took, rather than trusting brew's exit. + if ! cloud_cli_present; then + die install-verify-failed "brew reported success but 'temporal cloud version' still fails — check PATH / shell rehash, then re-run." + fi + result_open ok + result_kv installed_via brew + result_kv version "$(cli_version)" + result_close; return + fi + die install-failed "brew install temporalio/prerelease/temporal-cloud failed; see output above" + fi + die brew-missing "Homebrew not found. Install from https://brew.sh, or download temporal-cloud from https://github.com/temporalio/cloud-cli/releases/latest and put it on PATH." + ;; + *) + die manual-install "Download temporal-cloud from https://github.com/temporalio/cloud-cli/releases/latest, extract, and put it on PATH; then re-run." + ;; + esac +} + +cmd_login() { + # `temporal cloud login` opens a browser and BLOCKS until the user finishes. + log "Opening browser for Temporal Cloud sign-in... complete it in the browser." + if ! temporal cloud login >&2; then + die login-failed "temporal cloud login did not complete; finish the browser sign-in and retry." + fi + local who; who="$(temporal cloud whoami 2>/dev/null | head -n1)" + if [ -z "$who" ]; then + die not-authenticated "whoami returned empty after login — sign-in may not have completed." + fi + result_open ok; result_kv identity "$who"; result_close +} + +cmd_regions() { + # Re-verify auth cheaply first. + if ! temporal cloud whoami >/dev/null 2>&1; then + die not-authenticated "Not signed in. Run the login step first." + fi + log "Listing available Cloud regions..." + local list; list="$(temporal cloud region list 2>/dev/null)" + if [ -z "$list" ]; then + die regions-empty "region list returned empty; confirm auth (whoami) and CLI version." + fi + # Region guard: flag regions whose CloudProvider renders as UNKNOWN. On some accounts + # those (e.g. azure-centralus) ACCEPT a namespace create but never provision it (a + # phantom that later stalls await-namespace). Emit them so the agent can warn the user + # and steer to a known AWS/GCP region. ($1 = region id, $2 = CloudProvider column.) + local unsupported + unsupported="$(printf '%s\n' "$list" | awk '$2=="UNKNOWN"{print $1}' | tr '\n' ',' | sed 's/,$//')" + # Echo the raw list to stderr so the agent can present it; emit nothing + # secret. Recommendation is left to the agent (timezone heuristic) so the + # script never invents a region that isn't in the list. + printf '%s\n' "$list" >&2 + result_open ok + [ -n "$unsupported" ] && result_kv unsupported_regions "$unsupported" + result_close +} + +# user_tag -> up to 8 [a-z0-9] from the Temporal user's email LOCAL-PART (everything +# before '@'; the '@' and domain are never included, so the name is never email-shaped). +# Falls back to 'user' if the email is unavailable or sanitizes to empty. +user_tag() { + local email="" + if require_cmd jq; then + email="$(temporal cloud whoami --output json 2>/dev/null | jq -r '.user.spec.email // empty' 2>/dev/null)" + elif require_cmd python3; then + email="$(temporal cloud whoami --output json 2>/dev/null | python3 -c 'import json,sys +try: + print(json.load(sys.stdin).get("user",{}).get("spec",{}).get("email","") or "") +except Exception: + print("")' 2>/dev/null)" + fi + local tag + tag="$(printf '%s' "$email" | tr '[:upper:]' '[:lower:]' | cut -d@ -f1 | tr -cd 'a-z0-9' | cut -c1-8)" + [ -n "$tag" ] || tag="user" + printf '%s' "$tag" +} + +# rand_tag -> 8 random [a-z0-9] for per-namespace uniqueness (portable: /dev/urandom). +rand_tag() { + local r + r="$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom 2>/dev/null | head -c 8)" + [ "${#r}" -eq 8 ] || r="$(date +%s | tail -c 9)" # fallback if /dev/urandom is unavailable + printf '%s' "$r" +} + +# namespace_name_for -> unique name: quickstartai-- +# = up to 8 chars of the Temporal user's email local-part (see user_tag) +# = 8 random [a-z0-9] (see rand_tag) for per-namespace uniqueness +# No SDK / timestamp / telemetry marker — just an anonymized per-user tag + uniqueness. +# Cloud appends . itself, which does NOT count toward the <=39 char limit. +# (The SDK arg is ignored now; kept for call-site compatibility.) +namespace_name_for() { + local name + name="quickstartai-$(user_tag)-$(rand_tag)" + [ "${#name}" -le 39 ] || die name-too-long "namespace name '$name' exceeds 39 chars" + printf '%s' "$name" +} + +# namespace_active_info -> prints "|" + returns 0 when +# the namespace EXISTS and is ACTIVE. Return codes let the caller tell a phantom (a create +# that was accepted but never provisions) from a slow-but-real provision: +# 0 = ACTIVE (handle|address printed) +# 3 = EXISTS but not yet ACTIVE (ACTIVATING) — genuinely provisioning +# 1 = ABSENT (not in the list) or list unreadable +# +# Why the server-side exact `--name` filter (not a plain `namespace list`): an account +# can hold hundreds of namespaces and `namespace list` PAGES at 100, so scanning a single +# unfiltered page silently misses ours when its name sorts past the first page. The old +# resolver did exactly that, then fell back to CONSTRUCTING `.` from another +# entry and returning immediately — so it reported "provisioned" on the first poll +# without ever confirming the namespace existed or was ready, leaving create-key/await-auth +# to run against an endpoint that wasn't serving yet ("no children to pick from"). The +# filtered result also carries the authoritative endpoint, so we COPY the address from +# the API response — never assemble it. Primary = `endpoints.mtls_grpc_address` (the +# tmprl.cloud form API-key auth uses), fallback = `endpoints.grpc_address` (regional); +# both straight from the response. (Constructing it broke for providers whose endpoint +# isn't the `.tmprl.cloud` shape.) Needs jq or python3. +# +# State: this CLI build reports ACTIVE as state==3 (the `namespace list` table column +# renders 3 as "ACTIVE"); a just-created namespace is state==1 (ACTIVATING) for a few +# minutes. API-key auth uses mtls_grpc_address (`.tmprl.cloud:7233`), which only +# starts serving once the namespace reaches ACTIVE. +namespace_active_info() { + local name="$1" raw fields handle state mtls grpc addr + raw="$(temporal cloud namespace list --name "$name" -o json 2>/dev/null)" || return 1 + [ -n "$raw" ] || return 1 + # Pull all four fields in ONE pass (jq, or python3 fallback), pipe-delimited. Use '|' + # (not tab) as the delimiter so IFS-splitting preserves EMPTY fields — none of the + # values (handle, numeric state, host:port endpoints) ever contains a '|'. + if require_cmd jq; then + fields="$(printf '%s' "$raw" | jq -r 'if (.Namespaces|length)>0 then (.Namespaces[0] | [(.namespace//""),((.state // "")|tostring),(.endpoints.mtls_grpc_address//""),(.endpoints.grpc_address//"")] | join("|")) else "" end' 2>/dev/null)" + elif require_cmd python3; then + fields="$(printf '%s' "$raw" | python3 -c 'import json,sys +try: + a=json.load(sys.stdin).get("Namespaces",[]); x=a[0] if a else {} + e=x.get("endpoints",{}) or {} + print("|".join([str(x.get("namespace","")), str(x.get("state","")), str(e.get("mtls_grpc_address","")), str(e.get("grpc_address",""))]) if a else "") +except Exception: + print("")')" + else + return 1 + fi + IFS='|' read -r handle state mtls grpc < -> poll until the namespace is ACTIVE. +# Echoes "|
" + returns 0 on success. Return codes distinguish the two +# failure modes so the caller can message them differently: +# 2 = PHANTOM — the namespace never appeared in the list within NS_PHANTOM_GRACE_SECS. +# A real create shows up as ACTIVATING within seconds; persistent absence means the +# async create was accepted but isn't provisioning (commonly an unavailable region, +# e.g. azure-centralus whose provider reads UNKNOWN). Fail FAST instead of burning +# the full --max-secs. +# 1 = TIMEOUT — it appeared (ACTIVATING) but didn't reach ACTIVE within --max-secs. +# This is where the provisioning wait BELONGS (it used to leak into await-auth, which then +# mislabeled a still-provisioning namespace as "API key rejected"). +await_active_namespace() { + local name="$1" max="${2:-600}" waited=0 interval=2 info rc ever_seen=0 + local grace="${NS_PHANTOM_GRACE_SECS:-75}" + while :; do + info="$(namespace_active_info "$name")"; rc=$? + [ "$rc" -eq 0 ] && { printf '%s' "$info"; return 0; } + [ "$rc" -eq 3 ] && ever_seen=1 # appeared (ACTIVATING) -> genuinely provisioning + [ "$ever_seen" -eq 0 ] && [ "$waited" -ge "$grace" ] && return 2 # never showed up -> phantom + [ "$waited" -ge "$max" ] && return 1 + sleep "$interval"; waited=$(( waited + interval )) + done +} + +# cmd_start_namespace: TRUE fire-and-forget (PE-75). Submit the create with --async +# and return immediately — provisioning runs SERVER-SIDE (no local background job to +# survive across tool calls, so this works identically on Claude Code/Codex/Cursor). +# We GENERATE the name (namespace_name_for), so we don't need the create's output; +# join later with `await-namespace --name `, which reads the handle from +# `namespace list -o jsonl` (the reason --async was once avoided — its early return +# lacked the account-id — no longer matters now that we resolve the handle from list). +cmd_start_namespace() { + local sdk="" region="" + while [ $# -gt 0 ]; do + case "$1" in + --sdk) sdk="$2"; shift 2 ;; + --region) region="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + [ -n "$sdk" ] || die bad-args "--sdk is required" + [ -n "$region" ] || die bad-args "--region is required" + temporal cloud whoami >/dev/null 2>&1 || die not-authenticated "Not signed in; run login again." + + local name out rc + name="$(namespace_name_for "$sdk")" + log "Submitting namespace '$name' in '$region' (--async; provisions on Temporal's servers)..." + # --async returns as soon as the create is accepted; the namespace then provisions + # server-side over a few minutes. A non-zero exit here is a SUBMIT failure (region/ + # name), surfaced immediately so a bad value never leaves a half-created namespace. + # Capture rc on the SAME line — a later `printf | tail` (or the `if !` negation) would + # reset $? to the pipeline's status and mask the real create exit code. + out="$(temporal cloud namespace create \ + --name "$name" --region "$region" \ + --api-key-auth-enabled --retention-days 30 --auto-confirm --async 2>&1)"; rc=$? + if [ "$rc" -ne 0 ]; then + printf '%s\n' "$out" | tail -n 20 >&2 + die create-rejected "namespace create --async was rejected (exit $rc) — usually region or name format; see output above." + fi + printf '%s\n' "$out" >&2 + + result_open ok + result_kv namespace_name "$name" + result_kv region "$region" + result_close +} + +# cmd_await_namespace: the JOIN. Poll `namespace list --name -o json` until the +# async namespace is ACTIVE (state==3), bounded by NS_AWAIT_MAX_SECS. Reads the handle and +# endpoint from that authoritative, pagination-immune result. Gating on ACTIVE (not merely +# "visible") is the fix for the false-ready bug that left downstream steps connecting to a +# still-provisioning endpoint. +cmd_await_namespace() { + local name="" max="${NS_AWAIT_MAX_SECS:-600}" + while [ $# -gt 0 ]; do + case "$1" in + --name) name="$2"; shift 2 ;; + --max-secs) max="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + [ -n "$name" ] || die bad-args "--name is required (namespace_name from start-namespace)" + temporal cloud whoami >/dev/null 2>&1 || die not-authenticated "Not signed in; run login again." + + log "Waiting for namespace '$name' to become ACTIVE (polling 'namespace list --name')..." + local info + local rc + info="$(await_active_namespace "$name" "$max")"; rc=$? + case "$rc" in + 0) ;; + 2) die namespace-not-provisioning "namespace '$name' never appeared after ~${NS_PHANTOM_GRACE_SECS:-75}s — the create was accepted but isn't provisioning (commonly an unavailable region, e.g. azure-centralus). Re-run start-namespace with an AWS/GCP region." ;; + *) die namespace-timeout "namespace '$name' not ACTIVE after ~${max}s; a brand-new namespace can take a few minutes to provision. Re-run await-namespace to resume the wait. Do NOT switch endpoints or edit the profile." ;; + esac + + result_open ok + result_kv namespace_handle "${info%%|*}" + result_kv address "${info#*|}" + result_close +} + +# cmd_create_namespace: blocking convenience wrapper = start + await in one call +# (preserves the simple synchronous op for tests / a non-parallel fallback). To +# overlap provisioning with clone+deps, call start-namespace / await-namespace +# directly instead. +cmd_create_namespace() { + local sdk="" region="" + while [ $# -gt 0 ]; do + case "$1" in + --sdk) sdk="$2"; shift 2 ;; + --region) region="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + [ -n "$sdk" ] || die bad-args "--sdk is required" + [ -n "$region" ] || die bad-args "--region is required" + + # Start (RESULT captured to extract the generated name; its logs flow to stderr), + # then await prints the single final RESULT. + local start_out name + start_out="$(cmd_start_namespace --sdk "$sdk" --region "$region")" + name="$(printf '%s\n' "$start_out" | sed -n 's/^namespace_name=//p')" + if [ -z "$name" ]; then + printf '%s\n' "$start_out" # re-emit the start error RESULT + exit 1 + fi + cmd_await_namespace --name "$name" +} + +cmd_create_key() { + # Unique display name per run: `apikey create-for-me` ERRORS when a key matching the spec + # (same display name) already exists and --idempotent isn't set — and --idempotent would + # "succeed" without returning a fresh token (it's write-once), which we need. A random + # suffix guarantees a brand-new key every time, so a re-run (or re-mint) never conflicts. + local handle="" address="" keyname="money-transfer-cloud-setup-$(rand_tag)" + while [ $# -gt 0 ]; do + case "$1" in + --handle) handle="$2"; shift 2 ;; + --address) address="$2"; shift 2 ;; + --display-name) keyname="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + [ -n "$handle" ] || die bad-args "--handle is required (from create-namespace)" + [ -n "$address" ] || die bad-args "--address is required (from create-namespace)" + if ! require_cmd jq && ! require_cmd python3; then + die no-json-parser "need jq or python3 to capture the API key safely; install one and retry." + fi + + # Re-verify auth immediately before minting the key. + temporal cloud whoami >/dev/null 2>&1 || die not-authenticated "Not signed in; run login again before the key step." + + # Capture the key WITHOUT printing it. The token goes only into 0600 temp files and + # is read straight into a shell var. We capture stdout AND stderr to SEPARATE locked + # files: this prerelease CLI has emitted the freshly-minted key to stderr / human text + # rather than as JSON on stdout, so the old `2>/dev/null` discarded the token and the + # result looked empty (`key-empty`). We parse JSON when it's there and fall back to a + # JWT-pattern match over EITHER stream, so capture works regardless of output shape. + umask 077 + local tmp tmperr + tmp="$(mktemp "${TMPDIR:-/tmp}/tcloud-key.XXXXXX")" + tmperr="$(mktemp "${TMPDIR:-/tmp}/tcloud-key.XXXXXX")" + chmod 600 "$tmp" "$tmperr" + # shellcheck disable=SC2064 + trap "rm -f '$tmp' '$tmperr'" EXIT + + log "Creating API key (token captured to a locked file — never printed)..." + if ! temporal cloud apikey create-for-me \ + --display-name "$keyname" \ + --description "money-transfer Cloud setup" \ + --expiry-duration 25h \ + --auto-confirm \ + -o json > "$tmp" 2>"$tmperr"; then + # Show a redacted tail for context — never the token. + sed -E 's/eyJ[A-Za-z0-9._-]{6,}/eyJ...(redacted)/g' "$tmperr" 2>/dev/null | tail -n 5 >&2 + die key-create-failed "apikey create-for-me exited non-zero; re-check auth (whoami/login). See the redacted output above." + fi + if [ ! -s "$tmp" ] && [ ! -s "$tmperr" ]; then + die key-empty "apikey create returned nothing on either stream — almost always an expired login; run whoami/login and retry once." + fi + + local token keyid + # 1. Preferred: structured JSON on stdout. + token="$(json_field "$tmp" token)" + [ -n "$token" ] || token="$(json_field "$tmp" secretKey)" + keyid="$(json_field "$tmp" keyId)" + [ -n "$keyid" ] || keyid="$(json_field "$tmp" id)" + # 2. Fallback for output drift: the key was emitted to stderr or as human text. Pull + # the JWT by pattern from EITHER stream (API keys are JWTs: 'eyJ' + two more + # dot-separated base64url segments). Stays in a var — never printed, never argv. + if [ -z "$token" ]; then + token="$(grep -hoE 'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' "$tmp" "$tmperr" 2>/dev/null | head -n1)" + fi + if [ -z "$keyid" ]; then # key_id is NOT secret; best-effort scrape if JSON wasn't present + keyid="$(grep -hoiE 'key[ _-]?id["'"'"':= ]+[A-Za-z0-9_-]{16,}' "$tmp" "$tmperr" 2>/dev/null | head -n1 | grep -oE '[A-Za-z0-9_-]{16,}$')" + fi + if [ -z "$token" ]; then + # (a) Automatic capture failed (no JSON, no JWT pattern). Fall back to a HIDDEN + # paste straight from the controlling terminal — the token goes into a local var + # and then into the locked file; it is never echoed, printed, or passed as argv. + # If there is no interactive tty, bail with a code the agent maps to "ask the user + # to paste, then re-run". + if [ -r /dev/tty ]; then + log "Automatic key capture failed. Paste the one-time API key (input hidden), then press Enter:" + IFS= read -rs -t 120 token < /dev/tty || token="" # -t guards against a hang when no input arrives + printf '\n' >&2 + fi + fi + if [ -z "$token" ]; then + die manual-key-needed "could not capture the key from the CLI output (checked stdout+stderr, JSON + token pattern) and no terminal to paste into — have the user paste it, then re-run create-key from a context with a terminal." + fi + + # Write the cloud-setup profile straight into the TOML (default untouched). + write_profile "$handle" "$address" "$token" + # token goes out of scope; wipe the temp files now (trap is the backstop). + rm -f "$tmp" "$tmperr"; trap - EXIT + + local cfg; cfg="$(config_path)" + result_open ok + result_kv key_id "${keyid:-unknown}" # KeyId is NOT secret — safe to show + result_kv config_path "$cfg" + result_kv profile cloud-setup + result_close +} + +# strip_cloud_setup -> stdout +# Emits with EVERY [profile.cloud-setup] and [profile.cloud-setup.tls] +# section removed, preserving all other profiles (notably [profile.default] and +# its oauth/login session). Purely textual line-skipping, so it repairs a file +# that duplicate sections have already made TOML-invalid. Tolerates leading +# whitespace on headers; the (\.|\]) boundary keeps it from matching a different +# profile that merely shares the prefix (e.g. [profile.cloud-setup-staging]). +strip_cloud_setup() { + awk ' + /^[[:space:]]*\[profile\.cloud-setup(\.|\])/ { skip=1; next } + /^[[:space:]]*\[/ { if (skip) skip=0 } + skip { next } + { print } + ' "$1" +} + +# write_profile
+# Replaces any existing [profile.cloud-setup(.tls)] block(s), preserves +# everything else (notably [profile.default] with its oauth/login session), +# chmod 600. +write_profile() { + local handle="$1" address="$2" token="$3" + local cfg dir tmp oldumask + # Self-contained perms: don't depend on the caller's umask. The temp file holds the + # live token, so it must be 0600 from the moment of creation; tighten the config dir too. + oldumask="$(umask)"; umask 077 + cfg="$(config_path)" + dir="$(dirname "$cfg")" + mkdir -p "$dir"; chmod 700 "$dir" 2>/dev/null || true + tmp="$(mktemp "${dir}/temporal.toml.XXXXXX")" + chmod 600 "$tmp" + # A failed mv (read-only target, full disk) must NOT strand a 0600 temp file holding the + # token. RETURN trap is the backstop for any early return; the mv path rm's explicitly. + # shellcheck disable=SC2064 + trap "rm -f '$tmp'" RETURN + + if [ -f "$cfg" ]; then + strip_cloud_setup "$cfg" > "$tmp" # drop existing/duplicate cloud-setup section(s) + fi + + { + printf '\n[profile.cloud-setup]\n' + printf 'address = "%s"\n' "$address" + printf 'namespace = "%s"\n' "$handle" + printf 'api_key = "%s"\n' "$token" + printf '\n[profile.cloud-setup.tls]\n' + printf 'disabled = false\n' # TLS is required for Temporal Cloud; set it explicitly (empty section left it ambiguous, causing connect failures) + } >> "$tmp" + + if ! mv "$tmp" "$cfg"; then + rm -f "$tmp"; umask "$oldumask" + die config-write-failed "could not write the profile to $cfg — check directory permissions / free disk, then re-run create-key." + fi + chmod 600 "$cfg" + umask "$oldumask" + log "Wrote [profile.cloud-setup] to $cfg (chmod 600; api_key not shown)." +} + +cmd_verify_config() { + # `config list` lists profile names, not the key value — but scrub defensively + # so that even a CLI version that DID echo the key can't leak it through here. + local out + if ! out="$(temporal --profile cloud-setup config list 2>&1)"; then + die profile-missing "could not read the cloud-setup profile; was create-key run?" + fi + printf '%s\n' "$out" \ + | sed -E 's/([Aa][Pp][Ii][_-]?[Kk][Ee][Yy][[:space:]]*[=:]).*/\1 (redacted)/g; s/eyJ[A-Za-z0-9._-]{6,}/eyJ...(redacted)/g' >&2 + result_open ok; result_kv profile cloud-setup; result_close +} + +# cmd_await_auth: deterministic readiness gate to run BEFORE the Worker. A +# freshly-minted API key (and the namespace's just-enabled API-key auth) is not +# accepted by the data plane immediately, so a Worker that connects too soon hits +# `Request unauthorized` and crashes. Poll the cheapest authorized data-plane call +# (`workflow list`, exit 0 == accepted) until it succeeds, bounded. Foreground, +# synchronous — no background, no parallelism. +cmd_await_auth() { + local max="${AUTH_READY_MAX_SECS:-90}" interval=5 waited=0 + log "Waiting for the API key to be accepted before starting the Worker (auth readiness)..." + while :; do + if temporal --profile cloud-setup workflow list --limit 1 >/dev/null 2>&1; then + result_open ok; result_kv auth_ready true; result_kv waited_secs "$waited"; result_close + return 0 + fi + if [ "$waited" -ge "$max" ]; then + die auth-timeout "API key still not accepted after ${max}s. A just-minted key/namespace can lag — wait and re-run await-auth; if it never clears, re-run create-key. Do NOT switch endpoints or edit the profile." + fi + sleep "$interval" + waited=$(( waited + interval )) + done +} + +# worker_polling -> exit 0 if >=1 Worker is polling the queue. +# API-based readiness: `task-queue describe -o json` reports a top-level `pollers` +# array (null/empty when no Worker has polled). This is a DATA-plane call (works on +# the prerelease, unlike the control-plane `cloud namespace get -o json`); it also +# returns non-zero while auth is not yet accepted, which the caller treats as "not +# polling yet" (the Worker would be failing for the same reason). +worker_polling() { + local tq="$1" raw n + raw="$(temporal --profile cloud-setup task-queue describe --task-queue "$tq" -o json 2>/dev/null)" || return 1 + [ -n "$raw" ] || return 1 + if require_cmd jq; then + n="$(printf '%s' "$raw" | jq -r '(.pollers // []) | length' 2>/dev/null)" + elif require_cmd python3; then + n="$(printf '%s' "$raw" | python3 -c 'import json,sys +try: + d=json.load(sys.stdin); p=d.get("pollers") or [] + print(len(p)) +except Exception: + print(0)')" + else + n=0 + fi + [ "${n:-0}" -ge 1 ] +} + +# latest_workflow_ids -> echoes " " for the most recent execution +# in the cloud-setup namespace. `workflow list -o json` is an array whose [0] is the +# newest run; element shape is { "execution": { "workflowId", "runId" }, ... }. The +# namespace is dedicated to this setup, so --limit 1 is the run we just completed. +latest_workflow_ids() { + local raw wid="" rid="" + raw="$(temporal --profile cloud-setup workflow list --limit 1 -o json 2>/dev/null)" || return 1 + [ -n "$raw" ] || return 1 + if require_cmd jq; then + wid="$(printf '%s' "$raw" | jq -r '.[0].execution.workflowId // empty' 2>/dev/null)" + rid="$(printf '%s' "$raw" | jq -r '.[0].execution.runId // empty' 2>/dev/null)" + elif require_cmd python3; then + wid="$(printf '%s' "$raw" | python3 -c 'import json,sys +try: + a=json.load(sys.stdin); e=(a[0] if a else {}).get("execution",{}) or {} + print(e.get("workflowId","")) +except Exception: + print("")')" + rid="$(printf '%s' "$raw" | python3 -c 'import json,sys +try: + a=json.load(sys.stdin); e=(a[0] if a else {}).get("execution",{}) or {} + print(e.get("runId","")) +except Exception: + print("")')" + fi + [ -n "$wid" ] && [ -n "$rid" ] || return 1 + printf '%s %s' "$wid" "$rid" +} + +# workflow_status_for [runId] -> echoes the execution status string +# (e.g. WORKFLOW_EXECUTION_STATUS_COMPLETED) for that Workflow's run, or EMPTY if +# it isn't found yet / the call is momentarily unreadable (treat empty as "not +# yet"). Pin the run with [runId] when known so a reused WorkflowId can never +# read a prior run's status. Always returns 0 so a flaky call doesn't abort. +workflow_status_for() { + local wid="$1" rid="${2:-}" raw + if [ -n "$rid" ]; then + raw="$(temporal --profile cloud-setup workflow describe --workflow-id "$wid" --run-id "$rid" -o json 2>/dev/null)" || return 0 + else + raw="$(temporal --profile cloud-setup workflow describe --workflow-id "$wid" -o json 2>/dev/null)" || return 0 + fi + [ -n "$raw" ] || return 0 + if require_cmd jq; then + printf '%s' "$raw" | jq -r '.workflowExecutionInfo.status // empty' 2>/dev/null + elif require_cmd python3; then + printf '%s' "$raw" | python3 -c 'import json,sys +try: + print(json.load(sys.stdin).get("workflowExecutionInfo",{}).get("status","") or "") +except Exception: + print("")' + fi +} + +# stop_group -> terminate a backgrounded job AND its children. The job is +# launched under `set -m` so it leads its own process group (pgid == pid); a kill +# on the negative pid reaches the whole tree (go/mvn/npm spawn child processes that +# a bare `kill ` would orphan, leaving Workers polling Cloud). Best-effort. +stop_group() { + local p="${1:-}" + [ -n "$p" ] || return 0 + kill -TERM "-$p" 2>/dev/null || kill -TERM "$p" 2>/dev/null || true + sleep 1 + kill -KILL "-$p" 2>/dev/null || kill -KILL "$p" 2>/dev/null || true + wait "$p" 2>/dev/null || true + return 0 +} + +# cmd_run_workflow: THE single-call worker+starter path (PE-70). The Worker is a +# long-running process that must stay alive WHILE the starter triggers a Workflow — +# but background jobs don't reliably survive across tool calls on Codex/Cursor. So, +# like provision-and-scaffold, this owns the whole dance inside ONE synchronous call: +# launch the Worker detached (own process group) -> wait until it is polling the task +# queue (API-based readiness, not ps/pgrep) -> run the starter in the foreground until +# the Workflow returns -> tear the Worker down -> emit one RESULT block. The Worker only +# needs to live within this call, so no cross-tool-call survival is required. +# Run await-auth BEFORE this so the just-minted key is already accepted. +cmd_run_workflow() { + local sdk="" dir="" demo="" max="${RUN_WF_MAX_SECS:-180}" ready_max="${WORKER_READY_MAX_SECS:-120}" + while [ $# -gt 0 ]; do + case "$1" in + --sdk) sdk="$2"; shift 2 ;; + --dir) dir="$2"; shift 2 ;; + --demo-failure) demo="$2"; shift 2 ;; + --max-secs) max="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + [ -n "$sdk" ] || die bad-args "--sdk is required" + [ -n "$dir" ] || die bad-args "--dir is required (repo_path from provision-and-scaffold)" + [ -d "$dir" ] || die bad-args "repo dir not found: $dir" + local wcmd scmd tq + wcmd="$(worker_cmd_for "$sdk")"; scmd="$(starter_cmd_for "$sdk")"; tq="$(task_queue_for "$sdk")" + { [ -n "$wcmd" ] && [ -n "$scmd" ] && [ -n "$tq" ]; } || die unknown-sdk "no run-command mapping for SDK '$sdk'" + # SAFETY INVARIANT: these are SDK-keyed CONSTANT command strings (eval'd below). User + # input (dir/region/handle/name) is NEVER interpolated into them — it's applied via + # `cd "$dir"` and `export WORKFLOW_ID=...`. Assert no shell expansion ever sneaks in. + case "$wcmd$scmd" in *'$'*|*'`'*) die internal-bad-cmd "run-command table must not contain shell expansions" ;; esac + [ "$demo" = "" ] || [ "$demo" = "transient" ] || die bad-args "--demo-failure only supports 'transient'" + + # Workflow ID: distinct, self-describing names so the clean run and the + # failure-and-recovery run show up as two different Workflows in Cloud. The + # sample starters read WORKFLOW_ID from the env (defaulting to the clean name), + # so this never edits the sample source. + local wf_name="money-transfer-demo" + [ "$demo" = "transient" ] && wf_name="money-transfer-demo-recovery" + + # Pre-compile compiled SDKs so Worker and starter skip build time entirely, + # keeping the timing windows (ready_max, max) bounded to Temporal operations only. + case "$sdk" in + java) + log " -> pre-compiling Java sample app (mvn compile)..." + ( cd "$dir" || exit 127; mvn -q compile ) || die precompile-failed "Maven compile failed; check output above. Ensure Java + Maven are installed and the machine has network access for first-time dependency download." + ;; + dotnet|.net|net) + log " -> pre-compiling .NET sample app (dotnet build)..." + ( cd "$dir" || exit 127; dotnet build -v q ) || die precompile-failed "dotnet build failed; check output above. Ensure the .NET SDK is installed." + ;; + esac + + local job wlog slog wpid="" sp="" src waited swaited + job="$(mktemp -d "${TMPDIR:-/tmp}/tcloud-run.XXXXXX")" + wlog="$job/worker.log"; slog="$job/starter.log" + # Safety net: tear down the Worker + starter and the temp dir on ANY exit — + # including an external SIGTERM (e.g. the agent's Bash-tool timeout firing before + # our own --max-secs). Without this, a kill mid-poll orphans the Worker, leaving + # it polling Cloud (a reported leak). SIGKILL can't be trapped, but a tool-timeout + # sends TERM, and a normal/`die` exit runs the EXIT trap. stop_group tolerates an + # empty pid, so this is safe before wpid/sp are assigned. + # NOTE the `${var:-}` guards: at a NORMAL exit this function has already returned, + # so its `local` sp/wpid/job are out of scope when the EXIT trap fires — a bare + # `$sp` under `set -u` prints "sp: unbound variable" after the result block. The + # `:-` makes those no-ops at normal exit, while a SIGTERM mid-run fires the trap + # with the function still on the stack (locals in scope), so the Worker is killed. + trap 'stop_group "${sp:-}" 2>/dev/null; stop_group "${wpid:-}" 2>/dev/null; [ -n "${job:-}" ] && rm -rf "$job" 2>/dev/null' EXIT INT TERM + + # 1. Launch the Worker detached, in its own process group (set -m), output -> wlog. + if [ "$demo" = "transient" ]; then + log "Starting the Worker with DEMO_FAILURE=transient (background) for '$sdk'..." + else + log "Starting the Worker (background) for '$sdk'..." + fi + set -m + ( cd "$dir" || exit 127 + [ "$demo" = "transient" ] && export DEMO_FAILURE=transient + eval "$wcmd" ) >"$wlog" 2>&1 & + wpid=$! + set +m + + # 2. Readiness: wait until the Worker registers as a poller (API), or it dies first. + log " -> waiting for the Worker to start polling task queue '$tq'..." + waited=0 + while :; do + if worker_polling "$tq"; then break; fi + if ! kill -0 "$wpid" 2>/dev/null; then + tail -n 20 "$wlog" >&2 + if grep -qiE 'unauthorized|permission denied' "$wlog" 2>/dev/null; then + rm -rf "$job" 2>/dev/null + die worker-unauthorized "Worker exited with an auth error before polling — the key may not be accepted yet. Run await-auth, then retry run-workflow." + fi + rm -rf "$job" 2>/dev/null + die worker-start-failed "Worker exited before polling; see log tail above (e.g. missing deps/venv or wrong dir). Repo: $dir" + fi + if [ "$waited" -ge "$ready_max" ]; then + # API didn't show a poller in time; accept a log marker as a fallback signal. + if grep -qiE 'poll|worker started|started worker' "$wlog" 2>/dev/null; then + log " [warn] no poller via task-queue API within ${ready_max}s, but the Worker log shows polling — proceeding." + break + fi + stop_group "$wpid" + tail -n 20 "$wlog" >&2 + rm -rf "$job" 2>/dev/null + die worker-not-polling "Worker did not register as a poller on '$tq' within ${ready_max}s; see log tail above. Worker log: $wlog" + fi + sleep 3; waited=$(( waited + 3 )) + done + log " [ok] Worker is polling '$tq'." + + # 3. Run the starter. It submits the Workflow; some samples then block on the + # result, but others (e.g. Java) start it async and exit immediately. Launch + # it in its own group so a timeout kill is clean. + log " -> running the starter (Workflow runs against Cloud)..." + set -m + ( cd "$dir" || exit 127 + export WORKFLOW_ID="$wf_name" + eval "$scmd" ) >"$slog" 2>&1 & + sp=$! + set +m + + # 3b. Wait for the WORKFLOW itself to reach COMPLETED on the server — NOT merely + # for the starter to exit. An async starter returns before the Workflow + # finishes, so gating on its exit code falsely reports success and tears the + # Worker down mid-run, orphaning the execution. Keep the Worker alive while + # we poll, bounded by --max-secs. Pin the RunId (parsed from the starter + # log) so a reused WorkflowId can never read a prior run's status. + local wf_id="$wf_name" run_id="" wf_status="" + swaited=0; src=""; local nowf_waited=0 + while :; do + # Pin THIS run's RunId from the starter log as soon as it's printed (Java/Go/TS). + if [ -z "$run_id" ]; then + run_id="$(grep -ioE 'run[ _-]?id["'"'"':= ]+[A-Za-z0-9-]{8,}' "$slog" 2>/dev/null | head -n1 | grep -oE '[A-Za-z0-9-]{8,}$')" + fi + # Reap the starter when it exits. Once it's fully reaped its stdout is flushed, + # so make a definitive RunId parse here before we'd fall back to a by-id query. + if [ -z "$src" ] && ! kill -0 "$sp" 2>/dev/null; then + wait "$sp"; src=$? + [ -z "$run_id" ] && run_id="$(grep -ioE 'run[ _-]?id["'"'"':= ]+[A-Za-z0-9-]{8,}' "$slog" 2>/dev/null | head -n1 | grep -oE '[A-Za-z0-9-]{8,}$')" + fi + + # Only read status once it can be attributed to THIS run — either the RunId is + # pinned, or the starter has exited 0 (a sync SDK that waited for completion, so + # the new run is now the latest). Querying by WorkflowId before then could read a + # prior reused-WorkflowId run's COMPLETED status and bail before our run even ran. + if [ -n "$run_id" ]; then + wf_status="$(workflow_status_for "$wf_id" "$run_id")" + elif [ -n "$src" ] && [ "$src" -eq 0 ]; then + wf_status="$(workflow_status_for "$wf_id")" + else + wf_status="" + fi + case "$wf_status" in + *COMPLETED) break ;; + *FAILED|*TERMINATED|*CANCELED|*TIMED_OUT) + stop_group "$sp" 2>/dev/null; stop_group "$wpid" + tail -n 30 "$slog" >&2; rm -rf "$job" 2>/dev/null + die workflow-failed "Workflow ended ${wf_status##*STATUS_} (not COMPLETED). See log tail above." ;; + esac + # Starter died non-zero and never produced a trackable Workflow -> a real failure. + if [ -n "$src" ] && [ "$src" -ne 0 ] && [ -z "$run_id" ] && [ -z "$wf_status" ]; then + stop_group "$wpid" + tail -n 30 "$slog" >&2; rm -rf "$job" 2>/dev/null + die workflow-failed "starter exited ${src} and no Workflow was submitted. See log tail above." + fi + # Fast-fail: a starter that exited 0 but produced NO trackable Workflow (no RunId + # pinned, and a by-id lookup stays empty) never actually submitted one — e.g. a client + # that catches its own connect/start error and still exits 0 (the .NET sample does + # exactly this). Give the server a brief settle window for a flaky read, then fail + # clearly instead of burning the full --max-secs as a misleading timeout. + if [ -n "$src" ] && [ "$src" -eq 0 ] && [ -z "$run_id" ] && [ -z "$wf_status" ]; then + nowf_waited=$(( nowf_waited + 2 )) + if [ "$nowf_waited" -ge "${NOWF_SETTLE_SECS:-15}" ]; then + stop_group "$wpid" + tail -n 30 "$slog" >&2; rm -rf "$job" 2>/dev/null + die workflow-not-submitted "starter exited 0 but no Workflow was submitted within ${NOWF_SETTLE_SECS:-15}s — it likely swallowed a connect/start error (some samples catch-and-exit-0). See log tail above." + fi + else + nowf_waited=0 + fi + if [ "$swaited" -ge "$max" ]; then + stop_group "$sp" 2>/dev/null; stop_group "$wpid" + # Only terminate if the execution is genuinely still RUNNING — never kill a + # Workflow that actually completed but whose status was momentarily unreadable. + if [ -n "$wf_status" ] && [ -z "${wf_status##*RUNNING}" ]; then + log " [cleanup] terminating orphaned workflow '$wf_id' (local timeout)..." + if [ -n "$run_id" ]; then + temporal --profile cloud-setup workflow terminate --workflow-id "$wf_id" --run-id "$run_id" --reason "run-workflow timed out after ${max}s" >/dev/null 2>&1 || true + else + temporal --profile cloud-setup workflow terminate --workflow-id "$wf_id" --reason "run-workflow timed out after ${max}s" >/dev/null 2>&1 || true + fi + fi + tail -n 20 "$slog" >&2; rm -rf "$job" 2>/dev/null + die workflow-timeout "Workflow did not reach COMPLETED within ${max}s; see log tail above. Worker log: $wlog" + fi + sleep 2; swaited=$(( swaited + 2 )) + done + + # 4. Workflow reached COMPLETED on the server — now tear the Worker down. + stop_group "$sp" 2>/dev/null + stop_group "$wpid" + log " [ok] Worker stopped." + + # IDs for the run link. The Workflow ID is authoritative ($wf_name — set via + # WORKFLOW_ID and honored by every sample), so report it directly. run_id was + # pinned from the starter log during the completion wait above; if the sample + # never printed it (a sync SDK that already completed), resolve it from the + # data plane (retried briefly, only accepting the row that matches OUR + # Workflow ID). May still be empty, in which case the caller links to the + # Workflow detail page (never the bare namespace list). + if [ -z "$run_id" ]; then + local ids="" tries=0 + while [ "$tries" -lt 6 ]; do + ids="$(latest_workflow_ids)" || ids="" + if [ -n "$ids" ] && [ "${ids%% *}" = "$wf_id" ]; then run_id="${ids##* }"; break; fi + tries=$(( tries + 1 )); sleep 2 + done + fi + + result_open ok + result_kv workflow_status COMPLETED + result_kv workflow_id "$wf_id" + result_kv run_id "${run_id:-unknown}" + result_kv task_queue "$tq" + result_kv worker_log "$wlog" + [ "$demo" = "transient" ] && result_kv demo_failure transient + result_close +} + +cmd_clone() { + local sdk="" dir="" + while [ $# -gt 0 ]; do + case "$1" in + --sdk) sdk="$2"; shift 2 ;; + --dir) dir="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + [ -n "$sdk" ] || die bad-args "--sdk is required" + local url; url="$(repo_url_for_sdk "$sdk")" + [ -n "$url" ] || die unknown-sdk "no repo mapping for SDK '$sdk'" + local target="${dir:-$(basename "$url")}" + + log "Cloning ${url} (branch money-transfer-project-cloud-setup) into ${target}..." + if ! git clone --branch money-transfer-project-cloud-setup --single-branch "$url" "$target" >&2; then + die clone-failed "git clone failed for $url" + fi + result_open ok + result_kv repo_url "$url" + result_kv path "$target" + result_close +} + +# deps_already_present -> exit 0 if deps look installed (skip them). +# Manager-independent: an env dir / node_modules / satisfied bundle means a re-run +# need not reinstall. Best-effort; called from inside the target dir. +deps_already_present() { + case "$1" in + python) [ -d env ] ;; + ts|typescript) [ -d node_modules ] ;; + ruby) bundle check >/dev/null 2>&1 ;; + *) return 1 ;; # go/java/dotnet: install is cheap / handled at compile + esac +} + +# install_deps_for — manager-parameterized dependency +# install, detect-then-skip, backed by the (sdk,manager) matrix in install_cmd_for. +# All output goes to stderr (caller redirects) so it never pollutes the RESULT +# block. Best-effort: a deps hiccup warns but does not abort provisioning. +install_deps_for() { + local sdk="$1" mgr="$2" dir="$3" cmd + cmd="$(install_cmd_for "$sdk" "$mgr")" + if [ -z "$cmd" ]; then + log " [warn] no install command for sdk '$sdk' + manager '$mgr' — skipping deps" + return 0 + fi + # SAFETY INVARIANT: install_cmd_for returns SDK/manager-keyed CONSTANT strings (eval'd + # below). No user input is interpolated; assert no shell expansion can sneak in. + case "$cmd" in *'$'*|*'`'*) die internal-bad-cmd "install command table must not contain shell expansions" ;; esac + ( cd "$dir" 2>/dev/null || { log " [warn] could not enter $dir for deps"; exit 0; } + if deps_already_present "$sdk"; then + log " [skip] $sdk dependencies already present" + else + log " -> installing deps via '$mgr': $cmd" + eval "$cmd" + fi + ) || log " [warn] dependency install reported a problem (continuing; can be retried)" +} + +# cmd_install_deps: standalone manager-parameterized install (PE-75). Lets the +# agent install (or re-install) deps with an explicitly chosen/overridden manager. +# Defaults --manager to the SDK's deterministic default when omitted. Dies with +# manager-not-found if the chosen manager's binary isn't on PATH (a clear, +# fail-fast error rather than a confusing downstream Worker crash). +cmd_install_deps() { + local sdk="" mgr="" dir="" + while [ $# -gt 0 ]; do + case "$1" in + --sdk) sdk="$2"; shift 2 ;; + --manager) mgr="$2"; shift 2 ;; + --dir) dir="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + [ -n "$sdk" ] || die bad-args "--sdk is required" + [ -n "$dir" ] || die bad-args "--dir is required (repo_path)" + [ -d "$dir" ] || die bad-args "repo dir not found: $dir" + local candidates bin + resolve_pkg_inputs "$sdk" "$mgr" || die_pkg $? "$sdk" + mgr="$PKG_MGR"; candidates="$PKG_CANDIDATES" # mgr defaulted to the preferred candidate + bin="$(manager_bin "$mgr")" + require_cmd "$bin" || die manager-not-found "package manager '$mgr' (needs '$bin') is not installed; install it or pick another (supported: $(printf '%s' "$candidates" | tr ' ' ','))." + + install_deps_for "$sdk" "$mgr" "$dir" + result_open ok + result_kv sdk "$sdk" + result_kv manager "$mgr" + result_kv repo_path "$dir" + result_close +} + +# cmd_scaffold: set up the app only — clone the cloud-ready sample + install deps, +# NO namespace work (PE-75). Pairs with start-namespace/await-namespace so the app +# setup is its own gated step that overlaps the server-side namespace provisioning. +# Validates the manager fail-fast (manager-not-found / unsupported-manager) BEFORE +# cloning, same as provision-and-scaffold. +cmd_scaffold() { + local sdk="" dir="" mgr="" + while [ $# -gt 0 ]; do + case "$1" in + --sdk) sdk="$2"; shift 2 ;; + --dir) dir="$2"; shift 2 ;; + --manager) mgr="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + [ -n "$sdk" ] || die bad-args "--sdk is required" + local url candidates target bin + url="$(repo_url_for_sdk "$sdk")" + [ -n "$url" ] || die unknown-sdk "no repo mapping for SDK '$sdk'" + resolve_pkg_inputs "$sdk" "$mgr" || die_pkg $? "$sdk" + mgr="$PKG_MGR"; candidates="$PKG_CANDIDATES" + bin="$(manager_bin "$mgr")" + require_cmd "$bin" || die manager-not-found "package manager '$mgr' (needs '$bin') is not installed; install it or pick another (supported: $(printf '%s' "$candidates" | tr ' ' ','))." + target="${dir:-$(basename "$url")}" + + log "Cloning the sample for '$sdk' into '$target'..." + if ! git clone --branch money-transfer-project-cloud-setup --single-branch "$url" "$target" >&2; then + die clone-failed "git clone failed for $url" + fi + log " [ok] cloned" + log " -> installing dependencies ($sdk via $mgr)..." + install_deps_for "$sdk" "$mgr" "$target" >&2 + log " [ok] dependencies ready" + + result_open ok + result_kv repo_path "$target" + result_kv manager "$mgr" + result_close +} + +# cmd_provision_and_scaffold: THE single-call parallel path (PE-68). Runs as one +# ordinary SYNCHRONOUS foreground command; internally backgrounds the (synchronous, +# never --async) namespace create and overlaps it with clone + deps, then `wait`s +# and joins. Because the background job lives and dies inside this one invocation, +# no cross-tool-call survival is needed -> works on Claude Code, Codex, and Cursor. +# Emits append-only progress lines to stderr; one RESULT block on stdout. +cmd_provision_and_scaffold() { + local sdk="" region="" dir="" mgr="" + while [ $# -gt 0 ]; do + case "$1" in + --sdk) sdk="$2"; shift 2 ;; + --region) region="$2"; shift 2 ;; + --dir) dir="$2"; shift 2 ;; + --manager) mgr="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + [ -n "$sdk" ] || die bad-args "--sdk is required" + [ -n "$region" ] || die bad-args "--region is required" + temporal cloud whoami >/dev/null 2>&1 || die not-authenticated "Not signed in; run login again." + + local url name target nsout nspid rc out handle candidates bin + url="$(repo_url_for_sdk "$sdk")" + [ -n "$url" ] || die unknown-sdk "no repo mapping for SDK '$sdk'" + # Resolve the manager up front (default = preferred candidate) and fail fast if + # an explicitly chosen one is unsupported/uninstalled, BEFORE we create the + # namespace + clone (so a bad --manager never leaves half-provisioned state). + resolve_pkg_inputs "$sdk" "$mgr" || die_pkg $? "$sdk" + mgr="$PKG_MGR"; candidates="$PKG_CANDIDATES" + bin="$(manager_bin "$mgr")" + require_cmd "$bin" || die manager-not-found "package manager '$mgr' (needs '$bin') is not installed; install it or pick another (supported: $(printf '%s' "$candidates" | tr ' ' ','))." + name="$(namespace_name_for "$sdk")" + target="${dir:-$(basename "$url")}" + + log "Provisioning in parallel: namespace '$name' (background) + sample app for '$sdk'..." + + # Background the SYNCHRONOUS create (never --async); capture its output to a file. + nsout="$(mktemp "${TMPDIR:-/tmp}/tcloud-ns.XXXXXX")" + ( temporal cloud namespace create \ + --name "$name" --region "$region" \ + --api-key-auth-enabled --retention-days 30 --auto-confirm ) >"$nsout" 2>&1 & + nspid=$! + log " -> namespace create started in the background (provisioning takes a few minutes)" + + # Foreground overlap: clone + deps. On clone failure, stop the namespace job too. + log " -> cloning sample into '$target'..." + if ! git clone --branch money-transfer-project-cloud-setup --single-branch "$url" "$target" >&2; then + kill "$nspid" 2>/dev/null; wait "$nspid" 2>/dev/null; rm -f "$nsout" + die clone-failed "git clone failed for $url" + fi + log " [ok] cloned" + log " -> installing dependencies ($sdk via $mgr)..." + install_deps_for "$sdk" "$mgr" "$target" >&2 + log " [ok] dependencies ready" + + # Join: wait for the namespace create to finish, then resolve the handle. + log " -> joining: waiting for the namespace to finish provisioning..." + wait "$nspid"; rc=$? + out="$(cat "$nsout")"; rm -f "$nsout" + if [ "$rc" -ne 0 ]; then + printf '%s\n' "$out" | tail -n 20 >&2 + die create-rejected "namespace create exited $rc — usually region or name format; see output above." + fi + # Resolve from the authoritative, pagination-immune `--name` filter and wait until the + # namespace is ACTIVE (a just-submitted create is ACTIVATING for a few minutes). The + # filtered result carries the real endpoint, so read the address from it. + local info + info="$(await_active_namespace "$name" "${NS_AWAIT_MAX_SECS:-600}")"; rc=$? + case "$rc" in + 0) ;; + 2) die namespace-not-provisioning "namespace '$name' never appeared after ~${NS_PHANTOM_GRACE_SECS:-75}s — the create was accepted but isn't provisioning (commonly an unavailable region, e.g. azure-centralus). Re-run with an AWS/GCP region." ;; + *) die handle-not-found "namespace '$name' created but not ACTIVE after retries (still provisioning?). Re-run provision-and-scaffold or await-namespace to resume the wait." ;; + esac + handle="${info%%|*}" + log " [ok] namespace active: $handle" + + result_open ok + result_kv namespace_handle "$handle" + result_kv address "${info#*|}" + result_kv repo_path "$target" + result_kv manager "$mgr" + result_close +} + +# cleanup PRINTS the destructive commands; it never runs them. +cmd_cleanup_info() { + local handle="" keyid="" + while [ $# -gt 0 ]; do + case "$1" in + --handle) handle="$2"; shift 2 ;; + --key-id) keyid="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + local cfg; cfg="$(config_path)" + result_open ok + result_kv config_path "$cfg" + result_kv revoke_key_cmd "temporal cloud apikey delete --key-id ${keyid:-}" + result_kv delete_namespace_cmd "temporal cloud namespace delete --namespace ${handle:-}" + result_kv note "Run these yourself; never --auto-confirm a delete. Then remove the [profile.cloud-setup] block from the config file." + result_close +} + +# repair-config: strip ALL [profile.cloud-setup] block(s) from the shared TOML, +# preserving [profile.default]. Use when prior/partial runs left duplicate +# cloud-setup sections that made the file unparseable. Never reads the file into +# the agent's context (the work is in awk); never touches other profiles. After +# repair, re-run create-key to write one fresh, valid profile. +cmd_repair_config() { + local cfg; cfg="$(config_path)" + if [ ! -f "$cfg" ]; then + result_open ok; result_kv config_path "$cfg"; result_kv removed_blocks 0 + result_kv note "no config file present — nothing to repair"; result_close; return + fi + local before dir tmp + before="$(grep -cE '^[[:space:]]*\[profile\.cloud-setup\]' "$cfg" 2>/dev/null || true)" + dir="$(dirname "$cfg")" + tmp="$(mktemp "${dir}/temporal.toml.XXXXXX")" + chmod 600 "$tmp" + strip_cloud_setup "$cfg" > "$tmp" + mv "$tmp" "$cfg" + chmod 600 "$cfg" + log "Stripped cloud-setup profile(s) from $cfg (was ${before:-0}); [profile.default] preserved. Re-run create-key to write a fresh one." + result_open ok + result_kv config_path "$cfg" + result_kv removed_blocks "${before:-0}" + result_kv note "cloud-setup stripped; default preserved. Run create-key to recreate the profile." + result_close +} + +# emit_gate [ ]... (PE-75 determinism lever) +# Prints a READY-TO-RENDER gate block on stdout, AFTER the RESULT block, delimited +# by `=== GATE ===` / `=== END GATE ===`. The agent renders the text between the +# markers VERBATIM (then appends the numbered choices) instead of assembling the +# fenced block from prose rules — which is where the formatting kept breaking +# (dropped fence, comment-only, glued rules). ASCII only; comment goes ABOVE its +# command (green-comment style). Same idea as pinning the CLI flags: move the exact +# format into the script so the model only has to print it. +emit_gate() { + # Plain bold heading, then a fenced ```bash``` block: a "# " comment above each + # command. Keep the fence — without it a leading "#" renders as a markdown + # heading. The agent renders this verbatim, so it can't be mangled by hand. + local heading="$1"; shift + printf '=== GATE ===\n' + printf '**%s**\n\n' "$heading" + printf '```bash\n' + local first=1 + while [ $# -ge 2 ]; do + [ "$first" -eq 1 ] || printf '\n' + first=0 + printf '# %s\n%s\n' "$1" "$2" + shift 2 + done + printf '```\n' + printf '=== END GATE ===\n' +} + +# cmd_preview: PE-75 dry-run. Resolve and PRINT the concrete command(s) a +# subcommand would run, plus the resolved user-facing parameters, with NO side +# effects (no clone, no temporal calls, no installs) — this powers the per-command +# confirm gate and the "Edit" path in SKILL.md. Deterministic and offline: the +# namespace NAME is shown as its template (it is randomized at real run time), so +# preview never needs the network or auth. Emits cmd_1/cmd_2/... in the RESULT AND +# a ready-to-render `=== GATE ===` block (see emit_gate) the agent prints verbatim. +cmd_preview() { + local sub="${1:-}"; shift || true + [ -n "$sub" ] || die bad-args "preview needs a subcommand, e.g. preview run-workflow --sdk python --dir D" + local sdk="" region="" dir="" mgr="" handle="" address="" demo="" maxsecs="" + while [ $# -gt 0 ]; do + case "$1" in + --sdk) sdk="$2"; shift 2 ;; + --region) region="$2"; shift 2 ;; + --dir) dir="$2"; shift 2 ;; + --manager) mgr="$2"; shift 2 ;; + --handle) handle="$2"; shift 2 ;; + --address) address="$2"; shift 2 ;; + --demo-failure) demo="$2"; shift 2 ;; + --max-secs) maxsecs="$2"; shift 2 ;; + *) die bad-args "unknown arg: $1" ;; + esac + done + + local url target candidates wcmd scmd tq wf_name icmd + case "$sub" in + clone) + [ -n "$sdk" ] || die bad-args "preview clone needs --sdk" + url="$(repo_url_for_sdk "$sdk")"; [ -n "$url" ] || die unknown-sdk "no repo mapping for SDK '$sdk'" + target="${dir:-$(basename "$url")}" + result_open ok + result_kv preview clone + result_kv repo_url "$url" + result_kv clone_dir "$target" + result_kv cmd_1 "git clone --branch money-transfer-project-cloud-setup --single-branch $url $target" + result_close + emit_gate "Downloading the sample app (clone only)" \ + "clone the Cloud-ready sample" "git clone --branch money-transfer-project-cloud-setup --single-branch $url $target" ;; + install-deps) + [ -n "$sdk" ] || die bad-args "preview install-deps needs --sdk" + resolve_pkg_inputs "$sdk" "$mgr" || die_pkg $? "$sdk" + mgr="$PKG_MGR"; icmd="$PKG_ICMD"; candidates="$PKG_CANDIDATES" + result_open ok + result_kv preview install-deps + result_kv manager "$mgr" + result_kv supported_managers "$(printf '%s' "$candidates" | tr ' ' ',')" + result_kv clone_dir "${dir:-}" + result_kv cmd_1 "(cd ${dir:-} && $icmd)" + result_close + emit_gate "Installing the sample's dependencies ($mgr)" \ + "install dependencies with $mgr $(install_location_note "$sdk")" "(cd ${dir:-} && $icmd)" ;; + provision-and-scaffold) + [ -n "$sdk" ] || die bad-args "preview provision-and-scaffold needs --sdk" + url="$(repo_url_for_sdk "$sdk")"; [ -n "$url" ] || die unknown-sdk "no repo mapping for SDK '$sdk'" + resolve_pkg_inputs "$sdk" "$mgr" || die_pkg $? "$sdk" + mgr="$PKG_MGR"; icmd="$PKG_ICMD" + target="${dir:-$(basename "$url")}" + result_open ok + result_kv preview provision-and-scaffold + result_kv region "${region:-}" + result_kv manager "$mgr" + result_kv clone_dir "$target" + result_kv namespace_name_template "quickstartai-- (randomized at run; <=39 chars)" + result_kv cmd_1 "temporal cloud namespace create --name --region ${region:-} --api-key-auth-enabled --retention-days 30 --auto-confirm" + result_kv cmd_2 "git clone --branch money-transfer-project-cloud-setup --single-branch $url $target" + result_kv cmd_3 "(cd $target && $icmd)" + result_close + emit_gate "Creating your namespace & downloading the sample app" \ + "create your Cloud namespace - billable; provisions server-side (~a few min)" "temporal cloud namespace create --name --region ${region:-} --api-key-auth-enabled --retention-days 30 --auto-confirm" \ + "clone the Cloud-ready sample" "git clone --branch money-transfer-project-cloud-setup --single-branch $url $target" \ + "install dependencies with $mgr $(install_location_note "$sdk")" "(cd $target && $icmd)" ;; + run-workflow) + [ -n "$sdk" ] || die bad-args "preview run-workflow needs --sdk" + wcmd="$(worker_cmd_for "$sdk")"; scmd="$(starter_cmd_for "$sdk")"; tq="$(task_queue_for "$sdk")" + { [ -n "$wcmd" ] && [ -n "$scmd" ] && [ -n "$tq" ]; } || die unknown-sdk "no run-command mapping for SDK '$sdk'" + wf_name="money-transfer-demo"; [ "$demo" = "transient" ] && wf_name="money-transfer-demo-recovery" + result_open ok + result_kv preview run-workflow + result_kv repo_dir "${dir:-}" + result_kv workflow_id "$wf_name" + result_kv task_queue "$tq" + result_kv max_secs "${maxsecs:-180}" + [ "$demo" = "transient" ] && result_kv demo_failure transient + result_kv cmd_1 "(cd ${dir:-} && ${demo:+DEMO_FAILURE=transient }$wcmd) # Worker (background)" + result_kv cmd_2 "(cd ${dir:-} && WORKFLOW_ID=$wf_name $scmd) # starter" + result_close + local rw_head="Run your first Workflow" + [ "$demo" = "transient" ] && rw_head="Run the recovery Workflow (inject a failure)" + emit_gate "$rw_head" \ + "Worker - runs in the background, polls the task queue, stopped when done" "(cd ${dir:-} && ${demo:+DEMO_FAILURE=transient }$wcmd)" \ + "starter - submits the Workflow, waits for COMPLETED, exits" "(cd ${dir:-} && WORKFLOW_ID=$wf_name $scmd)" ;; + create-namespace) + result_open ok + result_kv preview create-namespace + result_kv region "${region:-}" + result_kv namespace_name_template "quickstartai-- (randomized at run; <=39 chars)" + result_kv cmd_1 "temporal cloud namespace create --name --region ${region:-} --api-key-auth-enabled --retention-days 30 --auto-confirm" + result_close + emit_gate "Creating your Cloud namespace" \ + "create your Cloud namespace - billable; provisions server-side (~a few min)" "temporal cloud namespace create --name --region ${region:-} --api-key-auth-enabled --retention-days 30 --auto-confirm" ;; + start-namespace) + result_open ok + result_kv preview start-namespace + result_kv region "${region:-}" + result_kv namespace_name_template "quickstartai-- (randomized at run; <=39 chars)" + result_kv cmd_1 "temporal cloud namespace create --name --region ${region:-} --api-key-auth-enabled --retention-days 30 --auto-confirm --async # submits, returns immediately; provisions server-side" + result_close + emit_gate "Creating your Cloud namespace" \ + "create your Cloud namespace - billable; submits async, provisions server-side (~a few min)" \ + "temporal cloud namespace create --name --region ${region:-} --api-key-auth-enabled --retention-days 30 --auto-confirm --async" ;; + await-namespace) + result_open ok + result_kv preview await-namespace + result_kv cmd_1 "temporal cloud namespace list --name -o json # polled until the namespace is ACTIVE (bounded)" + result_close + emit_gate "Waiting for the namespace to provision" \ + "poll until the namespace exists and is ACTIVE (bounded)" \ + "temporal cloud namespace list --name -o json" ;; + scaffold) + [ -n "$sdk" ] || die bad-args "preview scaffold needs --sdk" + url="$(repo_url_for_sdk "$sdk")"; [ -n "$url" ] || die unknown-sdk "no repo mapping for SDK '$sdk'" + resolve_pkg_inputs "$sdk" "$mgr" || die_pkg $? "$sdk" + mgr="$PKG_MGR"; icmd="$PKG_ICMD" + target="${dir:-$(basename "$url")}" + result_open ok + result_kv preview scaffold + result_kv manager "$mgr" + result_kv clone_dir "$target" + result_kv cmd_1 "git clone --branch money-transfer-project-cloud-setup --single-branch $url $target" + result_kv cmd_2 "(cd $target && $icmd)" + result_close + emit_gate "Downloading the sample app (clone + dependencies)" \ + "clone the Cloud-ready sample" "git clone --branch money-transfer-project-cloud-setup --single-branch $url $target" \ + "install dependencies with $mgr $(install_location_note "$sdk")" "(cd $target && $icmd)" ;; + create-key) + # Never show the token; the command itself is fine (it carries no secret). + result_open ok + result_kv preview create-key + result_kv handle "${handle:-}" + result_kv address "${address:-
}" + result_kv cmd_1 "temporal cloud apikey create-for-me --display-name money-transfer-cloud-setup --expiry-duration 25h --auto-confirm -o json # token captured to a 0600 file, never printed" + result_close + emit_gate "Creating your API key and saving the config" \ + "mint the key + write the cloud-setup profile to temporal.toml (token captured to a 0600 file, never printed)" \ + "temporal cloud apikey create-for-me --display-name money-transfer-cloud-setup --expiry-duration 25h --auto-confirm -o json" ;; + install-cli) + # Presence-aware (read-only): if the CLI is already here, disclose a no-op + # so the gate matches what cmd_install_cli will do (skip, no install/update). + if temporal cloud version >/dev/null 2>&1; then + result_open ok + result_kv preview install-cli + result_kv cli_present true + result_kv cmd_1 "# Temporal CLI already installed - this step is skipped (no install, no update)" + result_close + emit_gate "Temporal CLI already installed - nothing to do" \ + "the Temporal CLI is already on your machine, so this step is skipped (no install, no update)" "temporal cloud version" + else + local ic_cmd ic_note + case "$(uname -s)" in + Darwin) ic_cmd="brew install temporalio/prerelease/temporal-cloud"; ic_note="install the Temporal CLI via Homebrew (adds software)" ;; + *) ic_cmd="# download temporal-cloud from https://github.com/temporalio/cloud-cli/releases/latest and put it on PATH"; ic_note="install the Temporal CLI (download the binary, put it on PATH)" ;; + esac + result_open ok + result_kv preview install-cli + result_kv cli_present false + result_kv cmd_1 "$ic_cmd" + result_close + emit_gate "Installing the Temporal CLI" "$ic_note" "$ic_cmd" + fi ;; + login) + result_open ok + result_kv preview login + result_kv cmd_1 "temporal cloud login # opens a browser on your machine; blocks until you finish" + result_kv cmd_2 "temporal cloud whoami # confirms the signed-in identity" + result_close + emit_gate "Sign in to Temporal Cloud" \ + "open a browser to sign in (blocks until you finish)" "temporal cloud login" \ + "confirm the signed-in identity" "temporal cloud whoami" ;; + regions) + result_open ok + result_kv preview regions + result_kv cmd_1 "temporal cloud region list" + result_close + emit_gate "Listing your Cloud regions" \ + "list the regions your account can use" \ + "temporal cloud region list" ;; + await-auth) + result_open ok + result_kv preview await-auth + result_kv cmd_1 "temporal --profile cloud-setup workflow list --limit 1 # polled until the new API key is accepted (bounded ~90s)" + result_close + emit_gate "Waiting for the API key to be accepted" \ + "poll an authorized call until the new key is accepted (bounded ~90s)" \ + "temporal --profile cloud-setup workflow list --limit 1" ;; + verify-config) + result_open ok + result_kv preview verify-config + result_kv cmd_1 "temporal --profile cloud-setup config list # lists profile fields; api_key is redacted, never shown" + result_close + emit_gate "Verifying the saved config" \ + "list the cloud-setup profile fields (api_key redacted, never shown)" \ + "temporal --profile cloud-setup config list" ;; + preflight) + result_open ok + result_kv preview preflight + result_close + emit_gate "Checking your environment" \ + "check git / jq / brew are available (read-only, local)" "command -v git jq brew" \ + "flag any stray TEMPORAL_* env vars that would override your saved config" "env | grep '^TEMPORAL_' || true" ;; + detect-tools) + [ -n "$sdk" ] || die bad-args "preview detect-tools needs --sdk" + candidates="$(managers_for "$sdk")"; [ -n "$candidates" ] || die unknown-sdk "no package-manager mapping for SDK '$sdk'" + local dt_bins="" dt_m dt_b + for dt_m in $candidates; do dt_b="$(manager_bin "$dt_m")"; dt_bins="${dt_bins:+$dt_bins }$dt_b"; done + result_open ok + result_kv preview detect-tools + result_close + emit_gate "Detecting your local tools" \ + "detect which package managers are installed for $sdk (read-only, local)" "command -v $dt_bins" \ + "read each tool's version to flag anything below the minimum" "$(runtime_for "$sdk") --version" ;; + repair-config) + result_open ok + result_kv preview repair-config + result_kv cmd_1 "strip [profile.cloud-setup] block(s) from temporal.toml (keeps [profile.default])" + result_close + emit_gate "Repairing temporal.toml (strip duplicate cloud-setup profiles)" \ + "remove any existing [profile.cloud-setup] block(s); keeps [profile.default]" "scripts/provision.sh repair-config" ;; + *) + die bad-args "preview does not support subcommand '$sub' (try: preflight, detect-tools, install-cli, login, regions, start-namespace, scaffold, await-namespace, provision-and-scaffold, install-deps, clone, create-namespace, create-key, await-auth, run-workflow, verify-config, repair-config)" ;; + esac +} + +# announce_gate [args...] (PE-75 — deterministic disclosure floor) +# Before an effectful subcommand acts, print ITS gate to STDERR so the tool block +# ALWAYS records what the command runs — even when the agent skips rendering the +# chat-side gate from §Gate templates (the Cursor "sometimes doesn't disclose" +# failure). Reuses cmd_preview's single-source gate text, so the disclosure can +# never drift from the real command; strips the machine markers and frames it as +# plain human disclosure. Never blocks the real work: any preview failure (an arg +# preview doesn't know, an unsupported sub) is swallowed and the command proceeds. +# Opt out with TCLOUD_DISCLOSE=0 (the test harness sets this to keep stderr clean). +# It is a RECORD, not a pre-consent prompt — in the request/response tool model it +# surfaces bundled with the result; the chat-side gate remains the pre-action one. +announce_gate() { + [ "${TCLOUD_DISCLOSE:-1}" = "0" ] && return 0 + local sub="$1"; shift + # Forward only the flags cmd_preview understands; drop the rest (--name, + # --display-name, …) so disclosure never dies on an arg the real command accepts + # but preview doesn't. (--display-name carries no secret; it's dropped for parity + # with the preview create-key gate, which already shows the command tokenless.) + local pv=() + while [ $# -gt 0 ]; do + case "$1" in + --sdk|--region|--dir|--manager|--handle|--address|--demo-failure|--max-secs) + if [ $# -ge 2 ]; then pv+=("$1" "$2"); shift 2; else shift; fi ;; + *) shift ;; + esac + done + local block + # cmd_preview is offline + side-effect-free; a `die` inside it exits only this + # command-substitution subshell, so `|| return 0` keeps the real work going. + block="$(cmd_preview "$sub" ${pv[@]+"${pv[@]}"} 2>/dev/null)" || return 0 + # Keep only the lines between the GATE markers (drop the markers + the RESULT block). + block="$(printf '%s\n' "$block" | sed -n '/^=== GATE ===$/,/^=== END GATE ===$/{/^=== GATE ===$/d;/^=== END GATE ===$/d;p;}')" + [ -n "$block" ] || return 0 + { + printf 'disclosure — the command(s) this step runs (auto-recorded):\n' + printf '%s\n' "$block" + } >&2 +} + +usage() { + cat >&2 <<'EOF' +provision.sh — deterministic executor for temporal-cloud-setup + +Usage: provision.sh [args] + +Commands: + preflight [--sdk S] Environment checks + config path + stray TEMPORAL_* vars + detect-tools --sdk S Detect supported+installed package managers, pick a default, + report versions, surface discrepancies (PE-75 adaptation) + preview [args] Dry-run: print the concrete command(s) a subcommand would run + (no side effects) — powers the per-command confirm/Edit gate + install-cli Install the prerelease temporal-cloud CLI (skips if present) + login temporal cloud login (browser) + whoami + regions List Cloud regions (raw list to stderr) + start-namespace --sdk S --region R Fire-and-forget: submit `namespace create --async` (server-side); emits namespace_name + await-namespace --name N [--max-secs M] Join: poll `namespace list` until N provisions; emits namespace_handle + address + scaffold --sdk S [--dir D] [--manager M] Set up the app only: clone the cloud-ready sample + install deps; emits repo_path, manager + create-namespace --sdk S --region R Blocking wrapper = start + await (non-parallel fallback); emits namespace_handle + address + create-key --handle H --address A Mint API key, write [profile.cloud-setup] TOML; emits key_id (token never printed) + verify-config temporal --profile cloud-setup config list + await-auth Wait until the new API key is accepted (poll `workflow list`) before the Worker + run-workflow --sdk S --dir D [--demo-failure transient] [--max-secs N] + Single synchronous call: start Worker (bg) -> wait until polling -> run starter -> stop Worker; emits workflow_status, workflow_id, run_id + clone --sdk S [--dir D] Clone the cloud-ready sample for the SDK + install-deps --sdk S --dir D [--manager M] Install sample deps with a chosen/default package manager (PE-75) + provision-and-scaffold --sdk S --region R [--dir D] [--manager M] + Single synchronous call: namespace create (bg) || clone+deps; emits namespace_handle, address, repo_path, manager + repair-config Strip duplicate/old [profile.cloud-setup] blocks (keeps default) + cleanup-info --handle H --key-id K Print (do not run) the cleanup commands + +All commands print a delimited "=== RESULT ===" block on stdout for the agent to parse. +EOF +} + +main() { + local cmd="${1:-}"; shift || true + # Deterministic disclosure floor: echo the subcommand's gate to stderr before it + # acts, so the tool block records what ran even if the agent skipped the chat-side + # gate. `preview` only prints, `cleanup-info` runs nothing, help/empty don't act — + # none need an echo (and cmd_preview can't render them anyway). + case "$cmd" in + preview|cleanup-info|""|-h|--help|help) : ;; + *) announce_gate "$cmd" "$@" ;; + esac + case "$cmd" in + preflight) cmd_preflight "$@" ;; + detect-tools) cmd_detect_tools "$@" ;; + preview) cmd_preview "$@" ;; + install-deps) cmd_install_deps "$@" ;; + install-cli) cmd_install_cli "$@" ;; + login) cmd_login "$@" ;; + regions) cmd_regions "$@" ;; + start-namespace) cmd_start_namespace "$@" ;; + await-namespace) cmd_await_namespace "$@" ;; + scaffold) cmd_scaffold "$@" ;; + create-namespace) cmd_create_namespace "$@" ;; + create-key) cmd_create_key "$@" ;; + verify-config) cmd_verify_config "$@" ;; + await-auth) cmd_await_auth "$@" ;; + run-workflow) cmd_run_workflow "$@" ;; + clone) cmd_clone "$@" ;; + provision-and-scaffold) cmd_provision_and_scaffold "$@" ;; + repair-config) cmd_repair_config "$@" ;; + cleanup-info) cmd_cleanup_info "$@" ;; + ""|-h|--help|help) usage ;; + *) usage; die bad-command "unknown command: $cmd" ;; + esac +} + +main "$@" \ No newline at end of file