@sequenceholdings/studio-cli 0.1.12 → 0.1.18
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.
- package/README.md +116 -30
- package/dist/artifact/delegate.d.ts +2 -2
- package/dist/artifact/delegate.js +31 -73
- package/dist/atlas-client.js +52 -37
- package/dist/auth-cmds/commands.d.ts +1 -1
- package/dist/auth-cmds/commands.js +12 -7
- package/dist/auth.d.ts +97 -19
- package/dist/auth.js +376 -81
- package/dist/config.d.ts +13 -3
- package/dist/config.js +41 -14
- package/dist/env-catalog.js +13 -3
- package/dist/env-flags.d.ts +2 -0
- package/dist/env-flags.js +2 -0
- package/dist/env-registry.d.ts +27 -0
- package/dist/env-registry.js +204 -0
- package/dist/envs/commands.d.ts +1 -1
- package/dist/envs/commands.js +65 -10
- package/dist/file-lock.d.ts +5 -0
- package/dist/file-lock.js +187 -0
- package/dist/functions/commands.d.ts +9 -1
- package/dist/functions/commands.js +71 -29
- package/dist/functions/manifest.d.ts +1 -0
- package/dist/functions/manifest.js +36 -0
- package/dist/login.d.ts +14 -3
- package/dist/login.js +60 -40
- package/dist/main.d.ts +2 -1
- package/dist/main.js +36 -12
- package/dist/orm/delegate.d.ts +9 -0
- package/dist/orm/delegate.js +36 -4
- package/dist/pat-hints.js +2 -2
- package/dist/pipeline/commands.d.ts +58 -0
- package/dist/pipeline/commands.js +330 -0
- package/dist/pipeline/lifecycle.d.ts +58 -0
- package/dist/pipeline/lifecycle.js +348 -0
- package/dist/pipeline/pinning.d.ts +5 -0
- package/dist/pipeline/pinning.js +9 -0
- package/dist/pipeline/templates.d.ts +11 -0
- package/dist/pipeline/templates.js +166 -0
- package/dist/process/build.d.ts +12 -1
- package/dist/process/build.js +49 -2
- package/dist/process/codegen.js +21 -1
- package/dist/process/commands.d.ts +19 -0
- package/dist/process/commands.js +153 -40
- package/dist/process/compiler-subprocess.d.ts +29 -0
- package/dist/process/compiler-subprocess.js +99 -0
- package/dist/process/compiler-worker.d.ts +1 -0
- package/dist/process/compiler-worker.js +38 -0
- package/dist/process/discover.d.ts +4 -1
- package/dist/process/discover.js +5 -2
- package/dist/process/lint.d.ts +8 -0
- package/dist/process/lint.js +125 -29
- package/dist/process/repo-install.d.ts +21 -0
- package/dist/process/repo-install.js +99 -0
- package/dist/process/simulate.js +14 -1
- package/dist/repos/commands.d.ts +1 -1
- package/dist/repos/commands.js +17 -12
- package/dist/secrets/commands.d.ts +1 -1
- package/dist/secrets/commands.js +18 -18
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# `@sequenceholdings/studio-cli` — `seq-studio`
|
|
2
2
|
|
|
3
3
|
Standalone CLI for the Sequence platform: Lattice processes, Artifact Studio
|
|
4
|
-
apps, Managed Functions, Managed Secrets, and platform git repos. Runs from
|
|
4
|
+
apps, Managed Functions, Managed Secrets, Data Pipelines stage specs, and platform git repos. Runs from
|
|
5
5
|
any repo against the platform over HTTP — no monorepo checkout required.
|
|
6
6
|
|
|
7
7
|
```
|
|
@@ -9,13 +9,15 @@ seq-studio process lint
|
|
|
9
9
|
seq-studio process plan -e <env>
|
|
10
10
|
seq-studio process apply -e <env>
|
|
11
11
|
seq-studio artifact deploy -e <env>
|
|
12
|
+
seq-studio pipeline validate
|
|
12
13
|
seq-studio envs list
|
|
13
14
|
seq-studio doctor
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
> **Note:** all network commands require a Sequence platform account with the
|
|
17
18
|
> appropriate permissions. Without one, only the offline commands
|
|
18
|
-
> (`init`, `lint`, `simulate`, `build`, `bundle inspect`
|
|
19
|
+
> (`init`, `lint`, `simulate`, `build`, `bundle inspect`, `pipeline init`,
|
|
20
|
+
> `pipeline validate`) work.
|
|
19
21
|
> Sequence-internal contributors: see `INTERNAL.md` in the monorepo for
|
|
20
22
|
> rollout SOPs, preview environments, and publishing docs.
|
|
21
23
|
|
|
@@ -44,16 +46,20 @@ releases install immediately.)
|
|
|
44
46
|
|
|
45
47
|
## Authenticate
|
|
46
48
|
|
|
47
|
-
Run the built-in browser login
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
Run the built-in browser login. `seq-studio` and `seqapi` share short-lived
|
|
50
|
+
access tokens at `~/.config/sequence-api/tokens.json`, so logging in with either
|
|
51
|
+
CLI authenticates both. Refresh tokens are neither requested nor persisted;
|
|
52
|
+
interactive commands perform a bounded PKCE login again after expiry.
|
|
50
53
|
|
|
51
54
|
```bash
|
|
52
|
-
seq-studio login
|
|
53
|
-
seq-studio
|
|
55
|
+
seq-studio login # built-in Sequence environments
|
|
56
|
+
seq-studio envs add bsm-staging <tenant-url> # one-time tenant registration
|
|
57
|
+
seq-studio login --env bsm-staging # registered OpCo tenant realm
|
|
58
|
+
seq-studio doctor # confirms config + auth + authorization
|
|
54
59
|
```
|
|
55
60
|
|
|
56
|
-
Use `seq-studio logout` to remove the shared
|
|
61
|
+
Use `seq-studio logout` to remove the shared Sequence session, or
|
|
62
|
+
`seq-studio logout --env bsm-staging` to remove only that OpCo realm's session.
|
|
57
63
|
|
|
58
64
|
### Headless auth (CI) — M2M
|
|
59
65
|
|
|
@@ -62,16 +68,19 @@ secret and `seq-studio` mints a token via the Auth0 client-credentials grant
|
|
|
62
68
|
(M2M wins when the secret is set, otherwise the cached user token is used):
|
|
63
69
|
|
|
64
70
|
```bash
|
|
65
|
-
export AUTH0_M2M_CLIENT_SECRET=...
|
|
71
|
+
export AUTH0_M2M_CLIENT_SECRET=... # built-in Sequence environments
|
|
72
|
+
export AUTH0_M2M_CLIENT_SECRET_BSM_STAGING=... # registered bsm-staging realm
|
|
66
73
|
seq-studio artifact deploy -e <env>
|
|
67
74
|
```
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
Registered OpCo environment names are upper-snaked in the variable suffix
|
|
77
|
+
(`bsm-staging` → `BSM_STAGING`). Secrets are read at runtime — never commit them.
|
|
78
|
+
M2M carries app access but
|
|
70
79
|
**no user identity / workspace membership**, so it's blind to
|
|
71
80
|
user-scoped/private resources.
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
Artifact commands do not accept bearer tokens through argv or environment.
|
|
83
|
+
Use interactive login or the realm-specific M2M secret above.
|
|
75
84
|
|
|
76
85
|
## Environments
|
|
77
86
|
|
|
@@ -82,7 +91,7 @@ environment-discovery endpoint and caches the result at
|
|
|
82
91
|
`~/.config/lattice/environments.json`.
|
|
83
92
|
|
|
84
93
|
```bash
|
|
85
|
-
seq-studio login # or
|
|
94
|
+
seq-studio login # or use the matching M2M secret described above
|
|
86
95
|
seq-studio envs refresh # fetch the environments visible to your identity
|
|
87
96
|
seq-studio envs list # show them (name, URL, source)
|
|
88
97
|
```
|
|
@@ -93,17 +102,23 @@ see depends on who you are — unauthenticated installs get `local` only, and
|
|
|
93
102
|
authenticated identities get the deployments they're entitled to. Visibility
|
|
94
103
|
is not access control: every request is still authorized server-side.
|
|
95
104
|
|
|
96
|
-
You can
|
|
97
|
-
`~/.config/lattice/config.toml` (user entries win over discovered ones)
|
|
105
|
+
You can add or override trusted environments yourself in
|
|
106
|
+
`~/.config/lattice/config.toml` (user entries win over discovered ones).
|
|
107
|
+
Authenticated requests accept only HTTPS origins at `seqholdings.com` or its
|
|
108
|
+
subdomains, plus HTTP loopback origins for local development. The CLI
|
|
109
|
+
revalidates that exact origin immediately before attaching credentials and
|
|
110
|
+
never follows authenticated redirects.
|
|
98
111
|
|
|
99
112
|
```toml
|
|
113
|
+
# Must come before any [env.*] table — TOML attaches bare keys to the
|
|
114
|
+
# preceding table, so a trailing default_env is silently ignored.
|
|
115
|
+
default_env = "local"
|
|
116
|
+
|
|
100
117
|
[env.local]
|
|
101
118
|
url = "http://localhost:5001"
|
|
102
119
|
|
|
103
120
|
[env.my-atlas]
|
|
104
|
-
url = "https://atlas.
|
|
105
|
-
|
|
106
|
-
default_env = "local"
|
|
121
|
+
url = "https://my-atlas.seqholdings.com"
|
|
107
122
|
```
|
|
108
123
|
|
|
109
124
|
Pass `--env <name>` (or `-e <name>`) on commands that talk to the platform.
|
|
@@ -121,14 +136,43 @@ the artifact folder's `.artifact-studio/config.json` `defaultEnv` (set by
|
|
|
121
136
|
| `seq-studio process lint` | Static checks (graph, return contracts, agent schema, timeouts) |
|
|
122
137
|
| `seq-studio process plan -e <env>` | Build bundle, diff against currently-active version |
|
|
123
138
|
| `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) |
|
|
139
|
+
| `seq-studio process apply --repo processes/<name> [--ref <r>] -e <env> [--only <id1,id2>]` | Same as above, but materializes the source from a platform git-service repo. Pinned commit SHA is injected as bundle provenance. Requires `ATLAS_GIT_PAT` (or the target realm's M2M secret for CI) |
|
|
124
140
|
| `seq-studio process test -e <env>` | CI wrapper: lint + plan, non-zero exit on errors or BREAKING diffs |
|
|
125
|
-
| `seq-studio process simulate <id>` |
|
|
141
|
+
| `seq-studio process simulate <id>` | Isolated child-process walk with stubbed runners (offline) |
|
|
126
142
|
| `seq-studio process bundle build [-o file.json]` | Build a bundle locally |
|
|
127
143
|
| `seq-studio process bundle pull <hash> [-e <env>] [-o file.json]` | Fetch a stored bundle |
|
|
128
144
|
| `seq-studio process bundle inspect <bundle.json>` | Show a saved bundle's summary |
|
|
129
145
|
| `seq-studio process bundle list [-e <env>] [--limit N] [--cursor <hash>]` | List registered bundles (paginated; CLI auto-fetches all pages) |
|
|
130
146
|
| `seq-studio process bundle publish <hash or bundle.json> [-e <env>]` | Register a local bundle (no promote) |
|
|
131
147
|
|
|
148
|
+
### Remote source for `apply`
|
|
149
|
+
|
|
150
|
+
`apply` can deploy a process from the platform git service instead of a local
|
|
151
|
+
checkout:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
seq-studio process apply --repo processes/my-process -e staging
|
|
155
|
+
seq-studio process apply --repo processes/my-process --ref v1.2.0 -e production
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The source is materialized to a temp dir, dependencies are installed from the
|
|
159
|
+
committed `pnpm-lock.yaml` (must be Chainguard-resolved), process definitions
|
|
160
|
+
are discovered, the bundle is built with the pinned commit as provenance, and the
|
|
161
|
+
temp tree is cleaned up — even on error. Only the `processes` namespace is
|
|
162
|
+
accepted; other namespaces (artifacts, managed-functions) are rejected.
|
|
163
|
+
|
|
164
|
+
Auth: same as `artifact deploy --repo` — `ATLAS_GIT_PAT` for the smart-HTTP
|
|
165
|
+
clone path, or the target realm's M2M secret for CI's JSON materialize path.
|
|
166
|
+
Those credentials remain in the parent CLI and are not forwarded to dependency
|
|
167
|
+
installation or process build tooling. Each install uses fresh temporary
|
|
168
|
+
package-manager caches that are removed after the invocation.
|
|
169
|
+
|
|
170
|
+
This is ambient-credential isolation, not an OS security sandbox. Build workers
|
|
171
|
+
still run as the caller's uid, so hostile source may inspect other same-uid
|
|
172
|
+
processes or readable files on platforms that permit it (for example, Linux
|
|
173
|
+
`/proc`). Build reviewed source only; use a dedicated ephemeral runner with no
|
|
174
|
+
unrelated credentials when the source is not trusted.
|
|
175
|
+
|
|
132
176
|
### Process discovery
|
|
133
177
|
|
|
134
178
|
`seq-studio` walks the current working directory for any subfolder
|
|
@@ -184,14 +228,26 @@ of the pipeline (build → upload) is identical regardless of origin:
|
|
|
184
228
|
|------|--------|
|
|
185
229
|
| _(none)_ / `[dir]` | a local folder (default, unchanged) |
|
|
186
230
|
| `--repo <ns>/<name>` | a repo in the platform Git Service, e.g. `--repo artifacts/accounts` |
|
|
187
|
-
| `--git-url <url>` |
|
|
231
|
+
| `--git-url <url>` | a public, credential-free HTTPS git URL (cloned to a temp dir) |
|
|
188
232
|
|
|
189
233
|
`--ref <branch\|tag\|commit>` selects the revision (default: the repo's default
|
|
190
234
|
branch). A remote source is materialized to a temp dir, built, and torn down;
|
|
191
235
|
the target project is resolved from the source's `artifact.bundle.yml`
|
|
192
236
|
`project_id`. The deployment records the resolved commit as its `git_commit`
|
|
193
|
-
provenance. `--repo` needs `-e <env>` + a platform login
|
|
194
|
-
|
|
237
|
+
provenance. `--repo` needs `-e <env>` + a platform login. M2M/CI builds accept
|
|
238
|
+
only platform-managed `--repo` sources; `--git-url` is interactive-only and
|
|
239
|
+
rejects embedded credentials, query tokens, SSH/file URLs, and Git remote
|
|
240
|
+
helpers. `dev` is local-only.
|
|
241
|
+
|
|
242
|
+
Artifact builds, dependency installs, process-definition imports, and local
|
|
243
|
+
process simulations run in short-lived children with an allowlisted runtime
|
|
244
|
+
environment and a synthetic home/config directory. Parent bearer tokens, Auth0
|
|
245
|
+
M2M secrets, Doppler tokens, cloud credentials, GitHub tokens, and user config
|
|
246
|
+
files are not inherited. The platform Git Service clone path passes only its
|
|
247
|
+
single scoped PAT to `git` through an ephemeral askpass helper.
|
|
248
|
+
This is a credential-inheritance boundary, not an OS filesystem sandbox:
|
|
249
|
+
child code still runs as the invoking user and must be treated as trusted to
|
|
250
|
+
read files that it names explicitly outside the synthetic home.
|
|
195
251
|
|
|
196
252
|
## Pull / clone / deploy — which path?
|
|
197
253
|
|
|
@@ -230,7 +286,7 @@ You can also open **Repositories → Access tokens** / the clone popover’s
|
|
|
230
286
|
Requires Auth0 login. Same identity as the UI:
|
|
231
287
|
|
|
232
288
|
```bash
|
|
233
|
-
seq-studio login
|
|
289
|
+
seq-studio login # add --env <registered-opco-env> for a tenant realm
|
|
234
290
|
seq-studio auth pat create --name laptop --scopes repo:read,repo:write -e <env>
|
|
235
291
|
# optional: --expires 7d|30d|90d|1y|never (default 30d)
|
|
236
292
|
# optional: --store-credentials # git credential approve for the env host
|
|
@@ -248,18 +304,48 @@ JSON API — the same repos `--repo <ns>/<name>` sources build from.
|
|
|
248
304
|
|
|
249
305
|
| Command | What it does |
|
|
250
306
|
|---------|--------------|
|
|
251
|
-
| `seq-studio repos list
|
|
252
|
-
| `seq-studio repos namespaces [create <slug>]
|
|
253
|
-
| `seq-studio repos show <ns>/<name>
|
|
254
|
-
| `seq-studio repos create <ns>/<name>
|
|
255
|
-
| `seq-studio repos clone <ns>/<name> \| --url <clone-url> \| --id <uuid>
|
|
256
|
-
| `seq-studio repos pull <ns>/<name>
|
|
257
|
-
| `seq-studio repos delete <ns>/<name>
|
|
307
|
+
| `seq-studio repos list -e <env> [--namespace <slug>] [--mine]` | repos visible on the environment (permission-filtered) |
|
|
308
|
+
| `seq-studio repos namespaces [create <slug>] -e <env>` | list namespaces, or create one (creator becomes owner) |
|
|
309
|
+
| `seq-studio repos show <ns>/<name> -e <env>` | detail: id, branches, clone URL; artifact project id when slug matches |
|
|
310
|
+
| `seq-studio repos create <ns>/<name> -e <env> [--default-branch <b>]` | create an empty repo (needs namespace write) |
|
|
311
|
+
| `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 |
|
|
312
|
+
| `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` |
|
|
313
|
+
| `seq-studio repos delete <ns>/<name> -e <env> [--yes]` | delete a repo — interactive confirm unless `--yes` |
|
|
258
314
|
|
|
259
315
|
`show` prints the smart-HTTP clone URL (`…/repos/<id>/git`). Basic auth:
|
|
260
316
|
any username, PAT as password. Prefer `repos clone` over hand-rolling the
|
|
261
317
|
tree API.
|
|
262
318
|
|
|
319
|
+
## Pipeline commands
|
|
320
|
+
|
|
321
|
+
`seq-studio pipeline <sub>` authors and validates Data Pipelines **stage
|
|
322
|
+
specs** — the typed contracts (`<name>.stage.yml`) a Pipeline repo
|
|
323
|
+
(`pipelines/<domain>`) is made of. Validation logic lives in
|
|
324
|
+
`@sequenceholdings/pipeline-spec` (an optional peer, like `@sequenceholdings/orm`);
|
|
325
|
+
install it alongside the CLI to use this family.
|
|
326
|
+
|
|
327
|
+
| Command | What it does |
|
|
328
|
+
|---------|--------------|
|
|
329
|
+
| `seq-studio pipeline init --type ingestion\|transformation\|serving <name> [--dir <dir>]` | Scaffold `<name>.stage.yml` (commented per-kind template) plus a `src/` entrypoint stub (serving stages are declarative — no stub). Refuses to overwrite an existing spec |
|
|
330
|
+
| `seq-studio pipeline validate [dir] [--assets <file\|url>] [--json]` | Run the full offline spec gate: envelope + body validation, `schema_ref` resolution, and repo-level graph validation (reference resolution, single-writer, cycles, column subsets, serving projection checks). Exit 0/1 |
|
|
331
|
+
| `seq-studio pipeline plan --repo pipelines/<slug> --ref <sha\|branch> -e <env> [--json]` | Plan a Pipeline deploy (materialize → SDK/`validateSpecGraph` → compile → live-diff → provision findings). Does **not** run Databricks `bundle validate` (that is a Trigger deploy-path hard gate). Exit 1 on destructive findings (CI-safe). `--json` emits the stable plan envelope |
|
|
332
|
+
| `seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha> -e <env> [--approved-by <sub>] [--no-wait]` | Plan then enqueue deploy; Trigger runs `bundle validate` then `bundle deploy` against reviewed bytes. Polls to terminal unless `--no-wait`. Production/banksouth require a pinned 40-hex SHA (client + server) |
|
|
333
|
+
| `seq-studio pipeline run-now --stage <slug> -e <env> [--repo pipelines/<slug>] [--json]` | Run the stage's active job or DLT pipeline immediately and print its Databricks run URL |
|
|
334
|
+
| `seq-studio pipeline promote --stage <slug> --version <v> -e <env> [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]` | Promote a validated version to another environment. Prod/banksouth require `--approved-by` naming the authenticated caller (approvals are self-recorded); `--repo` disambiguates a slug that exists in multiple Pipelines |
|
|
335
|
+
| `seq-studio pipeline rollback --stage <slug> -e <env> [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]` | Redeploy the previously retired deployment's version. Prod/banksouth require `--approved-by` — approvals are explicit even for rollbacks |
|
|
336
|
+
|
|
337
|
+
`validate` is offline — no network or database. `--assets` supplies a
|
|
338
|
+
registry **asset export** (JSON) so inputs referencing other Pipelines'
|
|
339
|
+
assets resolve; pass a local file or an authenticated URL. `--json` emits a
|
|
340
|
+
stable machine shape (`{ ok, stages, findings[] }` with machine-readable
|
|
341
|
+
finding codes and consumer lists) for CI annotation — the
|
|
342
|
+
`pipeline-verify.yml` workflow runs exactly this on PRs touching
|
|
343
|
+
`pipelines/**`.
|
|
344
|
+
|
|
345
|
+
For local Atlas development, `-e local` uses the server's isolated `dev`
|
|
346
|
+
deployment target. Plan, deploy, and `run-now` therefore operate on the same
|
|
347
|
+
personal dev Databricks catalog and Lakebase branch configured for local Atlas.
|
|
348
|
+
|
|
263
349
|
## Authorization
|
|
264
350
|
|
|
265
351
|
The CLI carries no permissions of its own: every request is authorized
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* 1. Resolve `--env` from `~/.config/lattice/config.toml`.
|
|
8
8
|
* 2. Set `ARTIFACT_STUDIO_BASE_URL` so artifact-studio's API client
|
|
9
9
|
* uses the resolved URL (instead of its own built-in env map).
|
|
10
|
-
* 3.
|
|
11
|
-
*
|
|
10
|
+
* 3. Install an internal token provider so auth is shared with the rest of
|
|
11
|
+
* `seq-studio` without exposing the bearer in argv or environment.
|
|
12
12
|
* 4. Forward all remaining argv to `runCli`.
|
|
13
13
|
*/
|
|
14
14
|
export declare function runArtifactCommand(sub: string | undefined, rest: string[]): Promise<number>;
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* 1. Resolve `--env` from `~/.config/lattice/config.toml`.
|
|
8
8
|
* 2. Set `ARTIFACT_STUDIO_BASE_URL` so artifact-studio's API client
|
|
9
9
|
* uses the resolved URL (instead of its own built-in env map).
|
|
10
|
-
* 3.
|
|
11
|
-
*
|
|
10
|
+
* 3. Install an internal token provider so auth is shared with the rest of
|
|
11
|
+
* `seq-studio` without exposing the bearer in argv or environment.
|
|
12
12
|
* 4. Forward all remaining argv to `runCli`.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import { getAccessTokenWithMode, M2mTokenError, tryGetAccessTokenWithMode, } from '../auth.js';
|
|
15
15
|
import { readConfig, resolveEnvWithDiscovery } from '../config.js';
|
|
16
16
|
import { fetchCatalog, readCachedCatalog } from '../env-catalog.js';
|
|
17
17
|
import { normalizeShortEnvFlag, readEnvFromArgv } from '../env-flags.js';
|
|
@@ -36,11 +36,13 @@ const ARTIFACT_USAGE = `usage:
|
|
|
36
36
|
discovered after you authenticate).
|
|
37
37
|
|
|
38
38
|
Source for build/plan/deploy: a local [dir] (default), a platform git-service
|
|
39
|
-
repo (--repo <ns>/<name>), or
|
|
39
|
+
repo (--repo <ns>/<name>), or a public HTTPS git URL (--git-url <url>). --ref selects a
|
|
40
40
|
branch/tag/commit (default: the repo's default branch).
|
|
41
41
|
|
|
42
|
-
--repo
|
|
43
|
-
seq-studio auth pat create, or Atlas → Settings → Tokens).
|
|
42
|
+
Interactive --repo builds clone over smart-HTTP and require ATLAS_GIT_PAT
|
|
43
|
+
(a repo:read PAT — seq-studio auth pat create, or Atlas → Settings → Tokens).
|
|
44
|
+
Headless M2M builds use JSON materialize and accept only platform-managed
|
|
45
|
+
--repo sources.
|
|
44
46
|
|
|
45
47
|
Per-PR preview environments (https://studio-atlas-git-<slug>.preview.seqholdings.com):
|
|
46
48
|
-e preview:<branch-or-slug> compute the preview host from a branch name
|
|
@@ -53,23 +55,20 @@ const ARTIFACT_USAGE = `usage:
|
|
|
53
55
|
Authenticate with: seq-studio login
|
|
54
56
|
`;
|
|
55
57
|
export async function runArtifactCommand(sub, rest) {
|
|
58
|
+
if (rest.some((arg) => arg === '--token' || arg.startsWith('--token='))) {
|
|
59
|
+
console.error('seq-studio artifact does not accept bearer tokens via --token. ' +
|
|
60
|
+
'Use `seq-studio login` interactively or configure the realm-specific ' +
|
|
61
|
+
'AUTH0_M2M_CLIENT_SECRET for headless automation.');
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
// Never pass a legacy bearer environment variable into the embedded package.
|
|
65
|
+
delete process.env['ARTIFACT_STUDIO_TOKEN'];
|
|
56
66
|
// Older artifact-studio versions exposed nested login/logout commands backed
|
|
57
67
|
// by a separate token file. Keep those commands working, but route them to
|
|
58
68
|
// seq-studio's shared token so every namespace uses the same identity.
|
|
59
69
|
if (sub === 'login' || sub === 'logout') {
|
|
60
70
|
if (rest.length > 0) {
|
|
61
|
-
|
|
62
|
-
// The legacy `artifact login --token <jwt>` persisted a bearer to
|
|
63
|
-
// artifact-studio's own token file — a store this unification retires.
|
|
64
|
-
console.error('seq-studio artifact login no longer stores a bearer token.\n' +
|
|
65
|
-
'Scripted/headless options:\n' +
|
|
66
|
-
' - pass --token <jwt> directly to the artifact command (deploy/plan/whoami/...)\n' +
|
|
67
|
-
' - export ARTIFACT_STUDIO_TOKEN=<jwt> for the session\n' +
|
|
68
|
-
' - set AUTH0_M2M_CLIENT_SECRET for service-account (M2M) auth in CI');
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
console.error(`seq-studio artifact ${sub} does not accept arguments.`);
|
|
72
|
-
}
|
|
71
|
+
console.error(`seq-studio artifact ${sub} does not accept arguments.`);
|
|
73
72
|
return 1;
|
|
74
73
|
}
|
|
75
74
|
const auth = await import('../login.js');
|
|
@@ -133,51 +132,22 @@ export async function runArtifactCommand(sub, rest) {
|
|
|
133
132
|
// without an explicit --env flag — the stored `.artifact-studio/config.json`
|
|
134
133
|
// defaultEnv, `artifact env set <name>` — would otherwise fall back to
|
|
135
134
|
// artifact-studio's built-in local-only map and fail on deployed envs.
|
|
136
|
-
const
|
|
137
|
-
process.env['ARTIFACT_STUDIO_ENV_URLS'] = JSON.stringify(Object.fromEntries(Object.entries(envs).map(([name, { url }]) => [name, url])));
|
|
138
|
-
// An explicit `--token <jwt>` is the manual escape hatch and must win over
|
|
139
|
-
// everything, including a configured-but-failing M2M credential (which
|
|
140
|
-
// `tryGetAccessToken({ failClosedForM2m: true })` would otherwise turn into
|
|
141
|
-
// an abort before argv ever reaches artifact-studio). artifact-studio's
|
|
142
|
-
// `getOptionalToken` checks `flags.token` first, so when it's present we
|
|
143
|
-
// skip shared-token resolution entirely. Only the two-token `--token <jwt>`
|
|
144
|
-
// form counts: artifact-studio's parser does not split `--token=<jwt>`.
|
|
145
|
-
const hasExplicitToken = hasTokenFlag(argvForCli);
|
|
135
|
+
const routingConfig = await readConfig();
|
|
136
|
+
process.env['ARTIFACT_STUDIO_ENV_URLS'] = JSON.stringify(Object.fromEntries(Object.entries(routingConfig.envs).map(([name, { url }]) => [name, url])));
|
|
146
137
|
// Lazy import so `process` / `doctor` commands don't pull in
|
|
147
138
|
// artifact-studio's vite/react/tailwind dependency graph.
|
|
148
|
-
const { runCli: runArtifactStudio, setTokenProvider } = await import('@sequenceholdings/artifact-studio/cli');
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
// The ARTIFACT_STUDIO_TOKEN env var above is captured once and never
|
|
162
|
-
// refreshes, so long-running commands (notably `artifact dev`) would start
|
|
163
|
-
// failing with "Authentication failed" once the initial token's TTL
|
|
164
|
-
// elapses. Hand artifact-studio a refreshing source — getAccessToken()
|
|
165
|
-
// mints a fresh access token via the Auth0 refresh grant when the cached
|
|
166
|
-
// one is near expiry — so a watch session survives indefinitely.
|
|
167
|
-
setTokenProvider(async () => {
|
|
168
|
-
if (process.env.AUTH0_M2M_CLIENT_SECRET?.trim()) {
|
|
169
|
-
// Fail closed for configured M2M failures so headless deploys never
|
|
170
|
-
// silently fall back to another cached identity.
|
|
171
|
-
return await getAccessToken();
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
return await getAccessToken();
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
}
|
|
139
|
+
const { getConfiguredDefaultEnv, runCli: runArtifactStudio, setTokenProvider } = await import('@sequenceholdings/artifact-studio/cli');
|
|
140
|
+
const authEnvName = resolved?.name ?? (await getConfiguredDefaultEnv());
|
|
141
|
+
const targetUrl = resolved?.url ??
|
|
142
|
+
process.env['ARTIFACT_STUDIO_BASE_URL']?.trim() ??
|
|
143
|
+
(authEnvName ? routingConfig.envs[authEnvName]?.url : undefined);
|
|
144
|
+
setTokenProvider(({ allowInteractiveLogin }) => allowInteractiveLogin
|
|
145
|
+
? getAccessTokenWithMode({ env: authEnvName, targetUrl })
|
|
146
|
+
: tryGetAccessTokenWithMode({
|
|
147
|
+
failClosedForM2m: true,
|
|
148
|
+
env: authEnvName,
|
|
149
|
+
targetUrl,
|
|
150
|
+
}));
|
|
181
151
|
return runArtifactStudio([sub, ...argvForCli]);
|
|
182
152
|
}
|
|
183
153
|
/**
|
|
@@ -240,18 +210,6 @@ export function extractPreviewFlags(argv) {
|
|
|
240
210
|
}
|
|
241
211
|
return { rest, prNumber, envUrl };
|
|
242
212
|
}
|
|
243
|
-
/**
|
|
244
|
-
* True when argv carries an explicit two-token `--token <jwt>` that
|
|
245
|
-
* artifact-studio's parser will bind to `flags.token`. `--token=<jwt>` is
|
|
246
|
-
* excluded because that parser stores it as a stray flag and never binds it.
|
|
247
|
-
*/
|
|
248
|
-
function hasTokenFlag(argv) {
|
|
249
|
-
const index = argv.indexOf('--token');
|
|
250
|
-
if (index === -1)
|
|
251
|
-
return false;
|
|
252
|
-
const value = argv[index + 1];
|
|
253
|
-
return value !== undefined && !value.startsWith('--');
|
|
254
|
-
}
|
|
255
213
|
function splitInlineValue(arg) {
|
|
256
214
|
const eq = arg.indexOf('=');
|
|
257
215
|
if (arg.startsWith('--') && eq !== -1)
|
package/dist/atlas-client.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* consistent across both Sequence CLIs. Token + baseUrl are passed in by
|
|
5
5
|
* the caller — this file knows nothing about token files or config TOMLs.
|
|
6
6
|
*/
|
|
7
|
+
import { authenticatedRequestUrl } from '@sequenceholdings/artifact-studio/deployment-validation';
|
|
7
8
|
import { PREVIEW_DOMAIN } from './preview.js';
|
|
8
9
|
const MAX_503_RETRIES = 5;
|
|
9
10
|
const DEFAULT_RETRY_AFTER_SECONDS = 2;
|
|
@@ -34,6 +35,18 @@ async function fetchWith503Retry(input, init) {
|
|
|
34
35
|
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
async function authenticatedFetch({ baseUrl, init = {}, path, token, }) {
|
|
39
|
+
const url = authenticatedRequestUrl({ baseUrl, path });
|
|
40
|
+
return fetchWith503Retry(url, {
|
|
41
|
+
...init,
|
|
42
|
+
redirect: 'manual',
|
|
43
|
+
headers: {
|
|
44
|
+
...previewAccessHeaders(baseUrl),
|
|
45
|
+
...init.headers,
|
|
46
|
+
Authorization: `Bearer ${token}`,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
37
50
|
function previewAccessHeaders(baseUrl) {
|
|
38
51
|
const secret = process.env.PREVIEW_ACCESS_HEADER?.trim();
|
|
39
52
|
if (!secret)
|
|
@@ -89,17 +102,13 @@ async function responseError(response, path) {
|
|
|
89
102
|
return new AtlasApiError(response.status, message, path, response.statusText, body);
|
|
90
103
|
}
|
|
91
104
|
export async function getJson({ baseUrl, token, path, }) {
|
|
92
|
-
const response = await
|
|
93
|
-
headers: { ...previewAccessHeaders(baseUrl), Authorization: `Bearer ${token}` },
|
|
94
|
-
});
|
|
105
|
+
const response = await authenticatedFetch({ baseUrl, token, path });
|
|
95
106
|
if (!response.ok)
|
|
96
107
|
throw await responseError(response, path);
|
|
97
108
|
return response.json();
|
|
98
109
|
}
|
|
99
110
|
export async function getJsonOr404({ baseUrl, token, path, }) {
|
|
100
|
-
const response = await
|
|
101
|
-
headers: { ...previewAccessHeaders(baseUrl), Authorization: `Bearer ${token}` },
|
|
102
|
-
});
|
|
111
|
+
const response = await authenticatedFetch({ baseUrl, token, path });
|
|
103
112
|
if (response.status === 404)
|
|
104
113
|
return null;
|
|
105
114
|
if (!response.ok)
|
|
@@ -107,56 +116,60 @@ export async function getJsonOr404({ baseUrl, token, path, }) {
|
|
|
107
116
|
return response.json();
|
|
108
117
|
}
|
|
109
118
|
export async function postJson({ baseUrl, token, path, body, }) {
|
|
110
|
-
const response = await
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
const response = await authenticatedFetch({
|
|
120
|
+
baseUrl,
|
|
121
|
+
token,
|
|
122
|
+
path,
|
|
123
|
+
init: {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
116
127
|
},
|
|
117
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
118
128
|
});
|
|
119
129
|
if (!response.ok)
|
|
120
130
|
throw await responseError(response, path);
|
|
121
131
|
return response.json();
|
|
122
132
|
}
|
|
123
133
|
export async function putJson({ baseUrl, token, path, body, }) {
|
|
124
|
-
const response = await
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
const response = await authenticatedFetch({
|
|
135
|
+
baseUrl,
|
|
136
|
+
token,
|
|
137
|
+
path,
|
|
138
|
+
init: {
|
|
139
|
+
method: 'PUT',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
130
142
|
},
|
|
131
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
132
143
|
});
|
|
133
144
|
if (!response.ok)
|
|
134
145
|
throw await responseError(response, path);
|
|
135
146
|
return response.json();
|
|
136
147
|
}
|
|
137
148
|
export async function patchJson({ baseUrl, token, path, body, }) {
|
|
138
|
-
const response = await
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
const response = await authenticatedFetch({
|
|
150
|
+
baseUrl,
|
|
151
|
+
token,
|
|
152
|
+
path,
|
|
153
|
+
init: {
|
|
154
|
+
method: 'PATCH',
|
|
155
|
+
headers: { 'Content-Type': 'application/json' },
|
|
156
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
144
157
|
},
|
|
145
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
146
158
|
});
|
|
147
159
|
if (!response.ok)
|
|
148
160
|
throw await responseError(response, path);
|
|
149
161
|
return response.json();
|
|
150
162
|
}
|
|
151
163
|
export async function deleteJson({ baseUrl, token, path, body, }) {
|
|
152
|
-
const response = await
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
164
|
+
const response = await authenticatedFetch({
|
|
165
|
+
baseUrl,
|
|
166
|
+
token,
|
|
167
|
+
path,
|
|
168
|
+
init: {
|
|
169
|
+
method: 'DELETE',
|
|
170
|
+
headers: { 'Content-Type': 'application/json' },
|
|
171
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
158
172
|
},
|
|
159
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
160
173
|
});
|
|
161
174
|
if (!response.ok)
|
|
162
175
|
throw await responseError(response, path);
|
|
@@ -164,9 +177,11 @@ export async function deleteJson({ baseUrl, token, path, body, }) {
|
|
|
164
177
|
}
|
|
165
178
|
/** DELETE for endpoints that return 204 with an empty body (deleteJson would choke on it). */
|
|
166
179
|
export async function deleteNoContent({ baseUrl, token, path, }) {
|
|
167
|
-
const response = await
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
const response = await authenticatedFetch({
|
|
181
|
+
baseUrl,
|
|
182
|
+
token,
|
|
183
|
+
path,
|
|
184
|
+
init: { method: 'DELETE' },
|
|
170
185
|
});
|
|
171
186
|
if (!response.ok)
|
|
172
187
|
throw await responseError(response, path);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ParsedArgs } from '../process/commands.js';
|
|
2
2
|
declare const PAT_SCOPES: readonly ["repo:read", "repo:write", "repo:admin"];
|
|
3
3
|
type PatScope = (typeof PAT_SCOPES)[number];
|
|
4
|
-
export declare const AUTH_USAGE = "usage:\n seq-studio auth pat create --name <n> [--scopes repo:read,repo:write]
|
|
4
|
+
export declare const AUTH_USAGE = "usage:\n seq-studio auth pat create --name <n> -e <env> [--scopes repo:read,repo:write]\n [--expires 7d|30d|90d|1y|never] [--store-credentials]\n seq-studio auth pat list -e <env>\n seq-studio auth pat revoke <id> -e <env> [--yes]\n\n Issue a personal access token for git clone / git push against the platform\n git service.\n\n Authenticate with `seq-studio login`, then run `auth pat create -e <env>`.\n Alternatively, open Atlas \u2192 Settings \u2192 Tokens:\n https://<atlas-host>/settings/tokens\n Sign in, create a token (repo:read / repo:write), copy once, then:\n export ATLAS_GIT_PAT=<token>\n\n On create the raw token is printed ONCE \u2014 store it; Atlas cannot re-show it.\n Git Basic auth: any username (e.g. git), PAT as the password.\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n";
|
|
5
5
|
export declare function parsePatScopes(raw: string | undefined): PatScope[];
|
|
6
6
|
/**
|
|
7
7
|
* Map UI-style duration choices to an absolute ISO-8601 expiresAt, or undefined
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { deleteNoContent, getJson, postJson } from '../atlas-client.js';
|
|
12
12
|
import { printCliError } from '../cli-errors.js';
|
|
13
|
+
import { REQUIRE_EXPLICIT_ENV_MESSAGE } from '../env-flags.js';
|
|
13
14
|
import { buildContext, clientOptions, flagBool, LOG, } from '../functions/commands.js';
|
|
14
15
|
import { readConfig } from '../config.js';
|
|
15
16
|
import { confirmYes } from '../prompt.js';
|
|
@@ -38,15 +39,15 @@ function stringFlag(flags, key) {
|
|
|
38
39
|
return value;
|
|
39
40
|
}
|
|
40
41
|
export const AUTH_USAGE = `usage:
|
|
41
|
-
seq-studio auth pat create --name <n> [--scopes repo:read,repo:write]
|
|
42
|
+
seq-studio auth pat create --name <n> -e <env> [--scopes repo:read,repo:write]
|
|
42
43
|
[--expires 7d|30d|90d|1y|never] [--store-credentials]
|
|
43
|
-
seq-studio auth pat list
|
|
44
|
-
seq-studio auth pat revoke <id>
|
|
44
|
+
seq-studio auth pat list -e <env>
|
|
45
|
+
seq-studio auth pat revoke <id> -e <env> [--yes]
|
|
45
46
|
|
|
46
47
|
Issue a personal access token for git clone / git push against the platform
|
|
47
48
|
git service.
|
|
48
49
|
|
|
49
|
-
Authenticate with \`seq-studio login\`, then run \`auth pat create
|
|
50
|
+
Authenticate with \`seq-studio login\`, then run \`auth pat create -e <env>\`.
|
|
50
51
|
Alternatively, open Atlas → Settings → Tokens:
|
|
51
52
|
https://<atlas-host>/settings/tokens
|
|
52
53
|
Sign in, create a token (repo:read / repo:write), copy once, then:
|
|
@@ -55,7 +56,7 @@ export const AUTH_USAGE = `usage:
|
|
|
55
56
|
On create the raw token is printed ONCE — store it; Atlas cannot re-show it.
|
|
56
57
|
Git Basic auth: any username (e.g. git), PAT as the password.
|
|
57
58
|
|
|
58
|
-
Flags: -e/--env <env>
|
|
59
|
+
Flags: -e/--env <env> (required; see: seq-studio envs list)
|
|
59
60
|
`;
|
|
60
61
|
async function authContext(args) {
|
|
61
62
|
if (args.flags.env === true || args.flags.e === true) {
|
|
@@ -66,6 +67,10 @@ async function authContext(args) {
|
|
|
66
67
|
}
|
|
67
68
|
catch (err) {
|
|
68
69
|
const message = err instanceof Error ? err.message : String(err);
|
|
70
|
+
// Missing `-e` is actionable on its own — don't bury it under login/PAT
|
|
71
|
+
// setup advice (that wrap is for real auth/token failures).
|
|
72
|
+
if (message === REQUIRE_EXPLICIT_ENV_MESSAGE)
|
|
73
|
+
throw err;
|
|
69
74
|
// buildContext failed before we know the env URL; offer CLI login and
|
|
70
75
|
// render the tokens-page URLs from the environments visible to this
|
|
71
76
|
// identity (third-party / OpCo developers only see their own hosts).
|
|
@@ -119,7 +124,7 @@ export function resolveExpiresAt(choice) {
|
|
|
119
124
|
export async function authPatCreateCommand(args) {
|
|
120
125
|
const name = stringFlag(args.flags, 'name');
|
|
121
126
|
if (!name) {
|
|
122
|
-
console.error('usage: seq-studio auth pat create --name <n> [--scopes ...]
|
|
127
|
+
console.error('usage: seq-studio auth pat create --name <n> -e <env> [--scopes ...]');
|
|
123
128
|
return 1;
|
|
124
129
|
}
|
|
125
130
|
if (name.length < 3) {
|
|
@@ -193,7 +198,7 @@ export async function authPatListCommand(args) {
|
|
|
193
198
|
export async function authPatRevokeCommand(args) {
|
|
194
199
|
const id = args.positional[0];
|
|
195
200
|
if (!id) {
|
|
196
|
-
console.error('usage: seq-studio auth pat revoke <id>
|
|
201
|
+
console.error('usage: seq-studio auth pat revoke <id> -e <env> [--yes]');
|
|
197
202
|
return 1;
|
|
198
203
|
}
|
|
199
204
|
const ctx = await authContext(args);
|