@sequenceholdings/studio-cli 0.1.18 → 0.1.22

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.
@@ -15,8 +15,10 @@ import { confirmYes } from '../prompt.js';
15
15
  import { collectBundleFiles, isDirectory, validateLocalBundle, } from './bundle.js';
16
16
  import { managedFunctionManifestSchema, MF_MANIFEST_FILENAME, manifestEgressHosts, manifestEgressIpRanges, } from './manifest.js';
17
17
  import { buildEgressPreviewLines, egressPropagationNote, formatEgressSummary, printFunctionEgressHosts, } from './egress-preview.js';
18
- import { parseSourceSpec, resolveArtifactSource, } from '@sequenceholdings/artifact-studio/source-resolver';
18
+ import { resolveArtifactSource, } from '@sequenceholdings/artifact-studio/source-resolver';
19
19
  import { buildSecretPreviewLines, classifySecrets, } from './secret-reconcile.js';
20
+ import { parseFunctionsSourceSelection, resolveFunctionSourceDir, } from './source-selection.js';
21
+ export { parseFunctionsSourceSelection, parseFunctionsSourceSpec, resolveFunctionSourceDir } from './source-selection.js';
20
22
  const execFileAsync = promisify(execFile);
21
23
  export const LOG = '[seq-studio]';
22
24
  const POLL_INTERVAL_MS = 5_000;
@@ -85,20 +87,6 @@ export function workDir(args) {
85
87
  const dir = typeof args.flags.dir === 'string' ? args.flags.dir : '.';
86
88
  return resolve(dir);
87
89
  }
88
- /**
89
- * Source selection for build/deploy: --dir (local, default '.'), a platform
90
- * git-service repo (--repo <ns>/<name>), or any git URL (--git-url <url>);
91
- * --ref picks a branch/tag/commit. Reuses the artifact-studio resolver —
92
- * functions name their local dir with --dir rather than a positional, so map
93
- * it onto the spec parser's positional slot.
94
- */
95
- export function parseFunctionsSourceSpec(args) {
96
- const dir = typeof args.flags.dir === 'string' ? args.flags.dir : undefined;
97
- if (dir !== undefined && (args.flags.repo !== undefined || args.flags['git-url'] !== undefined)) {
98
- throw new Error('--dir cannot be combined with --repo / --git-url.');
99
- }
100
- return parseSourceSpec({ positional: dir === undefined ? [] : [dir], flags: args.flags });
101
- }
102
90
  /** Manifest read with a source-aware error for the missing case. */
103
91
  async function readSourceManifest(dir, spec) {
104
92
  const manifest = await readManifestOptional(dir);
@@ -106,7 +94,7 @@ async function readSourceManifest(dir, spec) {
106
94
  return manifest;
107
95
  throw new Error(spec.kind === 'local'
108
96
  ? `No ${MF_MANIFEST_FILENAME} in ${dir}. Run \`seq-studio functions init\` to scaffold one, or pass --dir.`
109
- : `No ${MF_MANIFEST_FILENAME} at the root of the source repoa managed-function repo keeps its manifest at the top level.`);
97
+ : `No ${MF_MANIFEST_FILENAME} in the selected remote source directorykeep it at the repo root or select a function directory with --path.`);
110
98
  }
