@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.
- package/README.md +116 -30
- package/dist/artifact/delegate.d.ts +2 -2
- package/dist/artifact/delegate.js +31 -73
- package/dist/atlas-client.js +52 -37
- package/dist/auth-cmds/commands.d.ts +1 -1
- package/dist/auth-cmds/commands.js +12 -7
- package/dist/auth.d.ts +97 -19
- package/dist/auth.js +376 -81
- package/dist/config.d.ts +13 -3
- package/dist/config.js +41 -14
- package/dist/env-catalog.js +13 -3
- package/dist/env-flags.d.ts +2 -0
- package/dist/env-flags.js +2 -0
- package/dist/env-registry.d.ts +27 -0
- package/dist/env-registry.js +204 -0
- package/dist/envs/commands.d.ts +1 -1
- package/dist/envs/commands.js +65 -10
- package/dist/file-lock.d.ts +5 -0
- package/dist/file-lock.js +187 -0
- package/dist/functions/commands.d.ts +9 -1
- package/dist/functions/commands.js +71 -29
- package/dist/functions/manifest.d.ts +1 -0
- package/dist/functions/manifest.js +36 -0
- package/dist/login.d.ts +14 -3
- package/dist/login.js +60 -40
- package/dist/main.d.ts +2 -1
- package/dist/main.js +36 -12
- package/dist/orm/delegate.d.ts +9 -0
- package/dist/orm/delegate.js +36 -4
- package/dist/pat-hints.js +2 -2
- package/dist/pipeline/commands.d.ts +58 -0
- package/dist/pipeline/commands.js +330 -0
- package/dist/pipeline/lifecycle.d.ts +58 -0
- package/dist/pipeline/lifecycle.js +348 -0
- package/dist/pipeline/pinning.d.ts +5 -0
- package/dist/pipeline/pinning.js +9 -0
- package/dist/pipeline/templates.d.ts +11 -0
- package/dist/pipeline/templates.js +166 -0
- package/dist/process/build.d.ts +12 -1
- package/dist/process/build.js +49 -2
- package/dist/process/codegen.js +21 -1
- package/dist/process/commands.d.ts +19 -0
- package/dist/process/commands.js +153 -40
- package/dist/process/compiler-subprocess.d.ts +29 -0
- package/dist/process/compiler-subprocess.js +99 -0
- package/dist/process/compiler-worker.d.ts +1 -0
- package/dist/process/compiler-worker.js +38 -0
- package/dist/process/discover.d.ts +4 -1
- package/dist/process/discover.js +5 -2
- package/dist/process/lint.d.ts +8 -0
- package/dist/process/lint.js +125 -29
- package/dist/process/repo-install.d.ts +21 -0
- package/dist/process/repo-install.js +99 -0
- package/dist/process/simulate.js +14 -1
- package/dist/repos/commands.d.ts +1 -1
- package/dist/repos/commands.js +17 -12
- package/dist/secrets/commands.d.ts +1 -1
- package/dist/secrets/commands.js +18 -18
- package/package.json +9 -4
package/dist/process/lint.js
CHANGED
|
@@ -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
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
414
|
+
enumValues = await loadAgentSchemaEdgeIdEnum(agentId);
|
|
368
415
|
}
|
|
369
416
|
catch (err) {
|
|
370
417
|
warnings.push({
|
|
371
418
|
process_id: processId,
|
|
372
|
-
node_id:
|
|
373
|
-
message: `could not load agent definition "${
|
|
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:
|
|
381
|
-
message: `agent "${
|
|
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:
|
|
401
|
-
message: `agent "${
|
|
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
|
+
}
|
package/dist/process/simulate.js
CHANGED
|
@@ -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
|
/**
|
package/dist/repos/commands.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/repos/commands.js
CHANGED
|
@@ -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>
|
|
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>
|
|
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
|
|
492
|
-
seq-studio repos namespaces [create <slug>]
|
|
493
|
-
seq-studio repos show <ns>/<name>
|
|
494
|
-
seq-studio repos create <ns>/<name>
|
|
495
|
-
seq-studio repos clone <ns>/<name>
|
|
496
|
-
seq-studio repos clone --url <https://…/repos/<id>/git>
|
|
497
|
-
seq-studio repos clone --id <uuid>
|
|
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>
|
|
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>
|
|
507
|
+
seq-studio repos delete <ns>/<name> -e <env> [--yes] delete a repo (confirm prompt)
|
|
503
508
|
|
|
504
|
-
Flags: -e/--env <env>
|
|
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
|
|
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>;
|
package/dist/secrets/commands.js
CHANGED
|
@@ -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>
|
|
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>
|
|
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
|
|
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>
|
|
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]
|
|
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]
|
|
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
|
|
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
|
|
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>
|
|
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.
|
|
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.
|
|
44
|
-
"@sequenceholdings/lattice": "0.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"
|