@sequenceholdings/studio-cli 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +258 -0
  2. package/dist/artifact/delegate.d.ts +25 -0
  3. package/dist/artifact/delegate.js +263 -0
  4. package/dist/atlas-client.d.ts +44 -0
  5. package/dist/atlas-client.js +173 -0
  6. package/dist/auth-cmds/commands.d.ts +15 -0
  7. package/dist/auth-cmds/commands.js +249 -0
  8. package/dist/auth.d.ts +26 -0
  9. package/dist/auth.js +171 -0
  10. package/dist/bin.d.ts +2 -0
  11. package/dist/bin.js +8 -0
  12. package/dist/cli-errors.d.ts +5 -0
  13. package/dist/cli-errors.js +78 -0
  14. package/dist/config.d.ts +44 -0
  15. package/dist/config.js +103 -0
  16. package/dist/env-flags.d.ts +8 -0
  17. package/dist/env-flags.js +47 -0
  18. package/dist/functions/bundle.d.ts +30 -0
  19. package/dist/functions/bundle.js +137 -0
  20. package/dist/functions/commands.d.ts +86 -0
  21. package/dist/functions/commands.js +999 -0
  22. package/dist/functions/egress-preview.d.ts +32 -0
  23. package/dist/functions/egress-preview.js +54 -0
  24. package/dist/functions/lockfile-origin.d.ts +16 -0
  25. package/dist/functions/lockfile-origin.js +45 -0
  26. package/dist/functions/manifest.d.ts +89 -0
  27. package/dist/functions/manifest.js +586 -0
  28. package/dist/functions/secret-reconcile.d.ts +79 -0
  29. package/dist/functions/secret-reconcile.js +86 -0
  30. package/dist/main.d.ts +14 -0
  31. package/dist/main.js +129 -0
  32. package/dist/orm/delegate.d.ts +8 -0
  33. package/dist/orm/delegate.js +61 -0
  34. package/dist/pat-hints.d.ts +17 -0
  35. package/dist/pat-hints.js +28 -0
  36. package/dist/preview.d.ts +89 -0
  37. package/dist/preview.js +291 -0
  38. package/dist/process/agent-loader.d.ts +24 -0
  39. package/dist/process/agent-loader.js +57 -0
  40. package/dist/process/build.d.ts +14 -0
  41. package/dist/process/build.js +368 -0
  42. package/dist/process/codegen.d.ts +18 -0
  43. package/dist/process/codegen.js +270 -0
  44. package/dist/process/commands.d.ts +47 -0
  45. package/dist/process/commands.js +786 -0
  46. package/dist/process/discover.d.ts +32 -0
  47. package/dist/process/discover.js +131 -0
  48. package/dist/process/lint.d.ts +39 -0
  49. package/dist/process/lint.js +485 -0
  50. package/dist/process/local-bundle.d.ts +17 -0
  51. package/dist/process/local-bundle.js +65 -0
  52. package/dist/process/plan-diff.d.ts +82 -0
  53. package/dist/process/plan-diff.js +333 -0
  54. package/dist/process/resolve-process-pin.d.ts +11 -0
  55. package/dist/process/resolve-process-pin.js +63 -0
  56. package/dist/process/simulate.d.ts +50 -0
  57. package/dist/process/simulate.js +328 -0
  58. package/dist/prompt.d.ts +35 -0
  59. package/dist/prompt.js +65 -0
  60. package/dist/repos/commands.d.ts +49 -0
  61. package/dist/repos/commands.js +548 -0
  62. package/dist/repos/git-clone.d.ts +10 -0
  63. package/dist/repos/git-clone.js +49 -0
  64. package/dist/secrets/commands.d.ts +24 -0
  65. package/dist/secrets/commands.js +704 -0
  66. package/dist/templates/process/example-process/process.ts +43 -0
  67. package/dist/templates/process/package.json +23 -0
  68. package/dist/templates/process/pnpm-workspace.yaml +21 -0
  69. package/dist/templates/process/tsconfig.json +17 -0
  70. package/package.json +78 -0
  71. package/templates/process/example-process/process.ts +43 -0
  72. package/templates/process/package.json +23 -0
  73. package/templates/process/pnpm-workspace.yaml +21 -0
  74. package/templates/process/tsconfig.json +17 -0