111
99
  /**
112
100
  * Resolve the target function: --fn <slug> wins, else the manifest in the
@@ -210,6 +198,7 @@ entrypoint: handler
210
198
  limits:
211
199
  memory_mb: 256
212
200
  timeout_seconds: 60
201
+ min_instances: 0
213
202
  max_instances: 3
214
203
  invoke_rate_per_minute: 60
215
204
 
@@ -349,14 +338,15 @@ export async function functionsInitCommand(args) {
349
338
  // build (local pre-flight)
350
339
  // ---------------------------------------------------------------------------
351
340
  export async function functionsBuildCommand(args) {
352
- const spec = parseFunctionsSourceSpec(args);
341
+ const { spec, path } = parseFunctionsSourceSelection(args);
353
342
  // A public git URL needs no bearer token, but optional auth resolution still
354
343
  // identifies a selected M2M principal so policy can reject arbitrary CI
355
344
  // input before cloning. Local builds remain completely offline.
356
345
  const remote = await buildSourceOptions({ args, spec });
357
346
  const source = await resolveArtifactSource(spec, remote);
358
347
  try {
359
- return await buildFromResolvedSource({ spec, source });
348
+ const dir = await resolveFunctionSourceDir({ repoDir: source.dir, path });
349
+ return await buildFromResolvedSource({ spec, source: { ...source, dir } });
360
350
  }
361
351
  finally {
362
352
  await source.cleanup();
@@ -417,7 +407,7 @@ export async function functionsDeployCommand(args) {
417
407
  console.error(`${LOG} ${REQUIRE_EXPLICIT_ENV_MESSAGE}`);
418
408
  return 1;
419
409
  }
420
- const spec = parseFunctionsSourceSpec(args);
410
+ const { spec, path } = parseFunctionsSourceSelection(args);
421
411
  // Remote deploys resolve auth up front: --repo needs it to fetch the tree,
422
412
  // while --git-url must reject a selected M2M principal before cloning.
423
413
  // Local sources still defer auth until after validation so malformed local
@@ -425,7 +415,8 @@ export async function functionsDeployCommand(args) {
425
415
  const ctx = spec.kind !== 'local' ? await buildContext(args) : null;
426
416
  const source = await resolveArtifactSource(spec, ctx ? sourceClientOptions(ctx) : {});
427
417
  try {
428
- return await deployFromResolvedSource({ args, spec, ctx, source });
418
+ const dir = await resolveFunctionSourceDir({ repoDir: source.dir, path });
419
+ return await deployFromResolvedSource({ args, spec, ctx, source: { ...source, dir } });
429
420
  }
430
421
  finally {
431
422
  await source.cleanup();
@@ -983,17 +974,19 @@ export const FUNCTIONS_USAGE = `usage:
983
974
  (version history is retained)
984
975
 
985
976
  Flags: -e/--env <env|preview:<slug>> (required for network commands; see: seq-studio envs list) · --fn <slug> · --dir <path>
977
+ --path <repo-subdirectory> (remote build/deploy only) select one function in a multi-function repo
986
978
  --from-env-file <path> (default: .env) source file for secret values
987
979
  --no-wait · --yes
988
980
  --no-provision (deploy) update-only: error instead of registering a new
989
981
  shell, writing secret values, or attaching secrets (CI sweep)
990
982
 
991
983
  Source for build/deploy: a local --dir (default .), a platform git-service
992
- repo (--repo <ns>/<name>), or a public HTTPS git URL (--git-url <url>). --ref selects a
993
- branch/tag/commit (default: the repo's default branch). Remote sources record
994
- the pinned commit as provenance (never dirty) and NEVER read a repo-committed
995
- .env for secret values provision secrets server-side or pass a local
996
- --from-env-file (resolved against your cwd).
984
+ repo (--repo <ns>/<name>), or a public HTTPS git URL (--git-url <url>). --path selects
985
+ a function directory within a remote repo; omit it for the existing root-manifest
986
+ layout. --ref selects a branch/tag/commit (default: the repo's default branch).
987
+ Remote sources record the pinned commit as provenance (never dirty) and NEVER
988
+ read a repo-committed .env for secret values — provision secrets server-side
989
+ or pass a local --from-env-file (resolved against your cwd).
997
990
 
998
991
  Interactive --repo builds clone over smart-HTTP and require a repo:read git
999
992
  PAT in ATLAS_GIT_PAT (\`seq-studio auth pat create --scopes repo:read\`, or
@@ -52,6 +52,7 @@ export declare const managedFunctionManifestSchema: z.ZodObject<{
52
52
  limits: z.ZodDefault<z.ZodObject<{
53
53
  memory_mb: z.ZodDefault<z.ZodNumber>;
54
54
  timeout_seconds: z.ZodDefault<z.ZodNumber>;
55
+ min_instances: z.ZodDefault<z.ZodNumber>;
55
56
  max_instances: z.ZodDefault<z.ZodNumber>;
56
57
  invoke_rate_per_minute: z.ZodDefault<z.ZodNumber>;
57
58
  }, z.core.$strip>>;
@@ -83,6 +84,7 @@ export declare const managedFunctionManifestSchema: z.ZodObject<{
83
84
  data: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
84
85
  tables: z.ZodDefault<z.ZodArray<z.ZodString>>;
85
86
  actions: z.ZodDefault<z.ZodArray<z.ZodString>>;
87
+ operations: z.ZodDefault<z.ZodArray<z.ZodString>>;
86
88
  query: z.ZodDefault<z.ZodBoolean>;
87
89
  }, z.core.$strip>>>;
88
90
  }, z.core.$strip>>;
@@ -493,6 +493,12 @@ function validateCapabilityGates({ gates, uses, roles, }) {
493
493
  * Mirrors atlas/src/server/services/managed-functions/manifest.ts.
494
494
  */
