@sequenceholdings/studio-cli 0.1.12 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +116 -30
  2. package/dist/artifact/delegate.d.ts +2 -2
  3. package/dist/artifact/delegate.js +31 -73
  4. package/dist/atlas-client.js +52 -37
  5. package/dist/auth-cmds/commands.d.ts +1 -1
  6. package/dist/auth-cmds/commands.js +12 -7
  7. package/dist/auth.d.ts +97 -19
  8. package/dist/auth.js +376 -81
  9. package/dist/config.d.ts +13 -3
  10. package/dist/config.js +41 -14
  11. package/dist/env-catalog.js +13 -3
  12. package/dist/env-flags.d.ts +2 -0
  13. package/dist/env-flags.js +2 -0
  14. package/dist/env-registry.d.ts +27 -0
  15. package/dist/env-registry.js +204 -0
  16. package/dist/envs/commands.d.ts +1 -1
  17. package/dist/envs/commands.js +65 -10
  18. package/dist/file-lock.d.ts +5 -0
  19. package/dist/file-lock.js +187 -0
  20. package/dist/functions/commands.d.ts +9 -1
  21. package/dist/functions/commands.js +71 -29
  22. package/dist/functions/manifest.d.ts +1 -0
  23. package/dist/functions/manifest.js +36 -0
  24. package/dist/login.d.ts +14 -3
  25. package/dist/login.js +60 -40
  26. package/dist/main.d.ts +2 -1
  27. package/dist/main.js +36 -12
  28. package/dist/orm/delegate.d.ts +9 -0
  29. package/dist/orm/delegate.js +36 -4
  30. package/dist/pat-hints.js +2 -2
  31. package/dist/pipeline/commands.d.ts +58 -0
  32. package/dist/pipeline/commands.js +330 -0
  33. package/dist/pipeline/lifecycle.d.ts +58 -0
  34. package/dist/pipeline/lifecycle.js +348 -0
  35. package/dist/pipeline/pinning.d.ts +5 -0
  36. package/dist/pipeline/pinning.js +9 -0
  37. package/dist/pipeline/templates.d.ts +11 -0
  38. package/dist/pipeline/templates.js +166 -0
  39. package/dist/process/build.d.ts +12 -1
  40. package/dist/process/build.js +49 -2
  41. package/dist/process/codegen.js +21 -1
  42. package/dist/process/commands.d.ts +19 -0
  43. package/dist/process/commands.js +153 -40
  44. package/dist/process/compiler-subprocess.d.ts +29 -0
  45. package/dist/process/compiler-subprocess.js +99 -0
  46. package/dist/process/compiler-worker.d.ts +1 -0
  47. package/dist/process/compiler-worker.js +38 -0
  48. package/dist/process/discover.d.ts +4 -1
  49. package/dist/process/discover.js +5 -2
  50. package/dist/process/lint.d.ts +8 -0
  51. package/dist/process/lint.js +125 -29
  52. package/dist/process/repo-install.d.ts +21 -0
  53. package/dist/process/repo-install.js +99 -0
  54. package/dist/process/simulate.js +14 -1
  55. package/dist/repos/commands.d.ts +1 -1
  56. package/dist/repos/commands.js +17 -12
  57. package/dist/secrets/commands.d.ts +1 -1
  58. package/dist/secrets/commands.js +18 -18
  59. package/package.json +9 -4
@@ -10,6 +10,7 @@
10
10
  * but on_timeout_edge_id is unset, warn
11
11
  */
12
12
  import { readFileSync } from 'node:fs';
13
+ import { forEachSerializedNode, } from '@sequenceholdings/lattice/bundle';
13
14
  import { parallelSubNodes, validateEmailReminders } from '@sequenceholdings/lattice/define';
14
15
  export async function lintProcesses(input) {
15
16
  const errors = [];
@@ -67,7 +68,7 @@ export async function lintProcesses(input) {
67
68
  }
68
69
  }
69
70
  for (const node of p.nodes) {
70
- await lintNodeContract(p.id, node, errors, warnings, input.loadAgentSchemaEdgeIdEnum);
71
+ await lintNodeContract(p.id, node, errors, warnings, input.loadAgentSchemaEdgeIdEnum, input.suppressUnavailableAgentWarnings ?? false);
71
72
  }
72
73
  // Closure-scope check: mappers / fn / join / fan_out / output are
73
74
  // serialized via `.toString()` and run in a vm with NOTHING from the
@@ -213,10 +214,10 @@ function lintSandboxClosures(processId, node, importedNames, warnings) {
213
214
  * edge_id-contract checks as a top-level node) and lints the `join` reducer's
214
215
  * own edge_id returns against the parallel node's outgoing_edges.
215
216
  */
