@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.
- package/README.md +258 -0
- package/dist/artifact/delegate.d.ts +25 -0
- package/dist/artifact/delegate.js +263 -0
- package/dist/atlas-client.d.ts +44 -0
- package/dist/atlas-client.js +173 -0
- package/dist/auth-cmds/commands.d.ts +15 -0
- package/dist/auth-cmds/commands.js +249 -0
- package/dist/auth.d.ts +26 -0
- package/dist/auth.js +171 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +8 -0
- package/dist/cli-errors.d.ts +5 -0
- package/dist/cli-errors.js +78 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +103 -0
- package/dist/env-flags.d.ts +8 -0
- package/dist/env-flags.js +47 -0
- package/dist/functions/bundle.d.ts +30 -0
- package/dist/functions/bundle.js +137 -0
- package/dist/functions/commands.d.ts +86 -0
- package/dist/functions/commands.js +999 -0
- package/dist/functions/egress-preview.d.ts +32 -0
- package/dist/functions/egress-preview.js +54 -0
- package/dist/functions/lockfile-origin.d.ts +16 -0
- package/dist/functions/lockfile-origin.js +45 -0
- package/dist/functions/manifest.d.ts +89 -0
- package/dist/functions/manifest.js +586 -0
- package/dist/functions/secret-reconcile.d.ts +79 -0
- package/dist/functions/secret-reconcile.js +86 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.js +129 -0
- package/dist/orm/delegate.d.ts +8 -0
- package/dist/orm/delegate.js +61 -0
- package/dist/pat-hints.d.ts +17 -0
- package/dist/pat-hints.js +28 -0
- package/dist/preview.d.ts +89 -0
- package/dist/preview.js +291 -0
- package/dist/process/agent-loader.d.ts +24 -0
- package/dist/process/agent-loader.js +57 -0
- package/dist/process/build.d.ts +14 -0
- package/dist/process/build.js +368 -0
- package/dist/process/codegen.d.ts +18 -0
- package/dist/process/codegen.js +270 -0
- package/dist/process/commands.d.ts +47 -0
- package/dist/process/commands.js +786 -0
- package/dist/process/discover.d.ts +32 -0
- package/dist/process/discover.js +131 -0
- package/dist/process/lint.d.ts +39 -0
- package/dist/process/lint.js +485 -0
- package/dist/process/local-bundle.d.ts +17 -0
- package/dist/process/local-bundle.js +65 -0
- package/dist/process/plan-diff.d.ts +82 -0
- package/dist/process/plan-diff.js +333 -0
- package/dist/process/resolve-process-pin.d.ts +11 -0
- package/dist/process/resolve-process-pin.js +63 -0
- package/dist/process/simulate.d.ts +50 -0
- package/dist/process/simulate.js +328 -0
- package/dist/prompt.d.ts +35 -0
- package/dist/prompt.js +65 -0
- package/dist/repos/commands.d.ts +49 -0
- package/dist/repos/commands.js +548 -0
- package/dist/repos/git-clone.d.ts +10 -0
- package/dist/repos/git-clone.js +49 -0
- package/dist/secrets/commands.d.ts +24 -0
- package/dist/secrets/commands.js +704 -0
- package/dist/templates/process/example-process/process.ts +43 -0
- package/dist/templates/process/package.json +23 -0
- package/dist/templates/process/pnpm-workspace.yaml +21 -0
- package/dist/templates/process/tsconfig.json +17 -0
- package/package.json +78 -0
- package/templates/process/example-process/process.ts +43 -0
- package/templates/process/package.json +23 -0
- package/templates/process/pnpm-workspace.yaml +21 -0
- 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>;
|