495
495
  const SERVICE_ACCOUNT_REF_RE = /^([a-z][a-z0-9-]{1,98}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
496
+ /**
497
+ * GraphQL Name grammar — persisted-operation names exactly as they appear in
498
+ * a namespace's operations manifest. Mirrors
499
+ * atlas/src/server/services/managed-functions/manifest.ts.
500
+ */
501
+ const GRAPHQL_OPERATION_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]*$/;
496
502
  export const managedFunctionManifestSchema = z.object({
497
503
  schema_version: z.literal(1).default(1),
498
504
  function: z.object({
@@ -510,12 +516,23 @@ export const managedFunctionManifestSchema = z.object({
510
516
  .object({
511
517
  memory_mb: z.number().int().min(128).max(2048).default(256),
512
518
  timeout_seconds: z.number().int().min(1).max(540).default(60),
519
+ min_instances: z.number().int().min(0).max(10).default(0),
513
520
  max_instances: z.number().int().min(1).max(10).default(3),
514
521
  invoke_rate_per_minute: z.number().int().min(1).max(600).default(60),
522
+ })
523
+ .superRefine((limits, ctx) => {
524
+ if (limits.min_instances > limits.max_instances) {
525
+ ctx.addIssue({
526
+ code: 'custom',
527
+ message: 'min_instances cannot exceed max_instances',
528
+ path: ['min_instances'],
529
+ });
530
+ }
515
531
  })
516
532
  .default({
517
533
  memory_mb: 256,
518
534
  timeout_seconds: 60,
535
+ min_instances: 0,
519
536
  max_instances: 3,
520
537
  invoke_rate_per_minute: 60,
521
538
  }),
@@ -567,12 +584,13 @@ export const managedFunctionManifestSchema = z.object({
567
584
  gates: capabilityGatesSchema,
568
585
  /**
569
586
  * ORM Data API consumer reach, grouped by namespace: the `tables` this
570
- * function may read, the `actions` it may invoke, and whether raw `query`
571
- * (arbitrary read SQL over the namespace) is allowed — all ON BEHALF OF the
572
- * invoking user. The invoke proxy mints a short-lived token scoped to
573
- * exactly these refs; the Data API re-resolves the user's claims per call,
574
- * so declaring a table never widens what the user could see. Writes go
575
- * through declared actions only raw table writes don't exist.
587
+ * function may read, the v1 `actions` and v2 persisted `operations` it may
588
+ * invoke, and whether raw `query` (arbitrary read SQL over the namespace)
589
+ * is allowed — all ON BEHALF OF the invoking user. The invoke proxy mints
590
+ * a short-lived token scoped to exactly these refs; the Data API
591
+ * re-resolves the user's claims per call, so declaring a table never
592
+ * widens what the user could see. Writes go through declared actions and
593
+ * operations only — raw table writes don't exist.
576
594
  *
577
595
  * Mirrors atlas/src/server/services/managed-functions/manifest.ts — must
578
596
  * stay in sync so `seq-studio` doesn't strip the block before deploy.
@@ -581,6 +599,18 @@ export const managedFunctionManifestSchema = z.object({
581
599
  .record(z.string().regex(/^[a-z][a-z0-9_]{0,40}$/, 'capabilities.data keys are ORM namespace names'), z.object({
582
600
  tables: z.array(z.string().regex(/^[a-z_][a-z0-9_]*$/, 'table names')).max(64).default([]),
583
601
  actions: z.array(z.string().regex(/^[a-z_][a-z0-9_]*$/, 'action names')).max(64).default([]),
602
+ /**
603
+ * ORM v2 persisted operations (GraphQL mutations/actions by name)
604
+ * this function may execute — each mints a `<ns>/ops/<Name>` write
605
+ * ref on the invoke token.
606
+ */
607
+ operations: z
608
+ .array(z
609
+ .string()
610
+ .max(128)
611
+ .regex(GRAPHQL_OPERATION_NAME_RE, 'operation names must match the GraphQL Name grammar'))
612
+ .max(64)
613
+ .default([]),
584
614
  query: z.boolean().default(false),
585
615
  }))
586
616
  .default({}),
@@ -600,9 +630,9 @@ export const managedFunctionManifestSchema = z.object({
600
630
  ctx.addIssue({ code: 'custom', message, path: ['capabilities', 'gates'] });
601
631
  }
602
632
  // A namespace is "reached" when its block declares at least one table,
603
- // action, or raw query — the same rule the server's floor reconciliation
604
- // uses (readManifestDataNamespaces).
605
- const reachesData = Object.values(manifest.capabilities.data).some((block) => block.tables.length > 0 || block.actions.length > 0 || block.query);
633
+ // action, operation, or raw query — the same rule the server's floor
634
+ // reconciliation uses (readManifestDataNamespaces).
635
+ const reachesData = Object.values(manifest.capabilities.data).some((block) => block.tables.length > 0 || block.actions.length > 0 || block.operations.length > 0 || block.query);
606
636
  if (reachesData && manifest.service_account === undefined) {
607
637
  ctx.addIssue({
608
638
  code: 'custom',
@@ -0,0 +1,24 @@
1
+ import { type SourceSpec } from '@sequenceholdings/artifact-studio/source-resolver';
2
+ import type { ParsedArgs } from '../process/commands.js';
3
+ export interface FunctionsSourceSelection {
4
+ spec: SourceSpec;
5
+ path?: string;
6
+ }
7
+ /**
8
+ * Functions name their local source with --dir rather than a positional, so
9
+ * map it onto the shared source parser used by other seq-studio primitives.
10
+ */
11
+ export declare function parseFunctionsSourceSpec(args: ParsedArgs): SourceSpec;
12
+ /**
13
+ * Select a managed function within a remote repository. Local callers already
14
+ * select the function root with --dir, so --path is remote-only.
15
+ */
16
+ export declare function parseFunctionsSourceSelection(args: ParsedArgs): FunctionsSourceSelection;
17
+ /**
18
+ * Resolve the selected function root after the repository is materialized.
19
+ * realpath containment prevents a committed symlink from escaping the repo.
20
+ */
21
+ export declare function resolveFunctionSourceDir({ repoDir, path, }: {
22
+ repoDir: string;
23
+ path?: string;
24
+ }): Promise<string>;
@@ -0,0 +1,67 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+ import { parseSourceSpec, } from '@sequenceholdings/artifact-studio/source-resolver';
4
+ import { isDirectory } from './bundle.js';
5
+ /**
6
+ * Functions name their local source with --dir rather than a positional, so
7
+ * map it onto the shared source parser used by other seq-studio primitives.
8
+ */
9
+ export function parseFunctionsSourceSpec(args) {
10
+ const dir = typeof args.flags.dir === 'string' ? args.flags.dir : undefined;
11
+ if (dir !== undefined && (args.flags.repo !== undefined || args.flags['git-url'] !== undefined)) {
12
+ throw new Error('--dir cannot be combined with --repo / --git-url.');
13
+ }
14
+ return parseSourceSpec({ positional: dir === undefined ? [] : [dir], flags: args.flags });
15
+ }
16
+ function validateFunctionRepoPath(path) {
17
+ const segments = path.split('/');
18
+ if (path.length === 0 ||
19
+ path.trim() !== path ||
20
+ isAbsolute(path) ||
21
+ path.includes('\\') ||
22
+ [...path].some((character) => character.charCodeAt(0) < 0x20) ||
23
+ segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
24
+ throw new Error(`--path must be a canonical relative directory within the repository (got ${JSON.stringify(path)}).`);
25
+ }
26
+ }
27
+ /**
28
+ * Select a managed function within a remote repository. Local callers already
29
+ * select the function root with --dir, so --path is remote-only.
30
+ */
31
+ export function parseFunctionsSourceSelection(args) {
32
+ const spec = parseFunctionsSourceSpec(args);
33
+ const pathFlag = args.flags.path;
34
+ if (pathFlag === true)
35
+ throw new Error('--path requires a value.');
36
+ if (pathFlag === undefined)
37
+ return { spec };
38
+ if (spec.kind === 'local') {
39
+ throw new Error('--path only applies together with --repo or --git-url; use --dir for local source.');
40
+ }
41
+ validateFunctionRepoPath(pathFlag);
42
+ return { spec, path: pathFlag };
43
+ }
44
+ /**
45
+ * Resolve the selected function root after the repository is materialized.
46
+ * realpath containment prevents a committed symlink from escaping the repo.
47
+ */
48
+ export async function resolveFunctionSourceDir({ repoDir, path, }) {
49
+ if (path === undefined)
50
+ return repoDir;
51
+ validateFunctionRepoPath(path);
52
+ const candidate = resolve(repoDir, path);
53
+ if (!(await isDirectory(candidate))) {
54
+ throw new Error(`--path does not name a directory in the repository: ${path}`);
55
+ }
56
+ const [canonicalRepoDir, canonicalCandidate] = await Promise.all([
57
+ realpath(repoDir),
58
+ realpath(candidate),
59
+ ]);
60
+ const relativePath = relative(canonicalRepoDir, canonicalCandidate);
61
+ if (relativePath === '..' ||
62
+ relativePath.startsWith(`..${sep}`) ||
63
+ isAbsolute(relativePath)) {
64
+ throw new Error(`--path must stay within the repository: ${path}`);
65
+ }
66
+ return canonicalCandidate;
67
+ }
package/dist/main.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * seq-studio process <sub> manage Lattice processes
6
6
  * seq-studio artifact <sub> manage Artifact Studio apps
7
7
  * seq-studio functions <sub> manage Managed Functions
8
+ * seq-studio agents <sub> manage typed agent definitions
8
9
  * seq-studio secrets <sub> manage org-owned Managed Secrets
9
10
  * seq-studio repos <sub> manage platform git-service repos
10
11
  * seq-studio pipeline <sub> author + validate Data Pipelines stage specs
package/dist/main.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * seq-studio process <sub> manage Lattice processes
6
6
  * seq-studio artifact <sub> manage Artifact Studio apps
7
7
  * seq-studio functions <sub> manage Managed Functions
8
+ * seq-studio agents <sub> manage typed agent definitions
8
9
  * seq-studio secrets <sub> manage org-owned Managed Secrets
9
10
  * seq-studio repos <sub> manage platform git-service repos
10
11
  * seq-studio pipeline <sub> author + validate Data Pipelines stage specs
@@ -23,6 +24,7 @@ const TOP_LEVEL_USAGE = `usage:
23
24
  seq-studio process <sub> [args] lint | plan | apply | test | simulate | bundle | init
24
25
  seq-studio artifact <sub> [args] init | build | plan | deploy | dev | list | show | pull | promote | rollback
25
26
  seq-studio functions <sub> [args] init | build | deploy | list | show | logs | promote | rollback | delete
27
+ seq-studio agents <sub> [args] init | validate | plan | apply | list | show
26
28
  seq-studio secrets <sub> [args] create | set | list | attach | detach | apply
27
29
  seq-studio repos <sub> [args] list | namespaces | show | create | clone | pull | delete
28
30
  seq-studio pipeline <sub> [args] init | validate — Data Pipelines stage specs
@@ -73,6 +75,10 @@ export async function run(argv = process.argv.slice(2)) {
73
75
  const { runFunctionsCommand } = await import('./functions/commands.js');
74
76
  return runFunctionsCommand(sub, parseArgs(rest));
75
77
  }
78
+ case 'agents': {
79
+ const { runAgentsCommand } = await import('./agents/commands.js');
80
+ return runAgentsCommand(sub, parseArgs(rest));
81
+ }
76
82
  case 'secrets': {
77
83
  const { runSecretsCommand } = await import('./secrets/commands.js');
78
84
  return runSecretsCommand(sub, parseArgs(rest));
@@ -9,14 +9,19 @@ import { getAccessToken, tryGetAccessToken } from '../auth.js';
9
9
  import { resolveEnvWithDiscovery } from '../config.js';
10
10
  import { normalizeShortEnvFlag, readEnvFromArgv } from '../env-flags.js';
11
11
  import { REQUIRE_EXPLICIT_ENV_MESSAGE } from '../env-flags.js';
12
- /** ORM subcommands that talk to Atlas — must not silently target `local`. */
12
+ /**
13
+ * ORM subcommands that talk to Atlas — must not silently target `local`.
14
+ * `init`, `generate`, `validate`, `diff`, and `migrate-from-yaml` are offline.
15
+ */
13
16
  const ORM_NETWORK_SUBS = new Set(['plan', 'apply']);
14
17
  const ORM_USAGE = `usage:
15
- seq-studio orm init <dir> scaffold a namespace directory
16
- seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
17
- seq-studio orm plan [dir] -e <env> compile definitions and diff against the registry
18
- seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh types.gen.ts (the everyday command)
19
- seq-studio orm diff [dir] write/verify the committed migration standalone (--check is the offline CI gate; --allow-destructive consents)
18
+ seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
19
+ seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (offline; runs consumer codegen when codegen.ts is present)
20
+ seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
21
+ seq-studio orm plan [dir] -e <env> compile definitions and diff against the registry
22
+ seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh the generated outputs (the everyday command)
23
+ seq-studio orm diff [dir] write/verify the committed migration standalone (--check is the offline CI gate; --allow-destructive consents; --rebase records a v1 -> v2 conversion)
24
+ seq-studio orm migrate-from-yaml <dir> convert a v1 YAML namespace to the v2 TypeScript format (offline; migrations/ preserved)
20
25
 
21
26
  Environments: see \`seq-studio envs list\` (built-in: local; more are
22
27
  discovered after you authenticate).
@@ -49,6 +49,19 @@ const PIPELINE_USAGE = `usage:
49
49
  banksouth require --approved-by — approvals are explicit even for
50
50
  rollbacks)
51
51
 
52
+ seq-studio pipeline adopt --stage <slug> --ref <sha|branch> -e <env>
53
+ --native-id <id> [--resource-key <key>] [--kind job|dlt_pipeline]
54
+ --approved-by <you> [--old-source-removal-pr <url>]
55
+ [--repo pipelines/<slug>] [--json]
56
+ bind a live Databricks job/pipeline to the stage without recreation
57
+ (SEQ-2449). Always requires --approved-by. Monorepo-declared keys also
58
+ require --old-source-removal-pr.
59
+
60
+ seq-studio pipeline unbind --stage <slug> --ref <sha|branch> -e <env>
61
+ --approved-by <you> [--resource-key <key>]
62
+ [--repo pipelines/<slug>] [--json]
63
+ release an adopted binding; the remote object stays live (never deleted)
64
+
52
65
  seq-studio pipeline run-now --stage <slug> -e <env> [--repo pipelines/<slug>] [--json]
53
66
  fire the stage's active deployment resource (job run-now / DLT
54
67
  start_update) and print the Databricks run URL
@@ -297,6 +310,14 @@ export async function runPipelineCommand(sub, args) {
297
310
  return pipelineInitCommand(args);
298
311
  case 'validate':
299
312
  return pipelineValidateCommand(args);
313
+ case 'adopt': {
314
+ const { pipelineAdoptCommand } = await import('./lifecycle.js');
315
+ return pipelineAdoptCommand(args);
316
+ }
317
+ case 'unbind': {
318
+ const { pipelineUnbindCommand } = await import('./lifecycle.js');
319
+ return pipelineUnbindCommand(args);
320
+ }
300
321
  case 'plan': {
301
322
  const { pipelinePlanCommand } = await import('./lifecycle.js');
302
323
  return pipelinePlanCommand(args);
@@ -56,3 +56,5 @@ export declare function pipelineDeployCommand(args: ParsedArgs): Promise<number>
56
56
  export declare function pipelinePromoteCommand(args: ParsedArgs): Promise<number>;
57
57
  export declare function pipelineRunNowCommand(args: ParsedArgs): Promise<number>;
58
58
  export declare function pipelineRollbackCommand(args: ParsedArgs): Promise<number>;
59
+ export declare function pipelineAdoptCommand(args: ParsedArgs): Promise<number>;
60
+ export declare function pipelineUnbindCommand(args: ParsedArgs): Promise<number>;
@@ -315,6 +315,113 @@ async function pollDeployment({ baseUrl, token, deploymentId, }) {
315
315
  console.error(`${LOG} timed out waiting for deployment ${deploymentId}`);
316
316
  return 1;
317
317
  }
318
+ export async function pipelineAdoptCommand(args) {
319
+ const stage = flagString(args.flags, 'stage');
320
+ const ref = flagString(args.flags, 'ref');
321
+ const nativeId = flagString(args.flags, 'native-id');
322
+ const approvedBy = flagString(args.flags, 'approved-by');
323
+ if (!stage || !ref || !nativeId || !approvedBy) {
324
+ console.error('usage: seq-studio pipeline adopt --stage <slug> --ref <sha|branch> -e <env> ' +
325
+ '--native-id <id> --approved-by <you> [--resource-key <key>] [--kind job|dlt_pipeline] ' +
326
+ '[--old-source-removal-pr <url>] [--repo pipelines/<slug>] [--json]');
327
+ return 1;
328
+ }
329
+ const { env, token } = await envAndToken(args);
330
+ const json = args.flags.json === true || args.flags.json === 'true';
331
+ const repo = flagString(args.flags, 'repo');
332
+ const resourceKey = flagString(args.flags, 'resource-key') ?? stage.replace(/-/g, '_');
333
+ const kind = flagString(args.flags, 'kind') ?? 'job';
334
+ if (kind !== 'job' && kind !== 'dlt_pipeline') {
335
+ console.error(`${LOG} --kind must be job or dlt_pipeline`);
336
+ return 1;
337
+ }
338
+ const oldSourceRemovalPr = flagString(args.flags, 'old-source-removal-pr');
339
+ const stageId = await resolveStageIdBySlug({
340
+ baseUrl: env.url,
341
+ token,
342
+ slug: stage,
343
+ repo,
344
+ });
345
+ try {
346
+ const response = await postJson({
347
+ baseUrl: env.url,
348
+ token,
349
+ path: `/api/data-pipelines/stages/${stageId}/adopt`,
350
+ body: {
351
+ environment: deployEnvironmentForEnv(env),
352
+ ref,
353
+ bindings: [{ resourceKey, nativeId, kind }],
354
+ approvedBy,
355
+ ...(oldSourceRemovalPr ? { oldSourceRemovalPr } : {}),
356
+ },
357
+ });
358
+ if (json) {
359
+ console.log(JSON.stringify(response, null, 2));
360
+ }
361
+ else {
362
+ console.log(`${LOG} enqueued adopt for ${stage} → ${resourceKey}=${nativeId} ` +
363
+ `(trigger=${response.triggerRunId}). Worker binds on Trigger; ` +
364
+ `confirm with plan or stage_resources once the run completes.`);
365
+ }
366
+ return 0;
367
+ }
368
+ catch (error) {
369
+ if (error instanceof AtlasApiError) {
370
+ console.error(`${LOG} adopt failed: ${error.message}`);
371
+ return 1;
372
+ }
373
+ throw error;
374
+ }
375
+ }
376
+ export async function pipelineUnbindCommand(args) {
377
+ const stage = flagString(args.flags, 'stage');
378
+ const ref = flagString(args.flags, 'ref');
379
+ const approvedBy = flagString(args.flags, 'approved-by');
380
+ if (!stage || !ref || !approvedBy) {
381
+ console.error('usage: seq-studio pipeline unbind --stage <slug> --ref <sha|branch> -e <env> ' +
382
+ '--approved-by <you> [--resource-key <key>] [--repo pipelines/<slug>] [--json]');
383
+ return 1;
384
+ }
385
+ const { env, token } = await envAndToken(args);
386
+ const json = args.flags.json === true || args.flags.json === 'true';
387
+ const repo = flagString(args.flags, 'repo');
388
+ const resourceKey = flagString(args.flags, 'resource-key');
389
+ const stageId = await resolveStageIdBySlug({
390
+ baseUrl: env.url,
391
+ token,
392
+ slug: stage,
393
+ repo,
394
+ });
395
+ try {
396
+ const response = await postJson({
397
+ baseUrl: env.url,
398
+ token,
399
+ path: `/api/data-pipelines/stages/${stageId}/unbind`,
400
+ body: {
401
+ environment: deployEnvironmentForEnv(env),
402
+ ref,
403
+ approvedBy,
404
+ ...(resourceKey ? { resourceKeys: [resourceKey] } : {}),
405
+ },
406
+ });
407
+ if (json) {
408
+ console.log(JSON.stringify(response, null, 2));
409
+ }
410
+ else {
411
+ console.log(`${LOG} enqueued unbind for ${stage}` +
412
+ (resourceKey ? ` (${resourceKey})` : '') +
413
+ ` (trigger=${response.triggerRunId}). Remote object is never deleted.`);
414
+ }
415
+ return 0;
416
+ }
417
+ catch (error) {
418
+ if (error instanceof AtlasApiError) {
419
+ console.error(`${LOG} unbind failed: ${error.message}`);
420
+ return 1;
421
+ }
422
+ throw error;
423
+ }
424
+ }
318
425
  /**
319
426
  * Stage identity is `(repo, slug)` — a bare slug can be ambiguous across
320
427
  * Pipelines. `--repo pipelines/<domain>` scopes the lookup server-side; an
@@ -190,8 +190,9 @@ function serializeNodeMetadata(node) {
190
190
  const human = node;
191
191
  const meta = {
192
192
  metadata: human.metadata ?? {},
193
- timeout: human.timeout ?? '7d',
194
193
  };
194
+ if (human.timeout !== undefined)
195
+ meta.timeout = human.timeout;
195
196
  if (human.on_timeout_edge_id)
196
197
  meta.on_timeout_edge_id = human.on_timeout_edge_id;
197
198
  if (human.completeLabel)
@@ -549,6 +549,14 @@ function lintHuman(processId, node, errors, warnings) {
549
549
  }
550
550
  }
551
551
  if (node.on_timeout_edge_id) {
552
+ if (node.timeout === undefined) {
553
+ errors.push({
554
+ process_id: processId,
555
+ node_id: node.id,
556
+ message: `on_timeout_edge_id "${node.on_timeout_edge_id}" requires timeout — ` +
557
+ `omit on_timeout_edge_id for an indefinite wait, or set timeout to enable the timeout edge`,
558
+ });
559
+ }
552
560
  const ok = node.outgoing_edges.some((e) => e.id === node.on_timeout_edge_id);
553
561
  if (!ok) {
554
562
  errors.push({
@@ -44,6 +44,58 @@ export declare function reposCloneCommand(args: ParsedArgs, deps?: {
44
44
  * so askpass never sends ATLAS_GIT_PAT to an attacker-controlled host.
45
45
  */
46
46
  export declare function normalizeCloneUrl(raw: string, env: ResolvedEnv): string;
47
+ type DiscoveredCliCheck = {
48
+ name: string;
49
+ script?: string;
50
+ command?: string[];
51
+ source: 'autodiscover' | 'ci.json';
52
+ };
53
+ /**
54
+ * Pure discovery shared by `repos ci show` / `import`. Mirrors atlas
55
+ * `discoverCiChecks` / `parseCiJson` so CLI preview matches execution.
56
+ */
57
+ export declare function discoverCliChecks({ ciJson, packageScripts, }: {
58
+ ciJson?: unknown;
59
+ packageScripts?: Record<string, string>;
60
+ }): {
61
+ ok: true;
62
+ checks: DiscoveredCliCheck[];
63
+ } | {
64
+ ok: false;
65
+ error: string;
66
+ };
67
+ /**
68
+ * Load `.seq/ci.json` / `package.json` for CI preview.
69
+ *
70
+ * Matches the runner: only a true 404 means "file absent". Invalid JSON and
71
+ * non-404 API errors fail closed (no silent package.json fallback over a bad
72
+ * `.seq/ci.json`).
73
+ */
74
+ export declare function loadRemoteCiSources({ ctx, repoId, ref, }: {
75
+ ctx: CommandContext;
76
+ repoId: string;
77
+ ref: string;
78
+ }): Promise<{
79
+ ok: true;
80
+ ciJson?: unknown;
81
+ packageScripts?: Record<string, string>;
82
+ } | {
83
+ ok: false;
84
+ error: string;
85
+ }>;
86
+ /**
87
+ * Discover CI checks from a remote repo tip (pure preview — no Settings write).
88
+ * Uses the same autodiscovery rules as the platform runner (PLA-378).
89
+ */
90
+ export declare function reposCiShowCommand(args: ParsedArgs): Promise<number>;
91
+ /** Add a check-run name to Settings `requiredChecks` (merge-blocking). */
92
+ export declare function reposCiRequireCommand(args: ParsedArgs): Promise<number>;
93
+ /**
94
+ * Set requiredChecks to every check discovered on the tip (opt-in bulk require).
95
+ */
96
+ export declare function reposCiImportCommand(args: ParsedArgs): Promise<number>;
97
+ export declare function reposCiCommand(args: ParsedArgs): Promise<number>;
47
98
  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> (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";
99
+ 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 seq-studio repos ci show <ns>/<name> -e <env> [--ref r] preview discovered CI checks\n seq-studio repos ci require <ns>/<name> --check <name> -e <env> reserved (refuses write until sandboxed runner)\n seq-studio repos ci import <ns>/<name> -e <env> [--ref r] reserved (refuses write until sandboxed runner)\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
100
  export declare function runReposCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
101
+ export {};