216
- async function lintNodeContract(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum) {
217
+ async function lintNodeContract(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum, suppressUnavailableAgentWarnings) {
217
218
  switch (node.kind) {
218
219
  case 'agent':
219
- await lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum);
220
+ await lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum, suppressUnavailableAgentWarnings);
220
221
  break;
221
222
  case 'human':
222
223
  lintHuman(processId, node, errors, warnings);
@@ -225,7 +226,7 @@ async function lintNodeContract(processId, node, errors, warnings, loadAgentSche
225
226
  const parallel = node;
226
227
  lintParallelJoin(processId, parallel, errors, warnings);
227
228
  for (const { node: sub } of parallelSubNodes(parallel)) {
228
- await lintNodeContract(processId, sub, errors, warnings, loadAgentSchemaEdgeIdEnum);
229
+ await lintNodeContract(processId, sub, errors, warnings, loadAgentSchemaEdgeIdEnum, suppressUnavailableAgentWarnings);
229
230
  }
230
231
  break;
231
232
  }
@@ -335,28 +336,74 @@ function lintSubprocessOutput(processId, node, errors, warnings) {
335
336
  }
336
337
  }
337
338
  const EDGE_ID_LITERAL_RE = /\bedge_id\s*:\s*['"`]([^'"`]+)['"`]/g;
338
- async function lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum) {
339
- const allEdgeIds = node.outgoing_edges.map((e) => e.id);
340
- // The reset edge (taken out-of-band by the reset intervention, never chosen
341
- // by the agent) must exist on the node but is excluded from the agent's
342
- // edge_id contract same way a human node's on_timeout_edge_id is an
343
- // orchestration-only edge. Validate membership, then verify the agent's
344
- // edge_id enum against the REAL (non-reset) edges only.
345
- if (node.on_reset_edge_id) {
346
- if (!allEdgeIds.includes(node.on_reset_edge_id)) {
347
- errors.push({
348
- process_id: processId,
349
- node_id: node.id,
350
- message: `on_reset_edge_id "${node.on_reset_edge_id}" is not in outgoing_edges`,
351
- });
352
- }
339
+ export async function lintSerializedAgentContracts({ bundle, loadAgentSchemaEdgeIdEnum, includeStaticValidation = true, }) {
340
+ const errors = [];
341
+ const warnings = [];
342
+ const pending = [];
343
+ for (const process of bundle.processes) {
344
+ forEachSerializedNode(process.nodes, (node) => {
345
+ const contract = serializedAgentContract(node);
346
+ if (!contract)
347
+ return;
348
+ pending.push(lintAgentContract({
349
+ processId: process.id,
350
+ nodeId: node.id,
351
+ ...contract,
352
+ errors,
353
+ warnings,
354
+ loadAgentSchemaEdgeIdEnum,
355
+ suppressUnavailableAgentWarnings: false,
356
+ includeStaticValidation,
357
+ }));
358
+ });
353
359
  }
354
- const outgoingIds = allEdgeIds.filter((id) => id !== node.on_reset_edge_id);
360
+ await Promise.all(pending);
361
+ return { errors, warnings };
362
+ }
363
+ function serializedAgentContract(node) {
364
+ if (node.kind !== 'agent' || !isUnknownRecord(node.metadata))
365
+ return null;
366
+ const agentId = node.metadata.agent_id;
367
+ if (typeof agentId !== 'string')
368
+ return null;
369
+ const onResetEdgeId = node.metadata.on_reset_edge_id;
370
+ return {
371
+ agentId,
372
+ allEdgeIds: node.outgoing_edges.map((edge) => edge.id),
373
+ ...(typeof onResetEdgeId === 'string' ? { onResetEdgeId } : {}),
374
+ };
375
+ }
376
+ function isUnknownRecord(value) {
377
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
378
+ }
379
+ async function lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum, suppressUnavailableAgentWarnings) {
380
+ await lintAgentContract({
381
+ processId,
382
+ nodeId: node.id,
383
+ agentId: node.agent.id,
384
+ allEdgeIds: node.outgoing_edges.map((edge) => edge.id),
385
+ onResetEdgeId: node.on_reset_edge_id,
386
+ errors,
387
+ warnings,
388
+ loadAgentSchemaEdgeIdEnum,
389
+ suppressUnavailableAgentWarnings,
390
+ includeStaticValidation: true,
391
+ });
392
+ }
393
+ async function lintAgentContract({ processId, nodeId, agentId, allEdgeIds, onResetEdgeId, errors, warnings, loadAgentSchemaEdgeIdEnum, suppressUnavailableAgentWarnings, includeStaticValidation, }) {
394
+ if (includeStaticValidation && onResetEdgeId && !allEdgeIds.includes(onResetEdgeId)) {
395
+ errors.push({
396
+ process_id: processId,
397
+ node_id: nodeId,
398
+ message: `on_reset_edge_id "${onResetEdgeId}" is not in outgoing_edges`,
399
+ });
400
+ }
401
+ const outgoingIds = allEdgeIds.filter((id) => id !== onResetEdgeId);
355
402
  if (!loadAgentSchemaEdgeIdEnum) {
356
- if (outgoingIds.length > 1) {
403
+ if (outgoingIds.length > 1 && !suppressUnavailableAgentWarnings) {
357
404
  warnings.push({
358
405
  process_id: processId,
359
- node_id: node.id,
406
+ node_id: nodeId,
360
407
  message: `agent node has ${outgoingIds.length} outgoing edges; agent output schema vs outgoing_edges contract not verified (no agent registry loader available)`,
361
408
  });
362
409
  }
@@ -364,21 +411,21 @@ async function lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeI
364
411
  }
365
412
  let enumValues;
366
413
  try {
367
- enumValues = await loadAgentSchemaEdgeIdEnum(node.agent.id);
414
+ enumValues = await loadAgentSchemaEdgeIdEnum(agentId);
368
415
  }
369
416
  catch (err) {
370
417
  warnings.push({
371
418
  process_id: processId,
372
- node_id: node.id,
373
- message: `could not load agent definition "${node.agent.id}" to verify output contract: ${err instanceof Error ? err.message : String(err)} (run with DB access to enable full check)`,
419
+ node_id: nodeId,
420
+ message: `could not load agent definition "${agentId}" to verify output contract: ${err instanceof Error ? err.message : String(err)} (run with DB access to enable full check)`,
374
421
  });
375
422
  return;
376
423
  }
377
424
  if (enumValues === 'not-found') {
378
425
  errors.push({
379
426
  process_id: processId,
380
- node_id: node.id,
381
- message: `agent "${node.agent.id}" not found in the target env — sync/seed the agent registry before applying`,
427
+ node_id: nodeId,
428
+ message: `agent "${agentId}" not found in the target env — sync/seed the agent registry before applying`,
382
429
  });
383
430
  return;
384
431
  }
@@ -397,22 +444,71 @@ async function lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeI
397
444
  if (missing.length > 0 || extra.length > 0) {
398
445
  errors.push({
399
446
  process_id: processId,
400
- node_id: node.id,
401
- message: `agent "${node.agent.id}" edge_id enum mismatch — missing: [${missing.join(', ')}], extra: [${extra.join(', ')}]; expected exactly: [${outgoingIds.join(', ')}]`,
447
+ node_id: nodeId,
448
+ message: `agent "${agentId}" edge_id enum mismatch — missing: [${missing.join(', ')}], extra: [${extra.join(', ')}]; expected exactly: [${outgoingIds.join(', ')}]`,
402
449
  });
403
450
  }
404
451
  }
452
+ /**
453
+ * Matches an undecided-sentinel return in a serialized join source: the
454
+ * literal `{ undecided: true }` (the correct authored form — reducers run in
455
+ * a vm with no module imports in scope) or a `JOIN_UNDECIDED` reference
456
+ * (recognized so the eager no-sentinel warning doesn't double-fire on top of
457
+ * the closure-scope warning that already flags the out-of-scope import).
458
+ */
459
+ const UNDECIDED_SENTINEL_RE = /\bundecided\s*:\s*true\b|\bJOIN_UNDECIDED\b/;
405
460
  /**
406
461
  * Lint a parallel node's `join` reducer the way we lint automation fns:
407
462
  * scan the serialized source for literal `edge_id: '…'` returns and confirm
408
463
  * each is in the parallel node's own outgoing_edges. The reducer chooses the
409
464
  * parallel node's exit edge, so a typo there is a routing bug the orchestrator
410
465
  * would only surface at run time.
466
+ *
467
+ * join_mode checks: a BARRIER join returning the `{undecided: true}` sentinel
468
+ * is an authoring error (the runner fails the block at runtime), and an EAGER
469
+ * join that never returns the sentinel decides on the first settlement —
470
+ * legal but suspicious, so it warns. No static engine check is possible for
471
+ * eager blocks: the execution engine is chosen per RUN (`{"engine": ...}` +
472
+ * environment rollout policy), not per process — the Trigger-only limitation
473
+ * is enforced at runtime (an eager block on a Temporal run fails with a clear
474
+ * unsupported error at branch resolution).
411
475
  */
412
476
  function lintParallelJoin(processId, node, errors, warnings) {
413
477
  const join = node.join;
414
478
  const source = typeof join === 'function' ? join.toString() : '';
415
479
  const outgoingIds = new Set(node.outgoing_edges.map((e) => e.id));
480
+ const joinMode = node.join_mode ?? 'barrier';
481
+ const returnsUndecided = UNDECIDED_SENTINEL_RE.test(source);
482
+ if (joinMode === 'eager') {
483
+ if (typeof join !== 'function') {
484
+ // defineParallelNode already rejects this; belt-and-suspenders for
485
+ // hand-built definitions that bypassed the SDK constructor.
486
+ errors.push({
487
+ process_id: processId,
488
+ node_id: node.id,
489
+ message: `join_mode 'eager' requires an explicit join reducer`,
490
+ });
491
+ }
492
+ if (!returnsUndecided) {
493
+ warnings.push({
494
+ process_id: processId,
495
+ node_id: node.id,
496
+ message: `join_mode is 'eager' but the join source never returns the ` +
497
+ `{undecided: true} sentinel — the block will decide on the FIRST ` +
498
+ `branch settlement; confirm that is intended`,
499
+ });
500
+ }
501
+ }
502
+ else if (returnsUndecided) {
503
+ errors.push({
504
+ process_id: processId,
505
+ node_id: node.id,
506
+ message: `parallel join returns the {undecided: true} sentinel but join_mode ` +
507
+ `is 'barrier' (the default) — the sentinel is only meaningful with ` +
508
+ `join_mode: 'eager'; at runtime the block FAILS when a barrier join ` +
509
+ `returns it`,
510
+ });
511
+ }
416
512
  const literals = new Set();
417
513
  for (const match of source.matchAll(EDGE_ID_LITERAL_RE)) {
418
514
  if (match[1])
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Remote-install helper for `process apply --repo`. Installs dependencies
3
+ * in a materialized process repo so `tsx` can import `process.ts` modules.
4
+ *
5
+ * Re-uses the artifact-studio trusted-install pipeline:
6
+ * 1. Require `package.json` and `pnpm-lock.yaml` (no floating installs)
7
+ * 2. Strip install-control files (.npmrc, pnpm-workspace.yaml, pnpm hooks)
8
+ * 3. Full trust validation: registry-only specs, no patches, no symlinks,
9
+ * Chainguard lockfile with repo/commit/directory/tarball checks
10
+ * 4. Run pinned pnpm with frozen lockfile, ignore-scripts, prod-only
11
+ */
12
+ /**
13
+ * Install dependencies in a materialized remote process repo. Process repos
14
+ * deployed from git-service must carry a `package.json` (process.ts files
15
+ * import `@sequenceholdings/lattice/define` and the module won't resolve
16
+ * without node_modules) and a Chainguard-resolved `pnpm-lock.yaml`.
17
+ *
18
+ * Throws when either file is missing or when the tree fails the full
19
+ * trusted-install validation.
20
+ */
21
+ export declare function prepareProcessBuildRoot(dir: string): Promise<void>;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Remote-install helper for `process apply --repo`. Installs dependencies
3
+ * in a materialized process repo so `tsx` can import `process.ts` modules.
4
+ *
5
+ * Re-uses the artifact-studio trusted-install pipeline:
6
+ * 1. Require `package.json` and `pnpm-lock.yaml` (no floating installs)
7
+ * 2. Strip install-control files (.npmrc, pnpm-workspace.yaml, pnpm hooks)
8
+ * 3. Full trust validation: registry-only specs, no patches, no symlinks,
9
+ * Chainguard lockfile with repo/commit/directory/tarball checks
10
+ * 4. Run pinned pnpm with frozen lockfile, ignore-scripts, prod-only
11
+ */
12
+ import { execFileSync } from 'node:child_process';
13
+ import { existsSync } from 'node:fs';
14
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
15
+ import { homedir, tmpdir } from 'node:os';
16
+ import { join } from 'node:path';
17
+ import { createScrubbedChildEnv } from '@sequenceholdings/artifact-studio/child-environment';
18
+ import { ARTIFACT_BUILD_PNPM, buildControlledArtifactNpmrc, } from '@sequenceholdings/artifact-studio/prepare-build';
19
+ import { stripInstallControlFiles } from '@sequenceholdings/artifact-studio/sanitize-remote-tree';
20
+ import { ArtifactInstallTrustError, assertTrustedArtifactInstallTree, } from '@sequenceholdings/artifact-studio/trusted-install';
21
+ class ProcessInstallError extends Error {
22
+ name = 'ProcessInstallError';
23
+ }
24
+ /**
25
+ * Install dependencies in a materialized remote process repo. Process repos
26
+ * deployed from git-service must carry a `package.json` (process.ts files
27
+ * import `@sequenceholdings/lattice/define` and the module won't resolve
28
+ * without node_modules) and a Chainguard-resolved `pnpm-lock.yaml`.
29
+ *
30
+ * Throws when either file is missing or when the tree fails the full
31
+ * trusted-install validation.
32
+ */
33
+ export async function prepareProcessBuildRoot(dir) {
34
+ if (!existsSync(join(dir, 'package.json'))) {
35
+ throw new ProcessInstallError('Remote process repos must include a package.json with @sequenceholdings/lattice as a dependency.');
36
+ }
37
+ if (!existsSync(join(dir, 'pnpm-lock.yaml'))) {
38
+ throw new ProcessInstallError('Remote process deploy requires pnpm-lock.yaml — run `pnpm install` locally and commit the lockfile.');
39
+ }
40
+ const stripped = stripInstallControlFiles(dir);
41
+ if (stripped.length > 0) {
42
+ console.warn(`[seq-studio] stripped install-control files from remote tree: ${stripped.join(', ')}`);
43
+ }
44
+ try {
45
+ assertTrustedArtifactInstallTree(dir);
46
+ }
47
+ catch (error) {
48
+ if (error instanceof ArtifactInstallTrustError) {
49
+ throw new ProcessInstallError(`Remote process repo is not trusted for install: ${error.message}`);
50
+ }
51
+ throw error;
52
+ }
53
+ await rm(join(dir, 'node_modules'), { recursive: true, force: true });
54
+ const configDir = await mkdtemp(join(tmpdir(), 'process-pnpm-'));
55
+ const controlledNpmrc = join(configDir, '.npmrc');
56
+ const homeDir = join(configDir, 'home');
57
+ // Keep package-manager writes scoped to this install. A persistent cache
58
+ // would be writable by the untrusted source install and could poison later
59
+ // process builds that run under the same uid.
60
+ const npmCache = join(configDir, 'npm-cache');
61
+ const pnpmStore = join(configDir, 'pnpm-store');
62
+ const userNpmrcPath = process.env.npm_config_userconfig ?? join(homedir(), '.npmrc');
63
+ console.log(`[seq-studio] installing process dependencies (frozen lockfile, prod only) via ${ARTIFACT_BUILD_PNPM}…`);
64
+ try {
65
+ await mkdir(homeDir, { recursive: true });
66
+ await writeFile(controlledNpmrc, buildControlledArtifactNpmrc(userNpmrcPath), { mode: 0o600 });
67
+ const spawnOpts = {
68
+ cwd: configDir,
69
+ stdio: 'inherit',
70
+ env: createScrubbedChildEnv({
71
+ homeDir,
72
+ additionalEnv: {
73
+ npm_config_userconfig: controlledNpmrc,
74
+ npm_config_cache: npmCache,
75
+ },
76
+ }),
77
+ };
78
+ execFileSync('npx', [
79
+ '--yes',
80
+ ARTIFACT_BUILD_PNPM,
81
+ '--dir',
82
+ dir,
83
+ 'install',
84
+ '--frozen-lockfile',
85
+ '--ignore-scripts',
86
+ '--prod',
87
+ '--store-dir',
88
+ pnpmStore,
89
+ ], spawnOpts);
90
+ }
91
+ catch (error) {
92
+ const detail = error instanceof Error ? error.message : String(error);
93
+ throw new ProcessInstallError(`process dependency install failed (${ARTIFACT_BUILD_PNPM}). ` +
94
+ `Run \`pnpm install\` locally and commit pnpm-lock.yaml. (${detail})`);
95
+ }
96
+ finally {
97
+ await rm(configDir, { recursive: true, force: true });
98
+ }
99
+ }
@@ -10,7 +10,7 @@
10
10
  * - Stubs automation runner identically — native handlers live in the
