@sequenceholdings/studio-cli 0.1.22 → 0.1.24
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 +115 -12
- package/dist/app/commands.d.ts +16 -0
- package/dist/app/commands.js +227 -0
- package/dist/app/deploy.d.ts +49 -0
- package/dist/app/deploy.js +197 -0
- package/dist/app/kinds.d.ts +10 -0
- package/dist/app/kinds.js +36 -0
- package/dist/app/manifest.d.ts +94 -0
- package/dist/app/manifest.js +273 -0
- package/dist/app/scaffold.d.ts +28 -0
- package/dist/app/scaffold.js +263 -0
- package/dist/functions/manifest.d.ts +22 -0
- package/dist/functions/manifest.js +45 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +21 -0
- package/dist/pipeline/codegen.d.ts +2 -0
- package/dist/pipeline/codegen.js +118 -0
- package/dist/pipeline/commands.d.ts +7 -0
- package/dist/pipeline/commands.js +86 -12
- package/dist/pipeline/lifecycle.d.ts +1 -12
- package/dist/pipeline/lifecycle.js +140 -35
- package/dist/pipeline/templates.js +3 -1
- package/dist/secrets/commands.d.ts +3 -1
- package/dist/secrets/commands.js +87 -26
- package/package.json +8 -8
- package/dist/pipeline/pinning.d.ts +0 -5
- package/dist/pipeline/pinning.js +0 -9
package/README.md
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Standalone CLI for the Sequence platform: typed agents, Lattice processes, Artifact Studio
|
|
4
4
|
apps, Managed Functions, Managed Secrets, ORM namespaces, Data Pipelines stage
|
|
5
|
-
specs, and platform git repos.
|
|
6
|
-
HTTP — no monorepo checkout required.
|
|
5
|
+
specs, and platform git repos. Its published workflows run from any repo
|
|
6
|
+
against the platform over HTTP — no monorepo checkout required. The v0
|
|
7
|
+
app-monorepo commands below are currently internal; see their availability
|
|
8
|
+
notice.
|
|
7
9
|
|
|
8
10
|
```
|
|
9
11
|
seq-studio process lint
|
|
@@ -43,7 +45,7 @@ equivalent and would happily install a freshly-published malicious
|
|
|
43
45
|
version of any transitive dep. **External process repos scaffolded by
|
|
44
46
|
`seq-studio process init` ship a `pnpm-workspace.yaml` with the same
|
|
45
47
|
guard.** Stick with pnpm so the policy actually applies.
|
|
46
|
-
(The seq-studio publish chain — `atlas-ui`, `lattice-form-renderer`,
|
|
48
|
+
(The seq-studio publish chain — `agent-spec`, `atlas-ui`, `lattice-form-renderer`,
|
|
47
49
|
`artifact-studio`, `lattice`, `studio-cli` — is excluded from the quarantine;
|
|
48
50
|
those come from the Studio repo's own publish pipeline, so new `seq-studio`
|
|
49
51
|
releases install immediately.)
|
|
@@ -134,6 +136,54 @@ Pass `--env <name>` (or `-e <name>`) on commands that talk to the platform.
|
|
|
134
136
|
the artifact folder's `.artifact-studio/config.json` `defaultEnv` (set by
|
|
135
137
|
`artifact link` / `artifact env use`).
|
|
136
138
|
|
|
139
|
+
## App monorepo commands
|
|
140
|
+
|
|
141
|
+
One app repo can contain ORM, managed functions, and an Artifact Studio UI.
|
|
142
|
+
The root `sequence.app.yml` manifest declares the structure — required for
|
|
143
|
+
scaffolding and for the eventual Applications UI. Do not hand-author apps
|
|
144
|
+
without it.
|
|
145
|
+
|
|
146
|
+
> **Availability:** This is currently an internal orchestration workflow.
|
|
147
|
+
> Generated ORM apps depend on unpublished `@sequenceholdings/orm`, so
|
|
148
|
+
> standalone external installs and the function-to-ORM runtime path are not
|
|
149
|
+
> supported yet.
|
|
150
|
+
|
|
151
|
+
| Command | What it does |
|
|
152
|
+
|---------|--------------|
|
|
153
|
+
| `seq-studio init <dir> --with <kinds>` | Scaffold an app monorepo + `sequence.app.yml` + selected primitive subfolders |
|
|
154
|
+
| `seq-studio add function|artifact <name>` | Add a function or Artifact Studio UI to the current app (updates the manifest) |
|
|
155
|
+
| `seq-studio deploy -e <env> [--yes] [--only id1,id2] [--dry-run]` | Deploy every primitive in `deploy.order` (orm → functions → artifact) |
|
|
156
|
+
|
|
157
|
+
`init` supports v0 kinds `orm`, `function`, and `artifact`; pass them as a
|
|
158
|
+
comma-list (`--with orm,function,artifact`) or boolean flags (`--orm`). `add`
|
|
159
|
+
currently supports functions and one Artifact Studio UI; ORM is created only by
|
|
160
|
+
`init`.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# Full-stack toy (ORM + function + artifact) — workspace / internal CLI only
|
|
164
|
+
seq-studio init pokedex --with orm,function,artifact --function-name get-pokemon
|
|
165
|
+
cd pokedex
|
|
166
|
+
(cd orm/pokedex && pnpm install)
|
|
167
|
+
(cd functions/get-pokemon && pnpm install)
|
|
168
|
+
(cd artifact && pnpm install)
|
|
169
|
+
|
|
170
|
+
seq-studio login
|
|
171
|
+
seq-studio deploy -e local --yes
|
|
172
|
+
|
|
173
|
+
# Day-2: add a second function without re-init
|
|
174
|
+
seq-studio add function list-types
|
|
175
|
+
(cd functions/list-types && pnpm install)
|
|
176
|
+
seq-studio deploy -e local --only list-types --yes
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Each primitive keeps its own `package.json` / lockfile. `deploy` fans out to
|
|
180
|
+
the existing per-primitive commands (`orm apply`, `functions deploy`,
|
|
181
|
+
`artifact deploy`). Pass `--yes` for non-interactive function deploys.
|
|
182
|
+
|
|
183
|
+
`deploy.on_error` defaults to `stop`. Set it to `continue` to deploy independent
|
|
184
|
+
primitives after a failure; any primitive whose `depends_on` prerequisite failed
|
|
185
|
+
or was skipped is skipped as well.
|
|
186
|
+
|
|
137
187
|
## Agent commands
|
|
138
188
|
|
|
139
189
|
Typed agent repositories export one or more `defineAgent(...)` values from files
|
|
@@ -355,6 +405,25 @@ limits:
|
|
|
355
405
|
`min_instances` defaults to `0`, cannot exceed `max_instances`, and incurs
|
|
356
406
|
Cloud Run idle-instance charges while warm.
|
|
357
407
|
|
|
408
|
+
### Server-owned resource authorization
|
|
409
|
+
|
|
410
|
+
Some functions expose regulated external resources whose authorization must be
|
|
411
|
+
enforced by Atlas rather than by author code. Those functions select a reviewed
|
|
412
|
+
adapter in `managed-function.yml`:
|
|
413
|
+
|
|
414
|
+
```yaml
|
|
415
|
+
authorization:
|
|
416
|
+
version: 1
|
|
417
|
+
adapter: encompass.loan-read-by-number
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The adapter name is a closed platform registry. Authors cannot provide JSON
|
|
421
|
+
pointers or custom filtering logic. `seq-studio functions build` rejects an
|
|
422
|
+
unknown adapter, an adapter on an unregistered function, or a protected
|
|
423
|
+
function whose required adapter is missing. The manifest declares only the
|
|
424
|
+
server-owned adapter; authors cannot configure its runtime resource selection
|
|
425
|
+
or policy.
|
|
426
|
+
|
|
358
427
|
### ORM data access
|
|
359
428
|
|
|
360
429
|
A function declares its ORM Data API reach in `capabilities.data`, grouped by
|
|
@@ -375,6 +444,22 @@ capabilities:
|
|
|
375
444
|
operations: [RollLuckyNumber]
|
|
376
445
|
```
|
|
377
446
|
|
|
447
|
+
## Managed Secret commands
|
|
448
|
+
|
|
449
|
+
| Command | What it does |
|
|
450
|
+
|---------|--------------|
|
|
451
|
+
| `seq-studio secrets create <NAME> -e <env> [--org <slug>]` | Register an org-owned secret (no value) |
|
|
452
|
+
| `seq-studio secrets set <NAME> -e <env> [--org <slug>] [--from-file <path>]` | Set the shared default (write-only; file is not echoed) |
|
|
453
|
+
| `seq-studio secrets list -e <env>` | Secrets you can see (never values) |
|
|
454
|
+
|
|
455
|
+
`--org` targets a managed-scope org other than the login tenant and selects
|
|
456
|
+
the intended same-named secret for `set`, `attach`, `detach`, `versions`,
|
|
457
|
+
`set-default`, and `pin`; it is required when the name exists in multiple
|
|
458
|
+
orgs. Tenant pipeline credentials must be created on that tenant's deployment:
|
|
459
|
+
`seq-studio secrets create NAME -e <tenant> --org <tenant>`. Sequence-owned
|
|
460
|
+
environments (`local`, `staging`, `production`) only accept `--org sequence`.
|
|
461
|
+
The API requires Managed Functions Admin on that org.
|
|
462
|
+
|
|
378
463
|
## Artifact commands
|
|
379
464
|
|
|
380
465
|
`seq-studio artifact <sub>` is the entry point for Artifact Studio. It runs
|
|
@@ -545,15 +630,33 @@ install it alongside the CLI to use this family.
|
|
|
545
630
|
|
|
546
631
|
| Command | What it does |
|
|
547
632
|
|---------|--------------|
|
|
548
|
-
| `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 |
|
|
549
|
-
| `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 |
|
|
550
|
-
| `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 |
|
|
551
|
-
| `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`.
|
|
552
|
-
| `seq-studio pipeline adopt --stage <slug> --ref <sha\|branch> -e <env> --native-id <id> --approved-by <you> [--resource-key <key>] [--kind job\|dlt_pipeline] [--old-source-removal-pr <url>] [--repo pipelines/<slug>]` | Bind a live Databricks job/pipeline into the stage without recreation (`bundle deployment bind` on Trigger). Always requires `--approved-by` naming the caller. When the key is still in the monorepo DAB, pass `--old-source-removal-pr` and follow the returned cutover checklist: unbind the old bundle state without deleting the remote, then remove its DAB declaration and add the target-specific adopted-resource entry in the same PR before redeploying. |
|
|
553
|
-
| `seq-studio pipeline unbind --stage <slug> --ref <sha\|branch> -e <env> --approved-by <you> [--resource-key <key>] [--repo pipelines/<slug>]` | Release an adopted binding on Trigger; the remote object stays live (never deleted) |
|
|
554
|
-
| `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 |
|
|
555
|
-
| `seq-studio pipeline promote --stage <slug> --version <v> -e <env> [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]` | Promote a validated version to another
|
|
556
|
-
| `seq-studio pipeline rollback --stage <slug> -e <env> [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]` | Redeploy the previously retired deployment's version.
|
|
633
|
+
| `seq-studio pipeline init --type ingestion\|transformation\|serving <name> [--dir <dir>]` | Scaffold `<name>.stage.yml` (commented per-kind template) plus a `src/` Databricks-notebook entrypoint stub (begins with `# Databricks notebook source`; serving stages are declarative — no stub). Refuses to overwrite an existing spec |
|
|
634
|
+
| `seq-studio pipeline validate [dir] [--assets <file\|url>] [--orm-contracts <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). Auto-loads `orm-contracts.json` from the pipeline dir when present. Exit 0/1 |
|
|
635
|
+
| `seq-studio pipeline plan --repo pipelines/<slug> --ref <sha\|branch> -e <env> [--target <id>] [--json]` | Plan a Pipeline deploy (materialize → SDK/`validateSpecGraph` → compile → live-diff → provision findings). Fails closed listing **every** missing target binding (alert channels, workspace, Databricks `source.credential`) plus Data Sync edge-worker machine/operation/`credEnvFamilies` mismatches before registry writes. 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 |
|
|
636
|
+
| `seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha\|branch> -e <env> [--target <id>] [--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`. Targets that require approval need `--approved-by` naming the authenticated caller. |
|
|
637
|
+
| `seq-studio pipeline adopt --stage <slug> --ref <sha\|branch> -e <env> [--target <id>] --native-id <id> --approved-by <you> [--resource-key <key>] [--kind job\|dlt_pipeline] [--old-source-removal-pr <url>] [--repo pipelines/<slug>]` | Bind a live Databricks job/pipeline into the stage without recreation (`bundle deployment bind` on Trigger). Always requires `--approved-by` naming the caller. When the key is still in the monorepo DAB, pass `--old-source-removal-pr` and follow the returned cutover checklist: unbind the old bundle state without deleting the remote, then remove its DAB declaration and add the target-specific adopted-resource entry in the same PR before redeploying. |
|
|
638
|
+
| `seq-studio pipeline unbind --stage <slug> --ref <sha\|branch> -e <env> [--target <id>] --approved-by <you> [--resource-key <key>] [--repo pipelines/<slug>]` | Release an adopted binding on Trigger; the remote object stays live (never deleted) |
|
|
639
|
+
| `seq-studio pipeline run-now --stage <slug> -e <env> [--target <id>] [--repo pipelines/<slug>] [--json]` | Run the stage's active job or DLT pipeline immediately and print its Databricks run URL |
|
|
640
|
+
| `seq-studio pipeline promote --stage <slug> --version <v> -e <env> [--target <id>] [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]` | Promote a validated version to another target. Targets that require approval need `--approved-by`; `--repo` disambiguates a slug that exists in multiple Pipelines |
|
|
641
|
+
| `seq-studio pipeline rollback --stage <slug> -e <env> [--target <id>] [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]` | Redeploy the previously retired deployment's version. Targets that require approval need `--approved-by`. |
|
|
642
|
+
|
|
643
|
+
`-e/--env` selects the Atlas connection. `--target` selects the logical
|
|
644
|
+
pipeline target advertised by that endpoint. It is optional when the alias
|
|
645
|
+
matches a target id or when the endpoint has exactly one target.
|
|
646
|
+
|
|
647
|
+
Unless `--no-wait` is set, `deploy`, `promote`, and `rollback` report status or
|
|
648
|
+
status-detail changes while waiting, then emit a 20-second progress heartbeat.
|
|
649
|
+
Terminal output includes the deployment ID and elapsed time; failures include
|
|
650
|
+
the status detail, and any available Trigger run ID is shown. `--json` output
|
|
651
|
+
is unchanged.
|
|
652
|
+
|
|
653
|
+
### Managed Pipelines asset identity
|
|
654
|
+
|
|
655
|
+
Compiled Databricks assets use stable, environment-qualified identities: display names are `{env}-MP-{type}-{stage}` (with `-trigger` for a scheduled DLT runner), DAB resource keys are `mp_{type}_{stage_snake_case}` (with `_trigger` for that runner), task keys are `{type}_{task}`, and bundles are `{env}-MP-pipelines-{domain}`. Compiled jobs and pipelines also carry `mp: "true"` and `mp_type: <type>` tags.
|
|
656
|
+
|
|
657
|
+
Changing a DAB resource key is a delete-and-create operation. Treat these identities as the existing-resource contract when planning, adopting, unbinding, or inspecting a Managed Pipeline; Unity Catalog catalogs, volumes, tables, and synced tables are not renamed by this convention.
|
|
658
|
+
|
|
659
|
+
The `{env}-MP-…` scheme itself is a one-shot break from the unprefixed names (`silverlake-core`, `pipelines-<domain>-<env>`). We do not migrate DAB bundle state or DLT resource identity: the first deploy after this change creates a new bundle and new jobs/pipelines, and can cascade-drop DLT-managed tables on the deleted resource. Operators should pause or delete leftover unprefixed jobs once the new ones are healthy. Each enabled OpCo deployment advertises and owns its own target profile.
|
|
557
660
|
|
|
558
661
|
`validate` is offline — no network or database. `--assets` supplies a
|
|
559
662
|
registry **asset export** (JSON) so inputs referencing other Pipelines'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level app monorepo commands:
|
|
3
|
+
* seq-studio init <dir> --with orm,function,artifact
|
|
4
|
+
* seq-studio add <kind> <name>
|
|
5
|
+
* seq-studio deploy -e <env>
|
|
6
|
+
*/
|
|
7
|
+
import type { ParsedArgs } from '../process/commands.js';
|
|
8
|
+
export declare const APP_INIT_USAGE: string;
|
|
9
|
+
export declare const APP_ADD_USAGE = "usage:\n seq-studio add <kind> <name>\n\n Scaffold another primitive into the current app monorepo (must contain\n sequence.app.yml) and append it to the manifest.\n\n Kinds: function | artifact\n (orm is only created at init today)\n\n Examples:\n seq-studio add function list-types\n seq-studio add artifact ui\n\n Run from the app root (directory with sequence.app.yml).\n";
|
|
10
|
+
export declare const APP_DEPLOY_USAGE = "usage:\n seq-studio deploy -e <env> [options]\n\n Deploy every primitive in sequence.app.yml in manifest deploy.order\n (default: orm \u2192 functions \u2192 artifact). Runs each kind's existing command:\n orm \u2192 seq-studio orm apply <path> -e <env>\n function \u2192 seq-studio functions deploy --dir <path> -e <env> [--yes]\n artifact \u2192 seq-studio artifact deploy <path> -e <env>\n\n Options:\n -e, --env <name> target environment (required)\n --dir <path> app root (default: cwd; must contain sequence.app.yml)\n --only <id1,id2> deploy only these primitive ids (order preserved)\n --yes non-interactive (forwarded to functions deploy)\n --dry-run print the plan without deploying\n\n Examples:\n seq-studio deploy -e staging --yes\n seq-studio deploy -e local --only get-pokemon --yes\n seq-studio deploy --dir ./pokedex -e staging --dry-run\n\n Run from the app root (directory with sequence.app.yml), or pass --dir.\n";
|
|
11
|
+
export declare function runAppInitCommand(args: ParsedArgs): Promise<number>;
|
|
12
|
+
export declare function runAppAddCommand({ kindArg, args, }: {
|
|
13
|
+
kindArg: string | undefined;
|
|
14
|
+
args: ParsedArgs;
|
|
15
|
+
}): Promise<number>;
|
|
16
|
+
export declare function runAppDeployCommand(args: ParsedArgs): Promise<number>;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level app monorepo commands:
|
|
3
|
+
* seq-studio init <dir> --with orm,function,artifact
|
|
4
|
+
* seq-studio add <kind> <name>
|
|
5
|
+
* seq-studio deploy -e <env>
|
|
6
|
+
*/
|
|
7
|
+
import { basename, resolve } from 'node:path';
|
|
8
|
+
import { REQUIRE_EXPLICIT_ENV_MESSAGE } from '../env-flags.js';
|
|
9
|
+
import { deployApp } from './deploy.js';
|
|
10
|
+
import { resolveRequestedKinds } from './kinds.js';
|
|
11
|
+
import { APP_MANIFEST_FILENAME, isPrimitiveKind, PRIMITIVE_KINDS, slugifyAppId, } from './manifest.js';
|
|
12
|
+
import { addPrimitiveToApp, defaultScaffoldNames, initAppMonorepo, printInitNextSteps, } from './scaffold.js';
|
|
13
|
+
const LOG = '[seq-studio]';
|
|
14
|
+
export const APP_INIT_USAGE = `usage:
|
|
15
|
+
seq-studio init <dir> --with <kinds> [options]
|
|
16
|
+
seq-studio init <dir> --orm --function --artifact [options]
|
|
17
|
+
|
|
18
|
+
Scaffold an app monorepo with ${APP_MANIFEST_FILENAME} and one subfolder per
|
|
19
|
+
selected primitive. At least one kind is required.
|
|
20
|
+
|
|
21
|
+
Kinds: ${PRIMITIVE_KINDS.join(', ')}
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--with <k1,k2,…> comma-separated kinds to scaffold
|
|
25
|
+
--orm --function … boolean aliases for --with
|
|
26
|
+
--function-name <n> name/slug for the first function (default: hello)
|
|
27
|
+
--description <text> optional app.description in the manifest
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
seq-studio init pokedex --with orm,function,artifact
|
|
31
|
+
seq-studio init pokedex --with orm,function,artifact --function-name get-pokemon
|
|
32
|
+
seq-studio init loan-tools --with function --function-name get-loan
|
|
33
|
+
`;
|
|
34
|
+
export const APP_ADD_USAGE = `usage:
|
|
35
|
+
seq-studio add <kind> <name>
|
|
36
|
+
|
|
37
|
+
Scaffold another primitive into the current app monorepo (must contain
|
|
38
|
+
${APP_MANIFEST_FILENAME}) and append it to the manifest.
|
|
39
|
+
|
|
40
|
+
Kinds: function | artifact
|
|
41
|
+
(orm is only created at init today)
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
seq-studio add function list-types
|
|
45
|
+
seq-studio add artifact ui
|
|
46
|
+
|
|
47
|
+
Run from the app root (directory with ${APP_MANIFEST_FILENAME}).
|
|
48
|
+
`;
|
|
49
|
+
export const APP_DEPLOY_USAGE = `usage:
|
|
50
|
+
seq-studio deploy -e <env> [options]
|
|
51
|
+
|
|
52
|
+
Deploy every primitive in ${APP_MANIFEST_FILENAME} in manifest deploy.order
|
|
53
|
+
(default: orm → functions → artifact). Runs each kind's existing command:
|
|
54
|
+
orm → seq-studio orm apply <path> -e <env>
|
|
55
|
+
function → seq-studio functions deploy --dir <path> -e <env> [--yes]
|
|
56
|
+
artifact → seq-studio artifact deploy <path> -e <env>
|
|
57
|
+
|
|
58
|
+
Options:
|
|
59
|
+
-e, --env <name> target environment (required)
|
|
60
|
+
--dir <path> app root (default: cwd; must contain ${APP_MANIFEST_FILENAME})
|
|
61
|
+
--only <id1,id2> deploy only these primitive ids (order preserved)
|
|
62
|
+
--yes non-interactive (forwarded to functions deploy)
|
|
63
|
+
--dry-run print the plan without deploying
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
seq-studio deploy -e staging --yes
|
|
67
|
+
seq-studio deploy -e local --only get-pokemon --yes
|
|
68
|
+
seq-studio deploy --dir ./pokedex -e staging --dry-run
|
|
69
|
+
|
|
70
|
+
Run from the app root (directory with ${APP_MANIFEST_FILENAME}), or pass --dir.
|
|
71
|
+
`;
|
|
72
|
+
function printError(error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
console.error(`${LOG} ${message}`);
|
|
75
|
+
}
|
|
76
|
+
export async function runAppInitCommand(args) {
|
|
77
|
+
if (args.flags.help === true || args.flags.h === true) {
|
|
78
|
+
console.log(APP_INIT_USAGE);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
const target = args.positional[0];
|
|
82
|
+
if (!target) {
|
|
83
|
+
console.error(APP_INIT_USAGE);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
let kinds;
|
|
87
|
+
try {
|
|
88
|
+
kinds = resolveRequestedKinds(args.flags);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
printError(error);
|
|
92
|
+
console.error(APP_INIT_USAGE);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
if (kinds.length === 0) {
|
|
96
|
+
console.error(`${LOG} select at least one primitive via --with orm,function,artifact`);
|
|
97
|
+
console.error(APP_INIT_USAGE);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
const appRoot = resolve(target);
|
|
101
|
+
const appId = slugifyAppId(basename(appRoot));
|
|
102
|
+
if (!appId) {
|
|
103
|
+
console.error(`${LOG} could not derive an app id from "${target}" — use a directory name with letters/numbers`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
const names = defaultScaffoldNames(appId);
|
|
107
|
+
if (typeof args.flags['function-name'] === 'string') {
|
|
108
|
+
names.functionName = slugifyAppId(args.flags['function-name']) || names.functionName;
|
|
109
|
+
}
|
|
110
|
+
else if (args.flags['function-name'] === true) {
|
|
111
|
+
console.error(`${LOG} --function-name requires a value`);
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
const description = typeof args.flags.description === 'string' ? args.flags.description : undefined;
|
|
115
|
+
if (args.flags.description === true) {
|
|
116
|
+
console.error(`${LOG} --description requires a value`);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const manifest = await initAppMonorepo({
|
|
121
|
+
rootDir: appRoot,
|
|
122
|
+
appId,
|
|
123
|
+
kinds,
|
|
124
|
+
names,
|
|
125
|
+
description,
|
|
126
|
+
});
|
|
127
|
+
printInitNextSteps({ appRoot, manifest });
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
printError(error);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export async function runAppAddCommand({ kindArg, args, }) {
|
|
136
|
+
if (!kindArg || kindArg === 'help' || kindArg === '--help' || kindArg === '-h') {
|
|
137
|
+
console.log(APP_ADD_USAGE);
|
|
138
|
+
return kindArg ? 0 : 1;
|
|
139
|
+
}
|
|
140
|
+
if (!isPrimitiveKind(kindArg)) {
|
|
141
|
+
console.error(`${LOG} unknown kind '${kindArg}' — expected one of: ${PRIMITIVE_KINDS.join(', ')}`);
|
|
142
|
+
console.error(APP_ADD_USAGE);
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
if (kindArg === 'orm') {
|
|
146
|
+
console.error(`${LOG} \`seq-studio add orm\` is not supported — orm is created at init`);
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
const nameRaw = args.positional[0];
|
|
150
|
+
if (!nameRaw) {
|
|
151
|
+
console.error(`${LOG} usage: seq-studio add ${kindArg} <name>`);
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
const name = slugifyAppId(nameRaw);
|
|
155
|
+
if (!name) {
|
|
156
|
+
console.error(`${LOG} invalid name "${nameRaw}" — use kebab-case`);
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const { entry } = await addPrimitiveToApp({
|
|
161
|
+
rootDir: process.cwd(),
|
|
162
|
+
kind: kindArg,
|
|
163
|
+
name,
|
|
164
|
+
});
|
|
165
|
+
console.log(`${LOG} added ${entry.kind} "${entry.id}" at ${entry.path}`);
|
|
166
|
+
console.log(`${LOG} updated ${APP_MANIFEST_FILENAME}`);
|
|
167
|
+
if (entry.kind === 'function') {
|
|
168
|
+
console.log(`${LOG} next: cd ${entry.path} && pnpm install && seq-studio functions deploy --dir ${entry.path} -e local`);
|
|
169
|
+
}
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
printError(error);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function flagString(flags, key) {
|
|
178
|
+
const value = flags[key];
|
|
179
|
+
return typeof value === 'string' ? value : undefined;
|
|
180
|
+
}
|
|
181
|
+
function flagBool(flags, key) {
|
|
182
|
+
return flags[key] === true;
|
|
183
|
+
}
|
|
184
|
+
export async function runAppDeployCommand(args) {
|
|
185
|
+
if (args.flags.help === true || args.flags.h === true) {
|
|
186
|
+
console.log(APP_DEPLOY_USAGE);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
const env = flagString(args.flags, 'env') ?? flagString(args.flags, 'e');
|
|
190
|
+
if (!env) {
|
|
191
|
+
console.error(`${LOG} ${REQUIRE_EXPLICIT_ENV_MESSAGE}`);
|
|
192
|
+
console.error(APP_DEPLOY_USAGE);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
const dirFlag = flagString(args.flags, 'dir');
|
|
196
|
+
const rootDir = resolve(dirFlag ?? process.cwd());
|
|
197
|
+
let only;
|
|
198
|
+
const onlyRaw = flagString(args.flags, 'only');
|
|
199
|
+
if (args.flags.only === true) {
|
|
200
|
+
console.error(`${LOG} --only requires a comma-separated list of primitive ids`);
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
if (onlyRaw) {
|
|
204
|
+
only = onlyRaw
|
|
205
|
+
.split(',')
|
|
206
|
+
.map((part) => part.trim())
|
|
207
|
+
.filter(Boolean);
|
|
208
|
+
if (only.length === 0) {
|
|
209
|
+
console.error(`${LOG} --only requires at least one primitive id`);
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const result = await deployApp({
|
|
215
|
+
rootDir,
|
|
216
|
+
env,
|
|
217
|
+
only,
|
|
218
|
+
yes: flagBool(args.flags, 'yes'),
|
|
219
|
+
dryRun: flagBool(args.flags, 'dry-run'),
|
|
220
|
+
});
|
|
221
|
+
return result.exitCode;
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
printError(error);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level `seq-studio deploy` — fan out from sequence.app.yml in deploy order.
|
|
3
|
+
*
|
|
4
|
+
* Child primitives keep their own deploy/apply verbs; this command only
|
|
5
|
+
* orchestrates them. Injectable runners keep unit tests offline.
|
|
6
|
+
*/
|
|
7
|
+
import { type AppManifest, type PrimitiveEntry, type PrimitiveKind } from './manifest.js';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a manifest primitive path under the app root. Rejects absolute paths,
|
|
10
|
+
* lexical traversal, and symlink escapes (via realpath) before a deployer runs.
|
|
11
|
+
* Missing targets keep the lexical candidate after a lexical containment check.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveContainedPrimitivePath({ rootDir, entryPath, }: {
|
|
14
|
+
rootDir: string;
|
|
15
|
+
entryPath: string;
|
|
16
|
+
}): Promise<string>;
|
|
17
|
+
export type DeployPrimitiveArgs = {
|
|
18
|
+
entry: PrimitiveEntry;
|
|
19
|
+
/** Absolute path to the primitive directory. */
|
|
20
|
+
path: string;
|
|
21
|
+
env: string;
|
|
22
|
+
yes: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type PrimitiveDeployers = {
|
|
25
|
+
[K in PrimitiveKind]: (args: DeployPrimitiveArgs) => Promise<number>;
|
|
26
|
+
};
|
|
27
|
+
export type DeployAppOptions = {
|
|
28
|
+
rootDir: string;
|
|
29
|
+
env: string;
|
|
30
|
+
/** Restrict to these primitive ids (manifest order still applies). */
|
|
31
|
+
only?: readonly string[];
|
|
32
|
+
yes?: boolean;
|
|
33
|
+
dryRun?: boolean;
|
|
34
|
+
deployers?: PrimitiveDeployers;
|
|
35
|
+
};
|
|
36
|
+
export type DeployAppResult = {
|
|
37
|
+
attempted: string[];
|
|
38
|
+
succeeded: string[];
|
|
39
|
+
failed: string[];
|
|
40
|
+
skipped: string[];
|
|
41
|
+
exitCode: number;
|
|
42
|
+
};
|
|
43
|
+
/** Resolve ordered primitive entries for deploy, validating order / --only ids. */
|
|
44
|
+
export declare function resolveDeployEntries({ manifest, only, }: {
|
|
45
|
+
manifest: AppManifest;
|
|
46
|
+
only?: readonly string[];
|
|
47
|
+
}): PrimitiveEntry[];
|
|
48
|
+
export declare function createDefaultDeployers(): Promise<PrimitiveDeployers>;
|
|
49
|
+
export declare function deployApp(options: DeployAppOptions): Promise<DeployAppResult>;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level `seq-studio deploy` — fan out from sequence.app.yml in deploy order.
|
|
3
|
+
*
|
|
4
|
+
* Child primitives keep their own deploy/apply verbs; this command only
|
|
5
|
+
* orchestrates them. Injectable runners keep unit tests offline.
|
|
6
|
+
*/
|
|
7
|
+
import { realpath } from 'node:fs/promises';
|
|
8
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
9
|
+
import { defaultDeployOrder, loadAppManifest, } from './manifest.js';
|
|
10
|
+
const LOG = '[seq-studio]';
|
|
11
|
+
const APP_MANIFEST_HINT = 'sequence.app.yml';
|
|
12
|
+
function assertRelativePrimitivePath(entryPath) {
|
|
13
|
+
const segments = entryPath.split('/');
|
|
14
|
+
if (entryPath.length === 0 ||
|
|
15
|
+
entryPath.trim() !== entryPath ||
|
|
16
|
+
isAbsolute(entryPath) ||
|
|
17
|
+
entryPath.includes('\\') ||
|
|
18
|
+
[...entryPath].some((character) => character.charCodeAt(0) < 0x20) ||
|
|
19
|
+
segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
|
|
20
|
+
throw new Error(`${APP_MANIFEST_HINT}: primitive path must be a relative directory within the app root (got ${JSON.stringify(entryPath)})`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function assertPathInsideRoot({ rootDir, candidate, entryPath, }) {
|
|
24
|
+
const relativePath = relative(rootDir, candidate);
|
|
25
|
+
if (relativePath === '..' ||
|
|
26
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
27
|
+
isAbsolute(relativePath)) {
|
|
28
|
+
throw new Error(`${APP_MANIFEST_HINT}: primitive path escapes the app root (got ${JSON.stringify(entryPath)})`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a manifest primitive path under the app root. Rejects absolute paths,
|
|
33
|
+
* lexical traversal, and symlink escapes (via realpath) before a deployer runs.
|
|
34
|
+
* Missing targets keep the lexical candidate after a lexical containment check.
|
|
35
|
+
*/
|
|
36
|
+
export async function resolveContainedPrimitivePath({ rootDir, entryPath, }) {
|
|
37
|
+
assertRelativePrimitivePath(entryPath);
|
|
38
|
+
const root = resolve(rootDir);
|
|
39
|
+
let canonicalRoot = root;
|
|
40
|
+
try {
|
|
41
|
+
canonicalRoot = await realpath(root);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code !== 'ENOENT')
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
// Resolve against the canonical root so macOS /var → /private/var does not
|
|
48
|
+
// look like an escape when comparing realpath(root) to a lexical candidate.
|
|
49
|
+
const candidate = resolve(canonicalRoot, entryPath);
|
|
50
|
+
assertPathInsideRoot({ rootDir: canonicalRoot, candidate, entryPath });
|
|
51
|
+
try {
|
|
52
|
+
const canonicalCandidate = await realpath(candidate);
|
|
53
|
+
assertPathInsideRoot({
|
|
54
|
+
rootDir: canonicalRoot,
|
|
55
|
+
candidate: canonicalCandidate,
|
|
56
|
+
entryPath,
|
|
57
|
+
});
|
|
58
|
+
return canonicalCandidate;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code !== 'ENOENT')
|
|
62
|
+
throw error;
|
|
63
|
+
// Target does not exist yet (dry-run / pre-scaffold) — lexical check stands.
|
|
64
|
+
return candidate;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Resolve ordered primitive entries for deploy, validating order / --only ids. */
|
|
68
|
+
export function resolveDeployEntries({ manifest, only, }) {
|
|
69
|
+
const byId = new Map(manifest.primitives.map((entry) => [entry.id, entry]));
|
|
70
|
+
const order = manifest.deploy.order && manifest.deploy.order.length > 0
|
|
71
|
+
? manifest.deploy.order
|
|
72
|
+
: defaultDeployOrder(manifest.primitives);
|
|
73
|
+
for (const id of order) {
|
|
74
|
+
if (!byId.has(id)) {
|
|
75
|
+
throw new Error(`${APP_MANIFEST_HINT}: deploy.order references unknown primitive id "${id}"`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Include primitives missing from an incomplete explicit order (append by default).
|
|
79
|
+
const orderedIds = [...order];
|
|
80
|
+
for (const entry of manifest.primitives) {
|
|
81
|
+
if (!orderedIds.includes(entry.id))
|
|
82
|
+
orderedIds.push(entry.id);
|
|
83
|
+
}
|
|
84
|
+
if (only && only.length > 0) {
|
|
85
|
+
const unknown = only.filter((id) => !byId.has(id));
|
|
86
|
+
if (unknown.length > 0) {
|
|
87
|
+
throw new Error(`Unknown primitive id(s) in --only: ${unknown.join(', ')}. Known: ${[...byId.keys()].join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
const onlySet = new Set(only);
|
|
90
|
+
return orderedIds
|
|
91
|
+
.filter((id) => onlySet.has(id))
|
|
92
|
+
.map((id) => byId.get(id));
|
|
93
|
+
}
|
|
94
|
+
return orderedIds.map((id) => byId.get(id));
|
|
95
|
+
}
|
|
96
|
+
export async function createDefaultDeployers() {
|
|
97
|
+
const { runOrmCommand } = await import('../orm/delegate.js');
|
|
98
|
+
const { functionsDeployCommand } = await import('../functions/commands.js');
|
|
99
|
+
const { runArtifactCommand } = await import('../artifact/delegate.js');
|
|
100
|
+
return {
|
|
101
|
+
async orm({ path, env }) {
|
|
102
|
+
return runOrmCommand('apply', [path, '--env', env]);
|
|
103
|
+
},
|
|
104
|
+
async function({ path, env, yes }) {
|
|
105
|
+
return functionsDeployCommand({
|
|
106
|
+
positional: [],
|
|
107
|
+
flags: {
|
|
108
|
+
dir: path,
|
|
109
|
+
env,
|
|
110
|
+
...(yes ? { yes: true } : {}),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
async artifact({ path, env }) {
|
|
115
|
+
return runArtifactCommand('deploy', [path, '--env', env]);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export async function deployApp(options) {
|
|
120
|
+
const rootDir = resolve(options.rootDir);
|
|
121
|
+
const manifest = await loadAppManifest(rootDir);
|
|
122
|
+
const entries = resolveDeployEntries({ manifest, only: options.only });
|
|
123
|
+
const onError = manifest.deploy.on_error;
|
|
124
|
+
const deployers = options.deployers ?? (await createDefaultDeployers());
|
|
125
|
+
// Validate every selected path before any deployer runs (including dry-run).
|
|
126
|
+
const resolvedPaths = new Map();
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
resolvedPaths.set(entry.id, await resolveContainedPrimitivePath({ rootDir, entryPath: entry.path }));
|
|
129
|
+
}
|
|
130
|
+
console.log(`${LOG} deploying app "${manifest.app.id}" (${entries.length} primitive(s)) → ${options.env}`);
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
console.log(`${LOG} - ${entry.kind.padEnd(9)} ${entry.id} (${entry.path})`);
|
|
133
|
+
}
|
|
134
|
+
if (options.dryRun) {
|
|
135
|
+
console.log(`${LOG} dry-run: no primitives deployed`);
|
|
136
|
+
return {
|
|
137
|
+
attempted: [],
|
|
138
|
+
succeeded: [],
|
|
139
|
+
failed: [],
|
|
140
|
+
skipped: entries.map((entry) => entry.id),
|
|
141
|
+
exitCode: 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const attempted = [];
|
|
145
|
+
const succeeded = [];
|
|
146
|
+
const failed = [];
|
|
147
|
+
const skipped = [];
|
|
148
|
+
/** Failed or dependency-skipped ids — blocks transitive `depends_on` edges. */
|
|
149
|
+
const unavailable = new Set();
|
|
150
|
+
let stop = false;
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (stop) {
|
|
153
|
+
skipped.push(entry.id);
|
|
154
|
+
unavailable.add(entry.id);
|
|
155
|
+
console.log(`${LOG} skip ${entry.id} (stopped after earlier failure)`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const blockedBy = entry.depends_on.filter((id) => unavailable.has(id));
|
|
159
|
+
if (blockedBy.length > 0) {
|
|
160
|
+
skipped.push(entry.id);
|
|
161
|
+
unavailable.add(entry.id);
|
|
162
|
+
console.log(`${LOG} skip ${entry.id} (depends on failed/skipped: ${blockedBy.join(', ')})`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const path = resolvedPaths.get(entry.id);
|
|
166
|
+
attempted.push(entry.id);
|
|
167
|
+
console.log(`${LOG} → ${entry.kind} ${entry.id}`);
|
|
168
|
+
let code;
|
|
169
|
+
try {
|
|
170
|
+
code = await deployers[entry.kind]({
|
|
171
|
+
entry,
|
|
172
|
+
path,
|
|
173
|
+
env: options.env,
|
|
174
|
+
yes: options.yes === true,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
179
|
+
console.error(`${LOG} ${entry.id} threw: ${message}`);
|
|
180
|
+
code = 1;
|
|
181
|
+
}
|
|
182
|
+
if (code === 0) {
|
|
183
|
+
succeeded.push(entry.id);
|
|
184
|
+
console.log(`${LOG} ✓ ${entry.id}`);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
failed.push(entry.id);
|
|
188
|
+
unavailable.add(entry.id);
|
|
189
|
+
console.error(`${LOG} ✗ ${entry.id} exited ${code}`);
|
|
190
|
+
if (onError === 'stop')
|
|
191
|
+
stop = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const exitCode = failed.length > 0 ? 1 : 0;
|
|
195
|
+
console.log(`${LOG} deploy finished: ${succeeded.length} ok, ${failed.length} failed, ${skipped.length} skipped`);
|
|
196
|
+
return { attempted, succeeded, failed, skipped, exitCode };
|
|
197
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type PrimitiveKind } from './manifest.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve which primitive kinds to scaffold from CLI flags.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* --with orm,function,artifact
|
|
7
|
+
* --with=orm,function
|
|
8
|
+
* --orm --function --artifact (boolean aliases)
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveRequestedKinds(flags: Record<string, string | true>): PrimitiveKind[];
|