@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/codegen.js
CHANGED
|
@@ -50,7 +50,16 @@ export function generateProcessFiles(process, bundle) {
|
|
|
50
50
|
? ` max_concurrent_runs: ${process.max_concurrent_runs},`
|
|
51
51
|
: null,
|
|
52
52
|
process.supervisor_retry !== undefined ? ` supervisor_retry: ${pretty(process.supervisor_retry, 1)},` : null,
|
|
53
|
-
process.run_as !== undefined
|
|
53
|
+
process.run_as !== undefined
|
|
54
|
+
? ` run_as: { ${[
|
|
55
|
+
process.run_as.email !== undefined ? `email: ${lit(process.run_as.email)}` : null,
|
|
56
|
+
process.run_as.service_account !== undefined
|
|
57
|
+
? `service_account: ${lit(process.run_as.service_account)}`
|
|
58
|
+
: null,
|
|
59
|
+
]
|
|
60
|
+
.filter((f) => f !== null)
|
|
61
|
+
.join(', ')} },`
|
|
62
|
+
: null,
|
|
54
63
|
Array.isArray(process.tags) && process.tags.length > 0
|
|
55
64
|
? ` tags: ${pretty(process.tags, 1)},`
|
|
56
65
|
: null,
|
|
@@ -125,6 +134,7 @@ function emitNode(node, ctx) {
|
|
|
125
134
|
` function: ${lit(String(m.registered_function_id))},`,
|
|
126
135
|
m.config !== undefined ? ` config: ${pretty(m.config, 1)},` : null,
|
|
127
136
|
m.limits !== undefined ? ` limits: ${pretty(m.limits, 1)},` : null,
|
|
137
|
+
runAsField(m.run_as),
|
|
128
138
|
bindings(m.bindings),
|
|
129
139
|
middleware(m.middleware),
|
|
130
140
|
mapper('input', m.input_mapper_source),
|
|
@@ -171,6 +181,8 @@ function emitNode(node, ctx) {
|
|
|
171
181
|
par.supervisor_retry !== undefined ? ` supervisor_retry: ${pretty(par.supervisor_retry, 1)},` : null,
|
|
172
182
|
bindings(m.bindings),
|
|
173
183
|
` on_branch_error: ${lit(par.on_branch_error)},`,
|
|
184
|
+
// Only 'eager' is ever serialized (barrier is the omitted default).
|
|
185
|
+
par.join_mode === 'eager' ? ` join_mode: ${lit('eager')},` : null,
|
|
174
186
|
` join: ${par.join_source},`,
|
|
175
187
|
mapper('input', m.input_mapper_source),
|
|
176
188
|
tail,
|
|
@@ -187,6 +199,7 @@ function emitNode(node, ctx) {
|
|
|
187
199
|
` function: ${lit(String(m.function_id))},`,
|
|
188
200
|
m.version ? ` version: ${lit(String(m.version))},` : null,
|
|
189
201
|
m.limits !== undefined ? ` limits: ${pretty(m.limits, 1)},` : null,
|
|
202
|
+
runAsField(m.run_as),
|
|
190
203
|
bindings(m.bindings),
|
|
191
204
|
middleware(m.middleware),
|
|
192
205
|
mapper('input', m.input_mapper_source),
|
|
@@ -207,6 +220,13 @@ function emitEdges(edges) {
|
|
|
207
220
|
const items = edges.map((e) => `{ id: ${lit(e.id)}, to: ${lit(e.to)} }`).join(', ');
|
|
208
221
|
return `[${items}] as const`;
|
|
209
222
|
}
|
|
223
|
+
/** Per-node service-account override — `metadata.run_as.service_account`. */
|
|
224
|
+
function runAsField(runAs) {
|
|
225
|
+
const sa = runAs?.service_account;
|
|
226
|
+
if (typeof sa !== 'string' || sa.length === 0)
|
|
227
|
+
return null;
|
|
228
|
+
return ` run_as: { service_account: ${lit(sa)} },`;
|
|
229
|
+
}
|
|
210
230
|
/** A mapper field whose value is a serialized `Function.toString()` string. */
|
|
211
231
|
function mapper(field, source) {
|
|
212
232
|
if (typeof source !== 'string' || source.length === 0)
|
|
@@ -9,6 +9,25 @@ export interface ParsedArgs {
|
|
|
9
9
|
flags: Record<string, string | true>;
|
|
10
10
|
}
|
|
11
11
|
export declare function parseArgs(rest: string[]): ParsedArgs;
|
|
12
|
+
export type ProcessSourceSpec = {
|
|
13
|
+
kind: 'local';
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'git-service';
|
|
16
|
+
namespace: string;
|
|
17
|
+
name: string;
|
|
18
|
+
ref?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Parse the process-apply source flags. `--repo processes/<name>` materializes
|
|
22
|
+
* from the platform git service; absent means local (cwd / LATTICE_PROCESSES_ROOT).
|
|
23
|
+
* `--git-url` is passed through to the underlying `resolveArtifactSource` when
|
|
24
|
+
* the repo flag is a full ns/name pair (future); today only git-service is
|
|
25
|
+
* supported.
|
|
26
|
+
*
|
|
27
|
+
* Process repos live exclusively in the `processes` namespace — other namespaces
|
|
28
|
+
* (artifacts, managed-functions) are rejected to avoid cross-kind accidents.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseProcessSourceSpec(flags: Record<string, string | true>): ProcessSourceSpec;
|
|
12
31
|
export declare function initCommand(args: ParsedArgs): Promise<number>;
|
|
13
32
|
export declare function lintCommand(args: ParsedArgs): Promise<number>;
|
|
14
33
|
export declare function planCommand(args: ParsedArgs): Promise<number>;
|
package/dist/process/commands.js
CHANGED
|
@@ -11,16 +11,19 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
import { forEachSerializedNode, } from '@sequenceholdings/lattice/bundle';
|
|
12
12
|
import { getJson, getJsonOr404, postJson, AtlasApiError } from '../atlas-client.js';
|
|
13
13
|
import { generateProcessFiles } from './codegen.js';
|
|
14
|
-
import { getAccessToken, NotLoggedInError } from '../auth.js';
|
|
14
|
+
import { getAccessToken, getAccessTokenWithMode, NotLoggedInError, } from '../auth.js';
|
|
15
15
|
import { readConfig, resolveEnvWithDiscovery } from '../config.js';
|
|
16
|
-
import {
|
|
16
|
+
import { finalizeCompiledBundle, summarizeBundle } from './build.js';
|
|
17
17
|
import { buildResolveProcessPinFromEnv } from './resolve-process-pin.js';
|
|
18
18
|
import { loadBundleForPublish } from './local-bundle.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { findProcessRoot } from './discover.js';
|
|
20
|
+
import { formatLintResult, lintSerializedAgentContracts, } from './lint.js';
|
|
21
21
|
import { diffBundleAgainstActive, formatPlanDiff, } from './plan-diff.js';
|
|
22
|
-
import {
|
|
22
|
+
import { formatSimulateResult } from './simulate.js';
|
|
23
23
|
import { buildAgentSchemaLoader } from './agent-loader.js';
|
|
24
|
+
import { localGitMetadata, resolveArtifactSource, } from '@sequenceholdings/artifact-studio/source-resolver';
|
|
25
|
+
import { prepareProcessBuildRoot } from './repo-install.js';
|
|
26
|
+
import { compileProcessSourceIsolated, simulateProcessIsolated, } from './compiler-subprocess.js';
|
|
24
27
|
export function parseArgs(rest) {
|
|
25
28
|
const positional = [];
|
|
26
29
|
const flags = {};
|
|
@@ -96,7 +99,7 @@ async function getEnvAndToken(args) {
|
|
|
96
99
|
const env = await resolveEnvWithDiscovery({ requested });
|
|
97
100
|
let token;
|
|
98
101
|
try {
|
|
99
|
-
token = await getAccessToken();
|
|
102
|
+
token = await getAccessToken({ env: env.name, targetUrl: env.url });
|
|
100
103
|
}
|
|
101
104
|
catch (err) {
|
|
102
105
|
if (err instanceof NotLoggedInError) {
|
|
@@ -106,6 +109,45 @@ async function getEnvAndToken(args) {
|
|
|
106
109
|
}
|
|
107
110
|
return { env, token };
|
|
108
111
|
}
|
|
112
|
+
async function getEnvAndAuth(args) {
|
|
113
|
+
const requested = typeof args.flags.env === 'string' ? args.flags.env : undefined;
|
|
114
|
+
const env = await resolveEnvWithDiscovery({ requested });
|
|
115
|
+
const auth = await getAccessTokenWithMode({ env: env.name, targetUrl: env.url });
|
|
116
|
+
return { authMode: auth.authMode, env, token: auth.token };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse the process-apply source flags. `--repo processes/<name>` materializes
|
|
120
|
+
* from the platform git service; absent means local (cwd / LATTICE_PROCESSES_ROOT).
|
|
121
|
+
* `--git-url` is passed through to the underlying `resolveArtifactSource` when
|
|
122
|
+
* the repo flag is a full ns/name pair (future); today only git-service is
|
|
123
|
+
* supported.
|
|
124
|
+
*
|
|
125
|
+
* Process repos live exclusively in the `processes` namespace — other namespaces
|
|
126
|
+
* (artifacts, managed-functions) are rejected to avoid cross-kind accidents.
|
|
127
|
+
*/
|
|
128
|
+
export function parseProcessSourceSpec(flags) {
|
|
129
|
+
const repo = typeof flags.repo === 'string' ? flags.repo : undefined;
|
|
130
|
+
const ref = typeof flags.ref === 'string' ? flags.ref : undefined;
|
|
131
|
+
if (flags.repo === true)
|
|
132
|
+
throw new Error('--repo requires a value: --repo processes/<name>');
|
|
133
|
+
if (flags.ref === true)
|
|
134
|
+
throw new Error('--ref requires a value.');
|
|
135
|
+
if (ref && !repo)
|
|
136
|
+
throw new Error('--ref only applies together with --repo.');
|
|
137
|
+
if (ref && ref.startsWith('-'))
|
|
138
|
+
throw new Error(`--ref must not start with "-" (got "${ref}").`);
|
|
139
|
+
if (!repo)
|
|
140
|
+
return { kind: 'local' };
|
|
141
|
+
const parts = repo.split('/');
|
|
142
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
143
|
+
throw new Error(`--repo must be "processes/<name>" (got "${repo}").`);
|
|
144
|
+
}
|
|
145
|
+
if (parts[0] !== 'processes') {
|
|
146
|
+
throw new Error(`Process deploys require the "processes" namespace — got "${parts[0]}". ` +
|
|
147
|
+
`Use --repo processes/<name>.`);
|
|
148
|
+
}
|
|
149
|
+
return { kind: 'git-service', namespace: parts[0], name: parts[1], ref };
|
|
150
|
+
}
|
|
109
151
|
// ---------------------------------------------------------------------------
|
|
110
152
|
// init
|
|
111
153
|
// ---------------------------------------------------------------------------
|
|
@@ -171,9 +213,11 @@ function slugify(value) {
|
|
|
171
213
|
// lint
|
|
172
214
|
// ---------------------------------------------------------------------------
|
|
173
215
|
export async function lintCommand(args) {
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
216
|
+
const compiled = await compileLocalProcessSource();
|
|
217
|
+
const result = await lintCompiledProcessSource({
|
|
218
|
+
compiled,
|
|
219
|
+
loadAgentSchemaEdgeIdEnum: await tryBuildAgentLoader(args),
|
|
220
|
+
});
|
|
177
221
|
const formatted = formatLintResult(result);
|
|
178
222
|
if (formatted.text)
|
|
179
223
|
console.log(formatted.text);
|
|
@@ -181,9 +225,29 @@ export async function lintCommand(args) {
|
|
|
181
225
|
console.error(`\nlint FAILED — ${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
|
|
182
226
|
return 1;
|
|
183
227
|
}
|
|
184
|
-
console.log(`\nlint ok — ${
|
|
228
|
+
console.log(`\nlint ok — ${compiled.bundle.processes.length} process(es) checked, ${result.warnings.length} warning(s)`);
|
|
185
229
|
return 0;
|
|
186
230
|
}
|
|
231
|
+
async function compileLocalProcessSource() {
|
|
232
|
+
const rootDir = await findProcessRoot();
|
|
233
|
+
return compileProcessSourceIsolated({
|
|
234
|
+
rootDir,
|
|
235
|
+
provenance: localGitMetadata(rootDir),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async function lintCompiledProcessSource({ compiled, loadAgentSchemaEdgeIdEnum, }) {
|
|
239
|
+
const agentLint = await lintSerializedAgentContracts({
|
|
240
|
+
bundle: compiled.bundle,
|
|
241
|
+
loadAgentSchemaEdgeIdEnum,
|
|
242
|
+
// The worker already performs source-local checks. The credential-owning
|
|
243
|
+
// parent only adds registry-dependent validation (or the offline warning).
|
|
244
|
+
includeStaticValidation: false,
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
errors: [...compiled.lint.errors, ...agentLint.errors],
|
|
248
|
+
warnings: [...compiled.lint.warnings, ...agentLint.warnings],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
187
251
|
async function tryBuildAgentLoader(args) {
|
|
188
252
|
// Agent loader is only meaningful when we have a target env + token.
|
|
189
253
|
// If either fails to resolve, skip the check; lint will emit a warning
|
|
@@ -199,23 +263,31 @@ async function tryBuildAgentLoader(args) {
|
|
|
199
263
|
// ---------------------------------------------------------------------------
|
|
200
264
|
// plan
|
|
201
265
|
// ---------------------------------------------------------------------------
|
|
202
|
-
async function buildBundleForPublish(args,
|
|
203
|
-
|
|
266
|
+
async function buildBundleForPublish(args, compiled) {
|
|
267
|
+
let hasSubprocess = false;
|
|
268
|
+
for (const process of compiled.bundle.processes) {
|
|
269
|
+
forEachSerializedNode(process.nodes, (node) => {
|
|
270
|
+
if (node.kind === 'subprocess')
|
|
271
|
+
hasSubprocess = true;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (!hasSubprocess)
|
|
275
|
+
return compiled.bundle;
|
|
204
276
|
try {
|
|
205
277
|
const { env, token } = await getEnvAndToken(args);
|
|
206
278
|
const resolveProcessPin = buildResolveProcessPinFromEnv(env, token);
|
|
207
|
-
return await
|
|
279
|
+
return await finalizeCompiledBundle({
|
|
280
|
+
bundle: compiled.bundle,
|
|
281
|
+
resolveProcessPin,
|
|
282
|
+
});
|
|
208
283
|
}
|
|
209
284
|
catch (err) {
|
|
210
|
-
|
|
211
|
-
throw new Error('subprocess nodes require -e <env> and `seq-studio login` to resolve child process versions', { cause: err });
|
|
212
|
-
}
|
|
213
|
-
return await buildBundleFromProcesses(defs);
|
|
285
|
+
throw new Error('subprocess nodes require -e <env> and `seq-studio login` to resolve child process versions', { cause: err });
|
|
214
286
|
}
|
|
215
287
|
}
|
|
216
288
|
export async function planCommand(args) {
|
|
217
|
-
const
|
|
218
|
-
const bundle = await buildBundleForPublish(args,
|
|
289
|
+
const compiled = await compileLocalProcessSource();
|
|
290
|
+
const bundle = await buildBundleForPublish(args, compiled);
|
|
219
291
|
const summary = summarizeBundle(bundle);
|
|
220
292
|
console.log(JSON.stringify(summary, null, 2));
|
|
221
293
|
const loadActiveProcess = await tryBuildActiveProcessLoader(args);
|
|
@@ -267,9 +339,11 @@ async function tryBuildActiveProcessLoader(args) {
|
|
|
267
339
|
// test (CI wrapper)
|
|
268
340
|
// ---------------------------------------------------------------------------
|
|
269
341
|
export async function testCommand(args) {
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
342
|
+
const compiled = await compileLocalProcessSource();
|
|
343
|
+
const lintResult = await lintCompiledProcessSource({
|
|
344
|
+
compiled,
|
|
345
|
+
loadAgentSchemaEdgeIdEnum: await tryBuildAgentLoader(args),
|
|
346
|
+
});
|
|
273
347
|
const formattedLint = formatLintResult(lintResult);
|
|
274
348
|
if (formattedLint.text)
|
|
275
349
|
console.log(formattedLint.text);
|
|
@@ -277,8 +351,8 @@ export async function testCommand(args) {
|
|
|
277
351
|
console.error(`\nlint FAILED — ${lintResult.errors.length} error(s), ${lintResult.warnings.length} warning(s)`);
|
|
278
352
|
return 1;
|
|
279
353
|
}
|
|
280
|
-
console.log(`\nlint ok — ${
|
|
281
|
-
const bundle = await buildBundleForPublish(args,
|
|
354
|
+
console.log(`\nlint ok — ${compiled.bundle.processes.length} process(es) checked, ${lintResult.warnings.length} warning(s)`);
|
|
355
|
+
const bundle = await buildBundleForPublish(args, compiled);
|
|
282
356
|
const offline = args.flags.offline === true;
|
|
283
357
|
const loadActiveProcess = await tryBuildActiveProcessLoader(args);
|
|
284
358
|
if (!loadActiveProcess) {
|
|
@@ -334,11 +408,41 @@ export function resolveOnlyIds({ only, knownIds, }) {
|
|
|
334
408
|
return { ids };
|
|
335
409
|
}
|
|
336
410
|
export async function applyCommand(args) {
|
|
411
|
+
const spec = parseProcessSourceSpec(args.flags);
|
|
412
|
+
if (spec.kind === 'git-service') {
|
|
413
|
+
return applyFromRepo(args, spec);
|
|
414
|
+
}
|
|
415
|
+
return applyLocal(args);
|
|
416
|
+
}
|
|
417
|
+
async function applyLocal(args) {
|
|
418
|
+
const compiled = await compileLocalProcessSource();
|
|
337
419
|
const { env, token } = await getEnvAndToken(args);
|
|
338
|
-
|
|
420
|
+
return registerAndPromote({ args, env, token, compiled });
|
|
421
|
+
}
|
|
422
|
+
async function applyFromRepo(args, spec) {
|
|
423
|
+
const { authMode, env, token } = await getEnvAndAuth(args);
|
|
424
|
+
const source = await resolveArtifactSource({ kind: 'git-service', namespace: spec.namespace, name: spec.name, ref: spec.ref }, { authMode, baseUrl: env.url, token });
|
|
425
|
+
try {
|
|
426
|
+
await prepareProcessBuildRoot(source.dir);
|
|
427
|
+
const compiled = await compileProcessSourceIsolated({
|
|
428
|
+
rootDir: source.dir,
|
|
429
|
+
provenance: source.provenance,
|
|
430
|
+
});
|
|
431
|
+
return await registerAndPromote({
|
|
432
|
+
args,
|
|
433
|
+
env,
|
|
434
|
+
token,
|
|
435
|
+
compiled,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
await source.cleanup();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function registerAndPromote({ args, env, token, compiled, }) {
|
|
339
443
|
const onlyResult = resolveOnlyIds({
|
|
340
444
|
only: args.flags.only,
|
|
341
|
-
knownIds: new Set(
|
|
445
|
+
knownIds: new Set(compiled.bundle.processes.map((process) => process.id)),
|
|
342
446
|
});
|
|
343
447
|
if (onlyResult.error) {
|
|
344
448
|
console.error(onlyResult.error);
|
|
@@ -349,7 +453,10 @@ export async function applyCommand(args) {
|
|
|
349
453
|
// again on POST /bundles; this is a local fast-fail so we don't ship
|
|
350
454
|
// a bundle that will be rejected.
|
|
351
455
|
const loadAgentSchemaEdgeIdEnum = buildAgentSchemaLoader({ baseUrl: env.url, token });
|
|
352
|
-
const lintResult = await
|
|
456
|
+
const lintResult = await lintCompiledProcessSource({
|
|
457
|
+
compiled,
|
|
458
|
+
loadAgentSchemaEdgeIdEnum,
|
|
459
|
+
});
|
|
353
460
|
const formatted = formatLintResult(lintResult);
|
|
354
461
|
if (formatted.text)
|
|
355
462
|
console.log(formatted.text);
|
|
@@ -357,7 +464,7 @@ export async function applyCommand(args) {
|
|
|
357
464
|
console.error('\napply FAILED — lint rejected the bundle');
|
|
358
465
|
return 1;
|
|
359
466
|
}
|
|
360
|
-
const bundle = await buildBundleForPublish(args,
|
|
467
|
+
const bundle = await buildBundleForPublish(args, compiled);
|
|
361
468
|
console.log(`[seq-studio] built bundle ${bundle.bundle_hash.slice(0, 12)} ` +
|
|
362
469
|
`(${bundle.processes.length} process(es))`);
|
|
363
470
|
// We send only the bundle. `created_by` (and `promoted_by` for the
|
|
@@ -506,8 +613,8 @@ export async function bundleCommand(args) {
|
|
|
506
613
|
const sub = args.positional[0];
|
|
507
614
|
switch (sub) {
|
|
508
615
|
case 'build': {
|
|
509
|
-
const
|
|
510
|
-
const bundle = await buildBundleForPublish(args,
|
|
616
|
+
const compiled = await compileLocalProcessSource();
|
|
617
|
+
const bundle = await buildBundleForPublish(args, compiled);
|
|
511
618
|
const out = typeof args.flags.out === 'string' ? resolve(args.flags.out) : null;
|
|
512
619
|
if (out) {
|
|
513
620
|
await mkdir(dirname(out), { recursive: true });
|
|
@@ -642,14 +749,15 @@ export async function simulateCommand(args) {
|
|
|
642
749
|
console.error('usage: seq-studio process simulate <process-id>');
|
|
643
750
|
return 1;
|
|
644
751
|
}
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
752
|
+
const simulation = await simulateProcessIsolated({
|
|
753
|
+
rootDir: await findProcessRoot(),
|
|
754
|
+
processId,
|
|
755
|
+
});
|
|
756
|
+
if (simulation.kind === 'not-found') {
|
|
757
|
+
console.error(`process "${simulation.processId}" not found in lattice process roots`);
|
|
649
758
|
return 1;
|
|
650
759
|
}
|
|
651
|
-
const
|
|
652
|
-
const result = await simulateProcess({ process: def.process, processesById });
|
|
760
|
+
const result = simulation.result;
|
|
653
761
|
console.log(formatSimulateResult(result));
|
|
654
762
|
return result.status === 'failed' ? 1 : 0;
|
|
655
763
|
}
|
|
@@ -668,7 +776,7 @@ export async function doctorCommand(args) {
|
|
|
668
776
|
lines.push(`config: ok — default_env=${config.defaultEnv}, envs=${Object.keys(config.envs).join(',')}`);
|
|
669
777
|
lines.push(`tier: ${config.tier}` +
|
|
670
778
|
(config.tier === 'anonymous'
|
|
671
|
-
? ' —
|
|
779
|
+
? ' — no environment catalog cached; run `seq-studio envs refresh` to discover more environments'
|
|
672
780
|
: ''));
|
|
673
781
|
}
|
|
674
782
|
const requested = typeof args.flags.env === 'string' ? args.flags.env : undefined;
|
|
@@ -685,7 +793,7 @@ export async function doctorCommand(args) {
|
|
|
685
793
|
}
|
|
686
794
|
let token = null;
|
|
687
795
|
try {
|
|
688
|
-
token = await getAccessToken();
|
|
796
|
+
token = await getAccessToken({ env: env?.name, targetUrl: env?.url });
|
|
689
797
|
lines.push('auth: ok — seqapi token present and not expired');
|
|
690
798
|
}
|
|
691
799
|
catch (err) {
|
|
@@ -749,9 +857,14 @@ export async function doctorCommand(args) {
|
|
|
749
857
|
const currentTotal = current.total ?? current.items.length;
|
|
750
858
|
if (currentTotal === 0 && env.name !== 'staging' && config?.envs.staging?.url) {
|
|
751
859
|
try {
|
|
860
|
+
const stagingUrl = config.envs.staging.url;
|
|
861
|
+
const stagingToken = await getAccessToken({
|
|
862
|
+
env: 'staging',
|
|
863
|
+
targetUrl: stagingUrl,
|
|
864
|
+
});
|
|
752
865
|
const staging = await getJson({
|
|
753
|
-
baseUrl:
|
|
754
|
-
token,
|
|
866
|
+
baseUrl: stagingUrl,
|
|
867
|
+
token: stagingToken,
|
|
755
868
|
path: '/api/git-service/repos?limit=1&offset=0',
|
|
756
869
|
});
|
|
757
870
|
const stagingTotal = staging.total ?? staging.items.length;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { LatticeBundle } from '@sequenceholdings/lattice/bundle';
|
|
2
|
+
import type { BuildBundleOptions } from './build.js';
|
|
3
|
+
import type { LintResult } from './lint.js';
|
|
4
|
+
import type { SimulateResult } from './simulate.js';
|
|
5
|
+
export interface CompiledProcessSource {
|
|
6
|
+
bundle: LatticeBundle;
|
|
7
|
+
lint: LintResult;
|
|
8
|
+
}
|
|
9
|
+
export type IsolatedSimulation = {
|
|
10
|
+
kind: 'not-found';
|
|
11
|
+
processId: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'result';
|
|
14
|
+
result: SimulateResult;
|
|
15
|
+
};
|
|
16
|
+
export declare function compileProcessSourceIsolated({ rootDir, provenance, workerPath, timeoutMs, sourceEnv, }: {
|
|
17
|
+
rootDir: string;
|
|
18
|
+
provenance?: NonNullable<BuildBundleOptions['provenance']>;
|
|
19
|
+
workerPath?: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
sourceEnv?: NodeJS.ProcessEnv;
|
|
22
|
+
}): Promise<CompiledProcessSource>;
|
|
23
|
+
export declare function simulateProcessIsolated({ rootDir, processId, workerPath, timeoutMs, sourceEnv, }: {
|
|
24
|
+
rootDir: string;
|
|
25
|
+
processId: string;
|
|
26
|
+
workerPath?: string;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
sourceEnv?: NodeJS.ProcessEnv;
|
|
29
|
+
}): Promise<IsolatedSimulation>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { createScrubbedChildEnv } from '@sequenceholdings/artifact-studio/child-environment';
|
|
8
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const BUILT_WORKER_PATH = join(MODULE_DIR, 'compiler-worker.js');
|
|
10
|
+
const DEFAULT_WORKER_PATH = existsSync(BUILT_WORKER_PATH)
|
|
11
|
+
? BUILT_WORKER_PATH
|
|
12
|
+
: join(MODULE_DIR, 'compiler-worker.ts');
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 2 * 60_000;
|
|
14
|
+
const MAX_STDERR_BYTES = 1024 * 1024;
|
|
15
|
+
export async function compileProcessSourceIsolated({ rootDir, provenance, workerPath = DEFAULT_WORKER_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, sourceEnv, }) {
|
|
16
|
+
return runProcessWorker({
|
|
17
|
+
request: {
|
|
18
|
+
action: 'compile',
|
|
19
|
+
rootDir,
|
|
20
|
+
...(provenance ? { provenance } : {}),
|
|
21
|
+
},
|
|
22
|
+
workerPath,
|
|
23
|
+
timeoutMs,
|
|
24
|
+
...(sourceEnv ? { sourceEnv } : {}),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export async function simulateProcessIsolated({ rootDir, processId, workerPath = DEFAULT_WORKER_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, sourceEnv, }) {
|
|
28
|
+
return runProcessWorker({
|
|
29
|
+
request: { action: 'simulate', rootDir, processId },
|
|
30
|
+
workerPath,
|
|
31
|
+
timeoutMs,
|
|
32
|
+
...(sourceEnv ? { sourceEnv } : {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function runProcessWorker({ request, workerPath, timeoutMs, sourceEnv, }) {
|
|
36
|
+
const scratch = await mkdtemp(join(tmpdir(), 'process-compiler-'));
|
|
37
|
+
const requestFile = join(scratch, 'request.json');
|
|
38
|
+
const outFile = join(scratch, 'result.json');
|
|
39
|
+
const homeDir = join(scratch, 'home');
|
|
40
|
+
try {
|
|
41
|
+
await mkdir(homeDir, { recursive: true });
|
|
42
|
+
await writeFile(requestFile, JSON.stringify(request), 'utf8');
|
|
43
|
+
await runWorker({
|
|
44
|
+
workerPath,
|
|
45
|
+
requestFile,
|
|
46
|
+
outFile,
|
|
47
|
+
homeDir,
|
|
48
|
+
timeoutMs,
|
|
49
|
+
...(sourceEnv ? { sourceEnv } : {}),
|
|
50
|
+
});
|
|
51
|
+
return JSON.parse(await readFile(outFile, 'utf8'));
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await rm(scratch, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function runWorker({ workerPath, requestFile, outFile, homeDir, timeoutMs, sourceEnv, }) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const workerArgs = workerPath.endsWith('.ts')
|
|
60
|
+
? ['--import', 'tsx', workerPath, requestFile, outFile]
|
|
61
|
+
: [workerPath, requestFile, outFile];
|
|
62
|
+
const child = spawn(process.execPath, workerArgs, {
|
|
63
|
+
stdio: ['ignore', 'inherit', 'pipe'],
|
|
64
|
+
env: createScrubbedChildEnv({
|
|
65
|
+
homeDir,
|
|
66
|
+
...(sourceEnv ? { source: sourceEnv } : {}),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
let stderr = '';
|
|
70
|
+
let timedOut = false;
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
timedOut = true;
|
|
73
|
+
child.kill('SIGKILL');
|
|
74
|
+
}, timeoutMs);
|
|
75
|
+
child.stderr.on('data', (chunk) => {
|
|
76
|
+
if (stderr.length < MAX_STDERR_BYTES) {
|
|
77
|
+
stderr += chunk.toString().slice(0, MAX_STDERR_BYTES - stderr.length);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
child.on('error', (error) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
reject(error);
|
|
83
|
+
});
|
|
84
|
+
child.on('close', (code) => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
if (timedOut) {
|
|
87
|
+
reject(new Error(`process compiler exceeded ${Math.round(timeoutMs / 1000)}s and was terminated`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (code === 0) {
|
|
91
|
+
if (stderr.trim())
|
|
92
|
+
process.stderr.write(stderr);
|
|
93
|
+
resolve();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
reject(new Error(stderr.trim() || `process compiler exited with code ${code ?? 'null'}`));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { buildBundleFromProcesses } from './build.js';
|
|
3
|
+
import { loadProcessDefinitions } from './discover.js';
|
|
4
|
+
import { lintProcesses } from './lint.js';
|
|
5
|
+
import { simulateProcess } from './simulate.js';
|
|
6
|
+
async function main() {
|
|
7
|
+
const [requestFile, outFile] = process.argv.slice(2);
|
|
8
|
+
if (!requestFile || !outFile) {
|
|
9
|
+
process.stderr.write('usage: compiler-worker <request.json> <result.json>\n');
|
|
10
|
+
process.exitCode = 2;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const request = JSON.parse(await readFile(requestFile, 'utf8'));
|
|
14
|
+
const defs = await loadProcessDefinitions(request.rootDir);
|
|
15
|
+
if (request.action === 'simulate') {
|
|
16
|
+
const process = defs.find((definition) => definition.process.id === request.processId)?.process;
|
|
17
|
+
if (!process) {
|
|
18
|
+
await writeFile(outFile, JSON.stringify({ kind: 'not-found', processId: request.processId }), 'utf8');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const processesById = new Map(defs.map((definition) => [definition.process.id, definition.process]));
|
|
22
|
+
const result = await simulateProcess({ process, processesById });
|
|
23
|
+
await writeFile(outFile, JSON.stringify({ kind: 'result', result }), 'utf8');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const lint = await lintProcesses({
|
|
27
|
+
defs,
|
|
28
|
+
suppressUnavailableAgentWarnings: true,
|
|
29
|
+
});
|
|
30
|
+
const bundle = await buildBundleFromProcesses(defs, {
|
|
31
|
+
...(request.provenance ? { provenance: request.provenance } : {}),
|
|
32
|
+
});
|
|
33
|
+
await writeFile(outFile, JSON.stringify({ bundle, lint }), 'utf8');
|
|
34
|
+
}
|
|
35
|
+
main().catch((error) => {
|
|
36
|
+
process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
});
|
|
@@ -28,5 +28,8 @@ export declare function findProcessRoot(): Promise<string>;
|
|
|
28
28
|
* Load every `<dir>/process.ts` under the configured root. Each module
|
|
29
29
|
* must `export default defineProcess(...)`. Process ids must be unique
|
|
30
30
|
* across the set — duplicates throw.
|
|
31
|
+
*
|
|
32
|
+
* @param explicitRoot When supplied, overrides LATTICE_PROCESSES_ROOT and cwd.
|
|
33
|
+
* Used by `--repo` deploys to point discovery at the materialized temp tree.
|
|
31
34
|
*/
|
|
32
|
-
export declare function loadProcessDefinitions(): Promise<readonly LoadedProcess[]>;
|
|
35
|
+
export declare function loadProcessDefinitions(explicitRoot?: string): Promise<readonly LoadedProcess[]>;
|
package/dist/process/discover.js
CHANGED
|
@@ -55,9 +55,12 @@ export async function findProcessRoot() {
|
|
|
55
55
|
* Load every `<dir>/process.ts` under the configured root. Each module
|
|
56
56
|
* must `export default defineProcess(...)`. Process ids must be unique
|
|
57
57
|
* across the set — duplicates throw.
|
|
58
|
+
*
|
|
59
|
+
* @param explicitRoot When supplied, overrides LATTICE_PROCESSES_ROOT and cwd.
|
|
60
|
+
* Used by `--repo` deploys to point discovery at the materialized temp tree.
|
|
58
61
|
*/
|
|
59
|
-
export async function loadProcessDefinitions() {
|
|
60
|
-
const root = await findProcessRoot();
|
|
62
|
+
export async function loadProcessDefinitions(explicitRoot) {
|
|
63
|
+
const root = explicitRoot ?? (await findProcessRoot());
|
|
61
64
|
const entries = await readdir(root);
|
|
62
65
|
const found = [];
|
|
63
66
|
const seenIds = new Set();
|
package/dist/process/lint.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - Human timeout edge — if timeout is set and outgoing_edges.length > 0
|
|
10
10
|
* but on_timeout_edge_id is unset, warn
|
|
11
11
|
*/
|
|
12
|
+
import { type LatticeBundle } from '@sequenceholdings/lattice/bundle';
|
|
12
13
|
import type { ProcessDefinition } from '@sequenceholdings/lattice/define';
|
|
13
14
|
export interface LintIssue {
|
|
14
15
|
process_id: string;
|
|
@@ -31,8 +32,15 @@ export interface LintInput {
|
|
|
31
32
|
* undefined and a warning is emitted instead of failing the build.
|
|
32
33
|
*/
|
|
33
34
|
loadAgentSchemaEdgeIdEnum?: (agentId: string) => Promise<string[] | null | 'not-found'>;
|
|
35
|
+
/** A scrubbed worker can defer registry-backed agent checks to its parent. */
|
|
36
|
+
suppressUnavailableAgentWarnings?: boolean;
|
|
34
37
|
}
|
|
35
38
|
export declare function lintProcesses(input: LintInput): Promise<LintResult>;
|
|
39
|
+
export declare function lintSerializedAgentContracts({ bundle, loadAgentSchemaEdgeIdEnum, includeStaticValidation, }: {
|
|
40
|
+
bundle: LatticeBundle;
|
|
41
|
+
loadAgentSchemaEdgeIdEnum?: LintInput['loadAgentSchemaEdgeIdEnum'];
|
|
42
|
+
includeStaticValidation?: boolean;
|
|
43
|
+
}): Promise<LintResult>;
|
|
36
44
|
export declare function formatLintResult(result: LintResult): {
|
|
37
45
|
text: string;
|
|
38
46
|
hasErrors: boolean;
|