11
11
  * Atlas image and can't execute here
12
12
  */
13
- import { DEFAULT_MAX_FANOUT_WIDTH, } from '@sequenceholdings/lattice/define';
13
+ import { DEFAULT_MAX_FANOUT_WIDTH, isJoinUndecided, } from '@sequenceholdings/lattice/define';
14
14
  import { extractOutputState, mergeRunState } from '@sequenceholdings/lattice';
15
15
  const DEFAULT_MAX_ITERATIONS = 1000;
16
16
  const DEFAULT_MAX_REVISITS = 100;
@@ -246,7 +246,20 @@ async function simulateParallel(args, node) {
246
246
  item: br.item,
247
247
  });
248
248
  }
249
+ // The simulator settles every branch before joining, so an EAGER join sees
250
+ // the all-settled case (its incremental undecided passes aren't modelled —
251
+ // simulate verifies topology/routing, not settlement timing). In that case
252
+ // the eager contract requires a decision; surface a sentinel return as the
253
+ // same clear error the runtime raises instead of a confusing edge failure.
249
254
  const joinOutput = node.join(results, args.ctx);
255
+ if (isJoinUndecided(joinOutput)) {
256
+ throw new Error(node.join_mode === 'eager'
257
+ ? `parallel node "${node.id}": eager join returned {undecided: true} with ` +
258
+ `every branch settled — an eager join must decide once nothing is pending`
259
+ : `parallel node "${node.id}": join returned the {undecided: true} sentinel ` +
260
+ `but join_mode is 'barrier' — the sentinel is only meaningful with ` +
261
+ `join_mode: 'eager'`);
262
+ }
250
263
  return foldBranchState(joinOutput, results);
