@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.
- package/README.md +232 -11
- package/dist/agents/apply-chunks.d.ts +13 -0
- package/dist/agents/apply-chunks.js +43 -0
- package/dist/agents/commands.d.ts +10 -0
- package/dist/agents/commands.js +238 -0
- package/dist/agents/scaffold.d.ts +2 -0
- package/dist/agents/scaffold.js +77 -0
- package/dist/agents/source.d.ts +20 -0
- package/dist/agents/source.js +144 -0
- package/dist/atlas-client.js +29 -0
- package/dist/auth.d.ts +19 -17
- package/dist/auth.js +102 -33
- package/dist/functions/commands.d.ts +2 -10
- package/dist/functions/commands.js +18 -25
- package/dist/functions/manifest.d.ts +2 -0
- package/dist/functions/manifest.js +39 -9
- package/dist/functions/source-selection.d.ts +24 -0
- package/dist/functions/source-selection.js +67 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +6 -0
- package/dist/orm/delegate.js +11 -6
- package/dist/pipeline/commands.js +21 -0
- package/dist/pipeline/lifecycle.d.ts +2 -0
- package/dist/pipeline/lifecycle.js +107 -0
- package/dist/process/build.js +2 -1
- package/dist/process/lint.js +8 -0
- package/dist/repos/commands.d.ts +53 -1
- package/dist/repos/commands.js +258 -1
- package/package.json +8 -6
|
@@ -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 {
|
|
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}
|
|
97
|
+
: `No ${MF_MANIFEST_FILENAME} in the selected remote source directory — keep 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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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>). --
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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`
|
|
571
|
-
* (arbitrary read SQL over the namespace)
|
|
572
|
-
* invoking user. The invoke proxy mints
|
|
573
|
-
* exactly these refs; the Data API
|
|
574
|
-
*
|
|
575
|
-
*
|
|
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
|
|
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));
|
package/dist/orm/delegate.js
CHANGED
|
@@ -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
|
-
/**
|
|
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>
|
|
16
|
-
seq-studio orm
|
|
17
|
-
seq-studio orm
|
|
18
|
-
seq-studio orm
|
|
19
|
-
seq-studio orm
|
|
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
|
package/dist/process/build.js
CHANGED
|
@@ -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)
|
package/dist/process/lint.js
CHANGED
|
@@ -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({
|
package/dist/repos/commands.d.ts
CHANGED
|
@@ -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 {};
|