@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 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. Runs from any repo against the platform over
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&#124;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`. Production/banksouth require a pinned 40-hex SHA (client + server) |
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 environment. Prod/banksouth require `--approved-by` naming the authenticated caller (approvals are self-recorded); `--repo` disambiguates a slug that exists in multiple Pipelines |
556
- | `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 |
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[];