251
264
  }
252
265
  /**
@@ -45,5 +45,5 @@ export declare function reposCloneCommand(args: ParsedArgs, deps?: {
45
45
  */
46
46
  export declare function normalizeCloneUrl(raw: string, env: ResolvedEnv): string;
47
47
  export declare function reposDeleteCommand(args: ParsedArgs): Promise<number>;
48
- export declare const REPOS_USAGE = "usage:\n seq-studio repos list [-e env] [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] [-e env] list or create namespaces\n seq-studio repos show <ns>/<name> [-e env] repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> [-e env] [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> [-e env] [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> [-e env] [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> [-e env] [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <env> (see: seq-studio envs list)\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
48
+ export declare const REPOS_USAGE = "usage:\n seq-studio repos list -e <env> [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] -e <env> list or create namespaces\n seq-studio repos show <ns>/<name> -e <env> repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> -e <env> [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> -e <env> [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> -e <env> [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
49
49
  export declare function runReposCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
@@ -25,6 +25,7 @@ import { resolveGitPatFromEnv, runGitClone, } from './git-clone.js';
25
25
  import { formatPatSetupHint } from '../pat-hints.js';
26
26
  import { tryGetAccessToken } from '../auth.js';
27
27
  import { resolveEnvWithDiscovery } from '../config.js';