package/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # `@sequenceholdings/studio-cli` — `seq-studio`
2
+
3
+ Standalone CLI for the Sequence platform: Lattice processes, Artifact Studio
4
+ apps, Managed Functions, Managed Secrets, and platform git repos. Runs from
5
+ any repo against the platform over HTTP — no monorepo checkout required.
6
+
7
+ ```
8
+ seq-studio process lint
9
+ seq-studio process plan -e staging
10
+ seq-studio process apply -e staging
11
+ seq-studio artifact deploy -e staging
12
+ seq-studio doctor
13
+ ```
14
+
15
+ > **Note:** all network commands require a Sequence platform account with the
16
+ > appropriate permissions. Without one, only the offline commands
17
+ > (`init`, `lint`, `simulate`, `build`, `bundle inspect`) work.
18
+ > Sequence-internal contributors: see `INTERNAL.md` in the monorepo for
19
+ > rollout SOPs, preview environments, and publishing docs.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ # In a process or artifact repo (devDependency, using pnpm — see below):
25
+ pnpm add -D @sequenceholdings/studio-cli @sequenceholdings/lattice
26
+
27
+ # Or globally:
28
+ pnpm add -g @sequenceholdings/studio-cli
29
+ ```
30
+
31
+ ### Why pnpm and not npm
32
+
33
+ Sequence uses pnpm's `minimumReleaseAge` setting (in `pnpm-workspace.yaml`)
34
+ as a 7-day supply-chain quarantine on new package releases. `npm` has no
35
+ equivalent and would happily install a freshly-published malicious
36
+ version of any transitive dep. **External process repos scaffolded by
37
+ `seq-studio process init` ship a `pnpm-workspace.yaml` with the same
38
+ guard.** Stick with pnpm so the policy actually applies.
39
+ (The seq-studio publish chain — `atlas-ui`, `lattice-form-renderer`,
40
+ `artifact-studio`, `lattice`, `studio-cli` — is excluded from the quarantine;
41
+ those come from the Studio repo's own publish pipeline, so new `seq-studio`
42
+ releases install immediately.)
43
+
44
+ ## Authenticate
45
+
46
+ `seq-studio` is a **read-only consumer of seqapi tokens** — it does not
47
+ have its own login flow. Run `seqapi login` once to mint tokens at
48
+ `~/.config/sequence-api/tokens.json`; `seqapi`, `seq-studio process`, and
49
+ `seq-studio artifact` all share that file.
50
+
51
+ ```bash
52
+ seqapi login
53
+ seq-studio doctor # confirms config + auth + authorization
54
+ ```
55
+
56
+ ### Headless auth (CI) — M2M
57
+
58
+ When there's no interactive login (CI, automation), set the service-account
59
+ secret and `seq-studio` mints a token via the Auth0 client-credentials grant
60
+ (M2M wins when the secret is set, otherwise the cached user token is used):
61
+
62
+ ```bash
63
+ export AUTH0_M2M_CLIENT_SECRET=... # provided by your platform administrator
64
+ seq-studio artifact deploy -e staging
65
+ ```
66
+
67
+ The secret is read at runtime — never commit it. M2M carries app scopes but
68
+ **no user identity / workspace membership**, so it's blind to
69
+ user-scoped/private resources.
70
+
71
+ **Manual escape hatch:** any `artifact` command also accepts an explicit
72
+ `--token <jwt>`, which wins over both the M2M and cached-user paths.
73
+
74
+ ## Environments (`~/.config/lattice/config.toml`)
75
+
76
+ Environment names map to platform URLs. Your deployment's URLs are configured
77
+ in `~/.config/lattice/config.toml`:
78
+
79
+ ```toml
80
+ [env.local]
81
+ url = "http://localhost:5001"
82
+
83
+ [env.staging]
84
+ url = "https://staging.example.com"
85
+
86
+ default_env = "local"
87
+ ```
88
+
89
+ Pass `--env <name>` (or `-e <name>`) on commands that talk to the platform.
90
+
91
+ `default_env` applies to `process` commands. `artifact` commands read
92
+ `config.toml` only when `--env` *is* passed; with no `--env` they fall back to
93
+ the artifact folder's `.artifact-studio/config.json` `defaultEnv` (set by
94
+ `artifact link` / `artifact env use`).
95
+
96
+ ## Process commands
97
+
98
+ | Command | What it does |
99
+ |---------|--------------|
100
+ | `seq-studio process init <dir>` | Scaffold a process repo: `package.json`, sample `process.ts`, `tsconfig.json` |
101
+ | `seq-studio process lint` | Static checks (graph, return contracts, agent schema, timeouts) |
102
+ | `seq-studio process plan -e <env>` | Build bundle, diff against currently-active version |
103
+ | `seq-studio process apply -e <env> [--only <id1,id2>]` | Build → register bundle → promote each process. `--only` promotes just the named process ids (the bundle still contains the whole root — registration is inert) |
104
+ | `seq-studio process test -e <env>` | CI wrapper: lint + plan, non-zero exit on errors or BREAKING diffs |
105
+ | `seq-studio process simulate <id>` | In-process walk with stubbed runners (offline) |
106
+ | `seq-studio process bundle build [-o file.json]` | Build a bundle locally |
107
+ | `seq-studio process bundle pull <hash> [-e <env>] [-o file.json]` | Fetch a stored bundle |
108
+ | `seq-studio process bundle inspect <bundle.json>` | Show a saved bundle's summary |
109
+ | `seq-studio process bundle list [-e <env>] [--limit N] [--cursor <hash>]` | List registered bundles (paginated; CLI auto-fetches all pages) |
110
+ | `seq-studio process bundle publish <hash or bundle.json> [-e <env>]` | Register a local bundle (no promote) |
111
+
112
+ ### Process discovery
113
+
114
+ `seq-studio` walks the current working directory for any subfolder
115
+ containing a `process.ts` file. Each `process.ts` must
116
+ `export default defineProcess(...)`. Set `LATTICE_PROCESSES_ROOT` to
117
+ point at a different folder.
118
+
119
+ ```
120
+ my-processes/
121
+ demo-process/
122
+ process.ts
123
+ loan-origination/
124
+ process.ts
125
+ ```
126
+
127
+ ```bash
128
+ cd my-processes
129
+ seq-studio process lint # finds both processes
130
+ ```
131
+
132
+ ## Artifact commands
133
+
134
+ `seq-studio artifact <sub>` is the entry point for Artifact Studio. It runs
135
+ the `@sequenceholdings/artifact-studio` library (its `./cli` `runCli` export),
136
+ routed through `~/.config/lattice/config.toml` and the shared seqapi token.
137
+
138
+ | Command | Underlying `runCli` verb |
139
+ |---------|--------------------------|
140
+ | `seq-studio artifact init <dir>` | `init <dir>` |
141
+ | `seq-studio artifact link [dir] -e <env> [--project <id>]` | `link [dir] --env <env>` |
142
+ | `seq-studio artifact build [dir]` | `build [dir]` |
143
+ | `seq-studio artifact plan [dir] -e <env>` | `plan [dir] --env <env>` |
144
+ | `seq-studio artifact deploy [dir] -e <env> [--skip-unchanged] [--no-create] [--project <id>]` | `deploy [dir] --env <env>` — `--skip-unchanged` no-ops (before building) when the remote active deployment's `sourceHash` already matches the local source; `--no-create` errors instead of creating a missing project; `--project <id>` targets a project directly when duplicate slugs make the lookup ambiguous (slug must still match the manifest) |
145
+ | `seq-studio artifact dev [dir] -e <env>` | `dev [dir] --env <env>` |
146
+ | `seq-studio artifact pull <project-id> -e <env> [--out <dir>]` | `pull <project-id> --env <env> [--out <dir>]` |
147
+ | `seq-studio artifact list -e <env>` | `list --env <env>` — projects visible on the environment (slug, id, active version, visibility) |
148
+ | `seq-studio artifact show <slug-or-id> -e <env>` | `show <slug-or-id> --env <env>` — one project's detail incl. active deployment + git provenance |
149
+ | `seq-studio artifact promote <deployment-id> -e <env>` | `promote <deployment-id> --env <env>` |
150
+ | `seq-studio artifact rollback <deployment-id> -e <env>` | `rollback <deployment-id> --env <env>` |
151
+
152
+ `pull` writes the **active deployment's** source files (default out dir:
153
+ `./<project-id>`) and errors when the project has no active deployment.
154
+ `promote` and `rollback` resolve the target project from the current
155
+ directory's `.artifact-studio/config.json` — run them from the linked artifact
156
+ folder (or run `seq-studio artifact link` first).
157
+
158
+ ### Source for `build` / `plan` / `deploy`
159
+
160
+ These three commands read their source tree from one of three places — the rest
161
+ of the pipeline (build → upload) is identical regardless of origin:
162
+
163
+ | Flag | Source |
164
+ |------|--------|
165
+ | _(none)_ / `[dir]` | a local folder (default, unchanged) |
166
+ | `--repo <ns>/<name>` | a repo in the platform Git Service, e.g. `--repo artifacts/accounts` |
167
+ | `--git-url <url>` | any git URL (cloned to a temp dir) |
168
+
169
+ `--ref <branch\|tag\|commit>` selects the revision (default: the repo's default
170
+ branch). A remote source is materialized to a temp dir, built, and torn down;
171
+ the target project is resolved from the source's `artifact.bundle.yml`
172
+ `project_id`. The deployment records the resolved commit as its `git_commit`
173
+ provenance. `--repo` needs `-e <env>` + a platform login; `--git-url` needs git
174
+ credentials for private repos. `dev` is local-only.
175
+
176
+ ## Pull / clone / deploy — which path?
177
+
178
+ These three look similar but fetch different things:
179
+
180
+ | Command | What you get |
181
+ |---------|--------------|
182
+ | `seq-studio artifact pull <project-id>` | Active **deployment** source files for an Artifact Studio project (built bundle inputs), keyed by project UUID — **not** git history |
183
+ | `seq-studio repos clone <ns>/<name>` | Repo source tree. With `ATLAS_GIT_PAT`, a real `git clone` via smart-HTTP; otherwise JSON-API materialize (no `.git` dir) |
184
+ | `seq-studio artifact deploy --repo <ns>/<name>` | Build + deploy an artifact whose source lives in the platform git service (materializes, builds, uploads) |
185
+
186
+ ## Auth (git-service PATs)
187
+
188
+ Personal Access Tokens authenticate `git clone` / `git push` (Basic auth:
189
+ any username, PAT as password). **You do not need `seqapi`.**
190
+
191
+ ### Everyone (recommended) — Atlas UI
192
+
193
+ 1. Open **Settings → Tokens** in Atlas for your environment, e.g.
194
+ - Staging: https://staging.atlas.seqholdings.com/settings/tokens
195
+ - Production: https://atlas.seqholdings.com/settings/tokens
196
+ - BankSouth: https://banksouth.seqholdings.com/settings/tokens
197
+ 2. **New token** → scopes `repo:read` (add `repo:write` for push) → copy once
198
+ 3. Export and clone:
199
+
200
+ ```bash
201
+ export ATLAS_GIT_PAT=atlas_git_…
202
+ seq-studio repos clone artifacts/ai-fluency -e staging
203
+ # or: git clone https://git:$ATLAS_GIT_PAT@staging.atlas.seqholdings.com/api/git-service/repos/<id>/git
204
+ ```
205
+
206
+ You can also open **Repositories → Access tokens** / the clone popover’s
207
+ **Manage tokens** link.
208
+
209
+ ### Sequence staff — CLI mint (optional)
210
+
211
+ Requires `seqapi login` (Auth0). Same tokens as the UI:
212
+
213
+ ```bash
214
+ seqapi login
215
+ seq-studio auth pat create --name laptop --scopes repo:read,repo:write -e staging
216
+ # optional: --expires 7d|30d|90d|1y|never (default 30d)
217
+ # optional: --store-credentials # git credential approve for the env host
218
+
219
+ seq-studio auth pat list -e staging
220
+ seq-studio auth pat revoke <id> -e staging --yes
221
+ ```
222
+
223
+ The raw token is printed **once** on create.
224
+
225
+ ## Repos commands
226
+
227
+ `seq-studio repos <sub>` manages repos in the platform Git Service over the
228
+ JSON API — the same repos `--repo <ns>/<name>` sources build from.
229
+
230
+ | Command | What it does |
231
+ |---------|--------------|
232
+ | `seq-studio repos list [-e <env>] [--namespace <slug>] [--mine]` | repos visible on the environment (permission-filtered) |
233
+ | `seq-studio repos namespaces [create <slug>] [-e <env>]` | list namespaces, or create one (creator becomes owner) |
234
+ | `seq-studio repos show <ns>/<name> [-e <env>]` | detail: id, branches, clone URL; artifact project id when slug matches |
235
+ | `seq-studio repos create <ns>/<name> [-e <env>] [--default-branch <b>]` | create an empty repo (needs namespace write) |
236
+ | `seq-studio repos clone <ns>/<name> \| --url <clone-url> \| --id <uuid> [-e <env>] [--ref <r>] [--out <dir>] [--force]` | smart-HTTP `git clone` when `ATLAS_GIT_PAT` is set (`--url`/`--id` need no seqapi); otherwise JSON materialize + PAT hint |
237
+ | `seq-studio repos pull <ns>/<name> [-e <env>] [--ref <r>] [--out <dir>] [--force]` | always materialize via JSON API (no `.git` dir); refuses a non-empty destination unless `--force` |
238
+ | `seq-studio repos delete <ns>/<name> [-e <env>] [--yes]` | delete a repo — interactive confirm unless `--yes` |
239
+
240
+ `show` prints the smart-HTTP clone URL (`…/repos/<id>/git`). Basic auth:
241
+ any username, PAT as password. Prefer `repos clone` over hand-rolling the
242
+ tree API.
243
+
244
+ ## Authorization
245
+
246
+ The CLI carries no permissions of its own: every request is authorized
247
+ server-side against your platform identity. Process `apply` and bundle
248
+ registration require platform write access; managed functions, secrets,
249
+ artifact projects, and repos are each gated by their own per-resource
250
+ permissions. A `403` means your account lacks the grant for that resource —
251
+ contact your platform administrator.
252
+
253
+ ## Design
254
+
255
+ `seq-studio` knows only the platform HTTP API, the seqapi token file, and
256
+ `config.toml`. The server does the work — register the bundle, promote the
257
+ version, write the audit log — so the CLI runs from any checkout without
258
+ server-side wiring on the developer's machine.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `seq-studio artifact <sub>` — manage Artifact Studio apps.
3
+ *
4
+ * Delegates to `runCli` from the `@sequenceholdings/artifact-studio/cli`
5
+ * library export, so the command implementations live in one place. We:
6
+ *
7
+ * 1. Resolve `--env` from `~/.config/lattice/config.toml`.
8
+ * 2. Set `ARTIFACT_STUDIO_BASE_URL` so artifact-studio's API client
9
+ * uses the resolved URL (instead of its own built-in env map).
10
+ * 3. Set `ARTIFACT_STUDIO_TOKEN` from the seqapi token file so auth
11
+ * is shared with the rest of `seq-studio`.
12
+ * 4. Forward all remaining argv to `runCli`.
13
+ */
14
+ export declare function runArtifactCommand(sub: string | undefined, rest: string[]): Promise<number>;
15
+ /**
16
+ * Strip the seq-studio-only `--pr <number>` and `--env-url <url>` flags from
17
+ * argv and return the rest (forwarded to artifact-studio) plus the parsed
18
+ * values. These never reach artifact-studio — they're consumed here to resolve
19
+ * the preview base URL.
20
+ */
21
+ export declare function extractPreviewFlags(argv: readonly string[]): {
22
+ rest: string[];
23
+ prNumber?: number;
24
+ envUrl?: string;
25
+ };
@@ -0,0 +1,263 @@
1
+ /**
2
+ * `seq-studio artifact <sub>` — manage Artifact Studio apps.
3
+ *
4
+ * Delegates to `runCli` from the `@sequenceholdings/artifact-studio/cli`
5
+ * library export, so the command implementations live in one place. We:
6
+ *
7
+ * 1. Resolve `--env` from `~/.config/lattice/config.toml`.
8
+ * 2. Set `ARTIFACT_STUDIO_BASE_URL` so artifact-studio's API client
9
+ * uses the resolved URL (instead of its own built-in env map).
10
+ * 3. Set `ARTIFACT_STUDIO_TOKEN` from the seqapi token file so auth
11
+ * is shared with the rest of `seq-studio`.
12
+ * 4. Forward all remaining argv to `runCli`.
13
+ */
14
+ import { getAccessToken, tryGetAccessToken } from '../auth.js';
15
+ import { readConfig, resolveEnv } from '../config.js';
16
+ import { normalizeShortEnvFlag, readEnvFromArgv } from '../env-flags.js';
17
+ import { PREVIEW_DOMAIN, PREVIEW_PROJECT, resolvePreviewByPr } from '../preview.js';
18
+ const ARTIFACT_USAGE = `usage:
19
+ seq-studio artifact init <dir>
20
+ seq-studio artifact validate [dir]
21
+ seq-studio artifact build [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--out file.json]
22
+ seq-studio artifact plan [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] -e <env>
23
+ seq-studio artifact deploy [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] -e <env> [--skip-unchanged] [--no-create] [--project <id>]
24
+ seq-studio artifact dev [dir] -e <env> [--once] [--key default]
25
+ seq-studio artifact pull <project-id> -e <env> --out <dir>
26
+ seq-studio artifact promote <deployment-id> -e <env>
27
+ seq-studio artifact rollback <deployment-id> -e <env>
28
+ seq-studio artifact list -e <env>
29
+ seq-studio artifact show <slug-or-id> -e <env>
30
+ seq-studio artifact status [-e <env>]
31
+ seq-studio artifact whoami -e <env>
32
+ seq-studio artifact env list
33
+
34
+ Built-in envs: local, staging, production, banksouth.
35
+
36
+ Source for build/plan/deploy: a local [dir] (default), a platform git-service
37
+ repo (--repo <ns>/<name>), or any git URL (--git-url <url>). --ref selects a
38
+ branch/tag/commit (default: the repo's default branch).
39
+
40
+ --repo clones over smart-HTTP and requires ATLAS_GIT_PAT (a repo:read PAT —
41
+ seq-studio auth pat create, or Atlas → Settings → Tokens).
42
+
43
+ Per-PR preview environments (https://studio-atlas-git-<slug>.preview.seqholdings.com):
44
+ -e preview:<branch-or-slug> compute the preview host from a branch name
45
+ --pr <number> resolve the preview host from a PR (handles long slugs)
46
+ --env-url <url> target an explicit preview URL (escape hatch)
47
+
48
+ Preview deploys sit behind a Cloudflare WAF gate. Off the company network/VPN,
49
+ set PREVIEW_ACCESS_HEADER=<secret> and the CLI sends it as x-preview-access.
50
+
51
+ Authenticate with: seqapi login
52
+ `;
53
+ export async function runArtifactCommand(sub, rest) {
54
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
55
+ console.log(ARTIFACT_USAGE);
56
+ return sub ? 0 : 1;
57
+ }
58
+ // Normalize `-e <env>` / `-e=<env>` to `--env <env>` because
59
+ // artifact-studio's argv parser (`shared/services/artifact-studio/src/cli.ts`)
60
+ // only recognizes long flags. Without this rewrite `seq-studio
61
+ // artifact plan -e staging` would land `-e` as positional and
62
+ // `staging` as the next positional, with `flags.env` undefined.
63
+ const normalizedRest = normalizeShortEnvFlag(rest);
64
+ // Pull off seq-studio-only preview flags (`--pr`, `--env-url`) so they're not
65
+ // forwarded to artifact-studio (its parser would store them as stray flags).
66
+ const { rest: forwardRest, prNumber, envUrl } = extractPreviewFlags(normalizedRest);
67
+ // Resolve --env if present so we can map to lattice config URLs. We
68
+ // pull from the cleaned argv so both `-e` and `--env` paths work.
69
+ const requested = readEnvFromArgv(forwardRest);
70
+ let resolved;
71
+ let argvForCli = forwardRest;
72
+ if (envUrl !== undefined) {
73
+ const validated = validatePreviewEnvUrl(envUrl);
74
+ // Explicit override wins over everything. Keep the user's --env name if
75
+ // they gave one, else label it `preview`.
76
+ resolved = { name: requested ?? 'preview', url: validated };
77
+ argvForCli = ensureEnvFlag(forwardRest, resolved.name);
78
+ }
79
+ else if (prNumber !== undefined) {
80
+ const preview = await resolvePreviewByPr({ pr: prNumber });
81
+ resolved = { name: `preview:${preview.slug}`, url: preview.url };
82
+ argvForCli = ensureEnvFlag(forwardRest, resolved.name);
83
+ }
84
+ else if (requested) {
85
+ const config = await readConfig();
86
+ resolved = resolveEnv({ config, requested });
87
+ // For `preview:<slug>` normalize the forwarded --env to the canonical name.
88
+ if (resolved.name !== requested)
89
+ argvForCli = ensureEnvFlag(forwardRest, resolved.name);
90
+ }
91
+ if (resolved) {
92
+ process.env['ARTIFACT_STUDIO_BASE_URL'] = resolved.url;
93
+ // When targeting a preview host, thread the Cloudflare WAF bypass header so
94
+ // deploys work off the company network/VPN (mirrors atlas-ui-shot). Harmless
95
+ // on built-in envs, so we only gate it on the host actually being a preview.
96
+ if (isPreviewUrl(resolved.url))
97
+ applyPreviewAccessHeader();
98
+ }
99
+ // Share the seqapi token. `tryGetAccessToken` resolves an M2M
100
+ // service-account token when AUTH0_M2M_CLIENT_SECRET is set (headless /
101
+ // CI / cloud-agent path) and otherwise the cached `seqapi login` user
102
+ // token (see ../auth.ts). If neither is available, artifact-studio
103
+ // commands that need a token surface their own error — we don't force
104
+ // `seqapi login` here because some commands (init, validate, build) work
105
+ // offline. Manual escape hatch: pass `--token <jwt>` (artifact-studio's
106
+ // CLI accepts it and it wins over everything).
107
+ const token = await tryGetAccessToken({ failClosedForM2m: true });
108
+ if (token) {
109
+ process.env['ARTIFACT_STUDIO_TOKEN'] = token;
110
+ }
111
+ // Lazy import so `process` / `doctor` commands don't pull in
112
+ // artifact-studio's vite/react/tailwind dependency graph.
113
+ const { runCli: runArtifactStudio, setTokenProvider } = await import('@sequenceholdings/artifact-studio/cli');
114
+ // The ARTIFACT_STUDIO_TOKEN env var above is captured once and never
115
+ // refreshes, so long-running commands (notably `artifact dev`) would start
116
+ // failing with "Authentication failed" once the initial token's TTL elapses.
117
+ // Hand artifact-studio a refreshing source — getAccessToken() mints a fresh
118
+ // access token via the Auth0 refresh grant when the cached one is near
119
+ // expiry — so a watch session survives indefinitely.
120
+ setTokenProvider(async () => {
121
+ if (process.env.AUTH0_M2M_CLIENT_SECRET?.trim()) {
122
+ // Fail closed for configured M2M failures so headless deploys never
123
+ // silently fall back to another cached identity.
124
+ return await getAccessToken();
125
+ }
126
+ try {
127
+ return await getAccessToken();
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ });
133
+ return runArtifactStudio([sub, ...argvForCli]);
134
+ }
135
+ /**
136
+ * Strip the seq-studio-only `--pr <number>` and `--env-url <url>` flags from
137
+ * argv and return the rest (forwarded to artifact-studio) plus the parsed
138
+ * values. These never reach artifact-studio — they're consumed here to resolve
139
+ * the preview base URL.
140
+ */
141
+ export function extractPreviewFlags(argv) {
142
+ const rest = [];
143
+ let prNumber;
144
+ let envUrl;
145
+ for (let i = 0; i < argv.length; i++) {
146
+ const arg = argv[i];
147
+ if (arg === undefined)
148
+ continue;
149
+ const [flag, inlineValue] = splitInlineValue(arg);
150
+ if (flag === '--pr') {
151
+ const value = inlineValue ?? takeValue(argv, i, () => (i += 1));
152
+ const parsed = Number(value);
153
+ if (!Number.isInteger(parsed) || parsed <= 0) {
154
+ throw new Error(`--pr requires a positive integer (got "${value ?? ''}").`);
155
+ }
156
+ prNumber = parsed;
157
+ }
158
+ else if (flag === '--env-url') {
159
+ const value = inlineValue ?? takeValue(argv, i, () => (i += 1));
160
+ if (!value)
161
+ throw new Error('--env-url requires a URL value.');
162
+ envUrl = value;
163
+ }
164
+ else {
165
+ rest.push(arg);
166
+ }
167
+ }
168
+ return { rest, prNumber, envUrl };
169
+ }
170
+ function splitInlineValue(arg) {
171
+ const eq = arg.indexOf('=');
172
+ if (arg.startsWith('--') && eq !== -1)
173
+ return [arg.slice(0, eq), arg.slice(eq + 1)];
174
+ return [arg, undefined];
175
+ }
176
+ function takeValue(argv, i, advance) {
177
+ const next = argv[i + 1];
178
+ if (next !== undefined && !next.startsWith('-')) {
179
+ advance();
180
+ return next;
181
+ }
182
+ return undefined;
183
+ }
184
+ /** Set or replace the `--env <name>` token so artifact-studio gets a clean name. */
185
+ function ensureEnvFlag(argv, name) {
186
+ const out = [];
187
+ let replaced = false;
188
+ for (let i = 0; i < argv.length; i++) {
189
+ const arg = argv[i];
190
+ if (arg === undefined)
191
+ continue;
192
+ if (arg === '--env') {
193
+ out.push('--env', name);
194
+ // Skip the original value token if present.
195
+ const next = argv[i + 1];
196
+ if (next !== undefined && !next.startsWith('-'))
197
+ i += 1;
198
+ replaced = true;
199
+ }
200
+ else {
201
+ out.push(arg);
202
+ }
203
+ }
204
+ if (!replaced)
205
+ out.push('--env', name);
206
+ return out;
207
+ }
208
+ function stripTrailingSlash(url) {
209
+ return url.replace(/\/+$/, '');
210
+ }
211
+ function validatePreviewEnvUrl(value) {
212
+ const trimmed = stripTrailingSlash(value.trim());
213
+ let parsed;
214
+ try {
215
+ parsed = new URL(trimmed);
216
+ }
217
+ catch {
218
+ throw new Error(`--env-url must be a valid URL (got "${value}").`);
219
+ }
220
+ if (parsed.username || parsed.password) {
221
+ throw new Error('--env-url must not include a username/password component.');
222
+ }
223
+ if (parsed.protocol !== 'https:') {
224
+ throw new Error(`--env-url must be an https preview URL on *.${PREVIEW_DOMAIN} (got "${parsed.protocol}//").`);
225
+ }
226
+ if (parsed.pathname !== '' && parsed.pathname !== '/') {
227
+ throw new Error('--env-url must be an origin URL without a path component.');
228
+ }
229
+ if (parsed.search || parsed.hash) {
230
+ throw new Error('--env-url must not include querystring or hash components.');
231
+ }
232
+ const host = parsed.hostname.toLowerCase();
233
+ if (!host.endsWith(`.${PREVIEW_DOMAIN}`)) {
234
+ throw new Error(`--env-url must be an https preview URL on *.${PREVIEW_DOMAIN} (got "${parsed.hostname}").`);
235
+ }
236
+ const expectedPrefix = `${PREVIEW_PROJECT}-git-`;
237
+ if (!host.startsWith(expectedPrefix)) {
238
+ throw new Error(`--env-url must target a ${PREVIEW_PROJECT} preview alias (expected host starting with "${expectedPrefix}", got "${parsed.hostname}").`);
239
+ }
240
+ return parsed.origin;
241
+ }
242
+ function isPreviewUrl(url) {
243
+ try {
244
+ return new URL(url).hostname.endsWith(`.${PREVIEW_DOMAIN}`);
245
+ }
246
+ catch {
247
+ return false;
248
+ }
249
+ }
250
+ /**
251
+ * Thread the Cloudflare WAF bypass header through artifact-studio's HTTP client
252
+ * via ARTIFACT_STUDIO_EXTRA_HEADERS (read in artifact-studio/src/api.ts). Reads
253
+ * the secret from PREVIEW_ACCESS_HEADER — the same env var atlas-ui-shot uses.
254
+ * No-op when the secret isn't set (engineer is expected to be on the VPN).
255
+ */
256
+ function applyPreviewAccessHeader() {
257
+ const secret = process.env['PREVIEW_ACCESS_HEADER']?.trim();
258
+ if (!secret)
259
+ return;
260
+ process.env['ARTIFACT_STUDIO_EXTRA_HEADERS'] = JSON.stringify({
261
+ 'x-preview-access': secret,
262
+ });
263
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Minimal authenticated HTTP client for Atlas. Same retry-on-503 contract
3
+ * as `shared/services/artifact-studio/src/api.ts` so behavior is
4
+ * consistent across both Sequence CLIs. Token + baseUrl are passed in by
5
+ * the caller — this file knows nothing about token files or config TOMLs.
6
+ */
7
+ export interface ClientOptions {
8
+ baseUrl: string;
9
+ token: string;
10
+ }
11
+ export declare class AtlasApiError extends Error {
12
+ readonly status: number;
13
+ readonly path: string;
14
+ readonly statusText: string;
15
+ readonly body: unknown;
16
+ readonly name = "AtlasApiError";
17
+ constructor(status: number, message: string, path: string, statusText?: string, body?: unknown);
18
+ }
19
+ export declare function getJson<T>({ baseUrl, token, path, }: ClientOptions & {
20
+ path: string;
21
+ }): Promise<T>;
22
+ export declare function getJsonOr404<T>({ baseUrl, token, path, }: ClientOptions & {
23
+ path: string;
24
+ }): Promise<T | null>;
25
+ export declare function postJson<T>({ baseUrl, token, path, body, }: ClientOptions & {
26
+ path: string;
27
+ body?: unknown;
28
+ }): Promise<T>;
29
+ export declare function putJson<T>({ baseUrl, token, path, body, }: ClientOptions & {
30
+ path: string;
31
+ body?: unknown;
32
+ }): Promise<T>;
33
+ export declare function patchJson<T>({ baseUrl, token, path, body, }: ClientOptions & {
34
+ path: string;
35
+ body?: unknown;
36
+ }): Promise<T>;
37
+ export declare function deleteJson<T>({ baseUrl, token, path, body, }: ClientOptions & {
38
+ path: string;
39
+ body?: unknown;
40
+ }): Promise<T>;
41
+ /** DELETE for endpoints that return 204 with an empty body (deleteJson would choke on it). */
42
+ export declare function deleteNoContent({ baseUrl, token, path, }: ClientOptions & {
43
+ path: string;
44
+ }): Promise<void>;