28
+ import { REQUIRE_EXPLICIT_ENV_MESSAGE } from '../env-flags.js';
28
29
  const PAGE_SIZE = 200;
29
30
  // ---------------------------------------------------------------------------
30
31
  // Helpers
@@ -158,7 +159,7 @@ export async function reposNamespacesCommand(args) {
158
159
  const ctx = await reposContext(args);
159
160
  if (action === 'create') {
160
161
  if (!slug) {
161
- console.error('usage: seq-studio repos namespaces create <slug> [-e env]');
162
+ console.error('usage: seq-studio repos namespaces create <slug> -e <env>');
162
163
  return 1;
163
164
  }
164
165
  const created = await postJson({
@@ -358,7 +359,7 @@ export async function reposCloneCommand(args, deps = {}) {
358
359
  // <ns>/<name> path — needs Auth0 for resolveRepo (JSON API) whether or not
359
360
  // we then use smart-HTTP. Without Auth0, point users at --url from the UI.
360
361
  if (!positional) {
361
- console.error('usage: seq-studio repos clone <ns>/<name> | --url <clone-url> | --id <uuid> [-e env] [--ref r] [--out dir]');
362
+ console.error('usage: seq-studio repos clone <ns>/<name> | --url <clone-url> | --id <uuid> -e <env> [--ref r] [--out dir]');
362
363
  return 1;
363
364
  }
364
365
  const { namespace, name } = parseRepoPath(positional);
@@ -418,6 +419,10 @@ async function resolveEnvOnly(args) {
418
419
  }
419
420
  const requested = (typeof args.flags.env === 'string' ? args.flags.env : undefined) ??
420
421
  (typeof args.flags.e === 'string' ? args.flags.e : undefined);
422
+ // Same contract as buildContext: never silently fall back to `local`.
423
+ if (!requested) {
424
+ throw new Error(REQUIRE_EXPLICIT_ENV_MESSAGE);
425
+ }
421
426
  return resolveEnvWithDiscovery({ requested });
422
427
  }
423
428
  /**
@@ -488,20 +493,20 @@ export async function reposDeleteCommand(args) {
488
493
  // Router
489
494
  // ---------------------------------------------------------------------------
490
495
  export const REPOS_USAGE = `usage:
491
- seq-studio repos list [-e env] [--namespace <slug>] [--mine] repos visible on the environment
492
- seq-studio repos namespaces [create <slug>] [-e env] list or create namespaces
493
- seq-studio repos show <ns>/<name> [-e env] repo detail (branches, clone URL)
494
- seq-studio repos create <ns>/<name> [-e env] [--default-branch b] create an empty repo
495
- seq-studio repos clone <ns>/<name> [-e env] [--ref r] [--out dir] [--force]
496
- seq-studio repos clone --url <https://…/repos/<id>/git> [-e env] [--ref r] [--out dir]
497
- seq-studio repos clone --id <uuid> [-e env] [--ref r] [--out dir]
496
+ seq-studio repos list -e <env> [--namespace <slug>] [--mine] repos visible on the environment
497
+ seq-studio repos namespaces [create <slug>] -e <env> list or create namespaces
498
+ seq-studio repos show <ns>/<name> -e <env> repo detail (branches, clone URL)
499
+ seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create an empty repo
500
+ seq-studio repos clone <ns>/<name> -e <env> [--ref r] [--out dir] [--force]
501
+ seq-studio repos clone --url <https://…/repos/<id>/git> -e <env> [--ref r] [--out dir]
502
+ seq-studio repos clone --id <uuid> -e <env> [--ref r] [--out dir]
498
503
  smart-HTTP when ATLAS_GIT_PAT is set;
499
504
  otherwise JSON materialize (<ns>/<name>)
500
- seq-studio repos pull <ns>/<name> [-e env] [--ref r] [--out dir] [--force]
505
+ seq-studio repos pull <ns>/<name> -e <env> [--ref r] [--out dir] [--force]
501
506
  materialize the tree at a ref (JSON API)
502
- seq-studio repos delete <ns>/<name> [-e env] [--yes] delete a repo (confirm prompt)
507
+ seq-studio repos delete <ns>/<name> -e <env> [--yes] delete a repo (confirm prompt)
503
508
 
504
- Flags: -e/--env <env> (see: seq-studio envs list)
509
+ Flags: -e/--env <env> (required; see: seq-studio envs list)
505
510
 
506
511
  clone prefers real git clone (PAT via askpass — never written into the remote
507
512
  URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints
@@ -20,5 +20,5 @@ export declare function secretsSetDefaultCommand(args: ParsedArgs): Promise<numb
20
20
  * UI: pin + redeploy the function's active version / pin only / cancel.
21
21
  */
22
22
  export declare function secretsPinCommand(args: ParsedArgs): Promise<number>;
23
- export declare const SECRETS_USAGE = "usage:\n seq-studio secrets create <NAME> [--description text] register an org-owned secret\n seq-studio secrets set <NAME> set the shared default value (write-only)\n seq-studio secrets list [-e env] secrets you can see (never values)\n seq-studio secrets attach <NAME> --fn <slug> mount default on a function env var\n seq-studio secrets detach <NAME> --fn <slug> remove attachment\n seq-studio secrets apply --from-env-file .env [-e env] push defaults + attach (keys default from manifest)\n seq-studio secrets versions <NAME> value version history (never values)\n seq-studio secrets set-default <NAME> [version] point the default at a prior version (editor)\n seq-studio secrets pin <NAME> --fn <slug> [version] pin one function to a version (sticky; --unpin clears)\n\n Note: for the common deploy loop, secrets declared in managed-function.yml are\n reconciled automatically by `seq-studio functions deploy` using a local .env.\n Use `secrets` commands for CI (no .env), bulk/multi-function ops, or write-only\n value changes without a redeploy.\n\n Flags: -e/--env <env> (see: seq-studio envs list)\n --fn <slug> \u00B7 --env-var <NAME> \u00B7 --yes\n --functions <f1,f2> \u00B7 --keys <K1,K2> \u00B7 --all (override key selection)\n set-default: [version|version-row-id] (defaults to the most recent non-default) \u00B7 --yes\n pin: [version|version-row-id] (defaults to the current default) \u00B7 --unpin \u00B7 --yes (redeploy) \u00B7 --yes --no-redeploy\n";
23
+ export declare const SECRETS_USAGE = "usage:\n seq-studio secrets create <NAME> -e <env> [--description text] register an org-owned secret\n seq-studio secrets set <NAME> -e <env> set the shared default value (write-only)\n seq-studio secrets list -e <env> secrets you can see (never values)\n seq-studio secrets attach <NAME> --fn <slug> -e <env> mount default on a function env var\n seq-studio secrets detach <NAME> --fn <slug> -e <env> remove attachment\n seq-studio secrets apply --from-env-file .env -e <env> push defaults + attach (keys default from manifest)\n seq-studio secrets versions <NAME> -e <env> value version history (never values)\n seq-studio secrets set-default <NAME> [version] -e <env> point the default at a prior version (editor)\n seq-studio secrets pin <NAME> --fn <slug> [version] -e <env> pin one function to a version (sticky; --unpin clears)\n\n Note: for the common deploy loop, secrets declared in managed-function.yml are\n reconciled automatically by `seq-studio functions deploy` using a local .env.\n Use `secrets` commands for CI (no .env), bulk/multi-function ops, or write-only\n value changes without a redeploy.\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n --fn <slug> \u00B7 --env-var <NAME> \u00B7 --yes\n --functions <f1,f2> \u00B7 --keys <K1,K2> \u00B7 --all (override key selection)\n set-default: [version|version-row-id] (defaults to the most recent non-default) \u00B7 --yes\n pin: [version|version-row-id] (defaults to the current default) \u00B7 --unpin \u00B7 --yes (redeploy) \u00B7 --yes --no-redeploy\n";
24
24
  export declare function runSecretsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
@@ -128,7 +128,7 @@ async function functionExistsBySlugEventually({ ctx, slug, attempts = 3, retryDe
128
128
  export async function secretsCreateCommand(args) {
129
129
  const name = args.positional[0];
130
130
  if (!name) {
131
- console.error('usage: seq-studio secrets create <NAME> [-e env] [--description text]');
131
+ console.error('usage: seq-studio secrets create <NAME> -e <env> [--description text]');
132
132
  return 1;
133
133
  }
134
134
  const ctx = await buildContext(args);
@@ -150,7 +150,7 @@ export async function secretsCreateCommand(args) {
150
150
  export async function secretsSetCommand(args) {
151
151
  const name = args.positional[0];
152
152
  if (!name) {
153
- console.error('usage: seq-studio secrets set <NAME> [-e env]');
153
+ console.error('usage: seq-studio secrets set <NAME> -e <env>');
154
154
  return 1;
155
155
  }
156
156
  const ctx = await buildContext(args);
@@ -194,7 +194,7 @@ export async function secretsAttachCommand(args) {
194
194
  const name = args.positional[0];
195
195
  const fnSlug = typeof args.flags.fn === 'string' ? args.flags.fn : args.positional[1];
196
196
  if (!name || !fnSlug) {
197
- console.error('usage: seq-studio secrets attach <NAME> --fn <slug> [--env-var NAME]');
197
+ console.error('usage: seq-studio secrets attach <NAME> --fn <slug> -e <env> [--env-var NAME]');
198
198
  return 1;
199
199
  }
200
200
  const ctx = await buildContext(args);
@@ -229,7 +229,7 @@ export async function secretsDetachCommand(args) {
229
229
  const name = args.positional[0];
230
230
  const fnSlug = typeof args.flags.fn === 'string' ? args.flags.fn : args.positional[1];
231
231
  if (!name || !fnSlug) {
232
- console.error('usage: seq-studio secrets detach <NAME> --fn <slug>');
232
+ console.error('usage: seq-studio secrets detach <NAME> --fn <slug> -e <env>');
233
233
  return 1;
234
234
  }
235
235
  const ctx = await buildContext(args);
@@ -284,7 +284,7 @@ function resolveApplyKeys({ parsed, manifest, explicitKeys, applyAll, }) {
284
284
  export async function secretsApplyCommand(args) {
285
285
  const fromEnvFile = args.flags['from-env-file'];
286
286
  if (typeof fromEnvFile !== 'string') {
287
- console.error('usage: seq-studio secrets apply --from-env-file .env [-e env] [--yes]');
287
+ console.error('usage: seq-studio secrets apply --from-env-file .env -e <env> [--yes]');
288
288
  return 1;
289
289
  }
290
290
  const dir = workDir(args);
@@ -464,7 +464,7 @@ async function attachedFunctionSlugs(ctx, secretId) {
464
464
  export async function secretsVersionsCommand(args) {
465
465
  const name = args.positional[0];
466
466
  if (!name) {
467
- console.error('usage: seq-studio secrets versions <NAME> [-e env]');
467
+ console.error('usage: seq-studio secrets versions <NAME> -e <env>');
468
468
  return 1;
469
469
  }
470
470
  const ctx = await buildContext(args);
@@ -492,7 +492,7 @@ export async function secretsVersionsCommand(args) {
492
492
  export async function secretsSetDefaultCommand(args) {
493
493
  const name = args.positional[0];
494
494
  if (!name) {
495
- console.error('usage: seq-studio secrets set-default <NAME> [version] [-e env] [--yes]');
495
+ console.error('usage: seq-studio secrets set-default <NAME> [version] -e <env> [--yes]');
496
496
  return 1;
497
497
  }
498
498
  const ctx = await buildContext(args);
@@ -558,7 +558,7 @@ export async function secretsPinCommand(args) {
558
558
  const name = args.positional[0];
559
559
  const fnSlug = typeof args.flags.fn === 'string' ? args.flags.fn : undefined;
560
560
  if (!name || !fnSlug) {
561
- console.error('usage: seq-studio secrets pin <NAME> --fn <slug> [version] [--unpin] [-e env] [--yes] [--no-redeploy]');
561
+ console.error('usage: seq-studio secrets pin <NAME> --fn <slug> [version] [--unpin] -e <env> [--yes] [--no-redeploy]');
562
562
  return 1;
563
563
  }
564
564
  const ctx = await buildContext(args);
@@ -643,22 +643,22 @@ export async function secretsPinCommand(args) {
643
643
  // dispatcher
644
644
  // ---------------------------------------------------------------------------
645
645
  export const SECRETS_USAGE = `usage:
646
- seq-studio secrets create <NAME> [--description text] register an org-owned secret
647
- seq-studio secrets set <NAME> set the shared default value (write-only)
648
- seq-studio secrets list [-e env] secrets you can see (never values)
649
- seq-studio secrets attach <NAME> --fn <slug> mount default on a function env var
650
- seq-studio secrets detach <NAME> --fn <slug> remove attachment
651
- seq-studio secrets apply --from-env-file .env [-e env] push defaults + attach (keys default from manifest)
652
- seq-studio secrets versions <NAME> value version history (never values)
653
- seq-studio secrets set-default <NAME> [version] point the default at a prior version (editor)
654
- seq-studio secrets pin <NAME> --fn <slug> [version] pin one function to a version (sticky; --unpin clears)
646
+ seq-studio secrets create <NAME> -e <env> [--description text] register an org-owned secret
647
+ seq-studio secrets set <NAME> -e <env> set the shared default value (write-only)
648
+ seq-studio secrets list -e <env> secrets you can see (never values)
649
+ seq-studio secrets attach <NAME> --fn <slug> -e <env> mount default on a function env var
650
+ seq-studio secrets detach <NAME> --fn <slug> -e <env> remove attachment
651
+ seq-studio secrets apply --from-env-file .env -e <env> push defaults + attach (keys default from manifest)
652
+ seq-studio secrets versions <NAME> -e <env> value version history (never values)
653
+ seq-studio secrets set-default <NAME> [version] -e <env> point the default at a prior version (editor)
654
+ seq-studio secrets pin <NAME> --fn <slug> [version] -e <env> pin one function to a version (sticky; --unpin clears)
655
655
 
656
656
  Note: for the common deploy loop, secrets declared in managed-function.yml are
657
657
  reconciled automatically by \`seq-studio functions deploy\` using a local .env.
658
658
  Use \`secrets\` commands for CI (no .env), bulk/multi-function ops, or write-only
659
659
  value changes without a redeploy.
660
660
 
661
- Flags: -e/--env <env> (see: seq-studio envs list)
661
+ Flags: -e/--env <env> (required; see: seq-studio envs list)
662
662
  --fn <slug> · --env-var <NAME> · --yes
663
663
  --functions <f1,f2> · --keys <K1,K2> · --all (override key selection)
664
664
  set-default: [version|version-row-id] (defaults to the most recent non-default) · --yes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequenceholdings/studio-cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.18",
4
4
  "description": "Unified Sequence Studio CLI — `seq-studio process` (Lattice), `seq-studio artifact` (Artifact Studio), `seq-studio functions` / `secrets`, `seq-studio repos` (platform git-service), and `seq-studio auth pat` (git-service PATs). Includes Auth0 browser login shared with seqapi.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -40,15 +40,19 @@
40
40
  "smol-toml": "^1.4.2",
41
41
  "tsx": "^4.20.3",
42
42
  "zod": "^4.1.13",
43
- "@sequenceholdings/artifact-studio": "0.1.11",
44
- "@sequenceholdings/lattice": "0.1.1"
43
+ "@sequenceholdings/artifact-studio": "0.1.15",
44
+ "@sequenceholdings/lattice": "0.1.2"
45
45
  },
46
46
  "peerDependencies": {
47
+ "@sequenceholdings/pipeline-spec": "0.1.0",
47
48
  "@sequenceholdings/orm": "0.1.1"
48
49
  },
49
50
  "peerDependenciesMeta": {
50
51
  "@sequenceholdings/orm": {
51
52
  "optional": true
53
+ },
54
+ "@sequenceholdings/pipeline-spec": {
55
+ "optional": true
52
56
  }
53
57
  },
54
58
  "devDependencies": {
@@ -56,7 +60,8 @@
56
60
  "@types/node": "^22.0.0",
57
61
  "typescript": "^5.6.0",
58
62
  "vitest": "^4.1.5",
59
- "@sequenceholdings/orm": "0.1.1"
63
+ "@sequenceholdings/orm": "0.1.1",
64
+ "@sequenceholdings/pipeline-spec": "0.1.0"
60
65
  },
61
66
  "engines": {
62
67
  "node": ">=20"