@sequenceholdings/studio-cli 0.1.12 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +116 -30
  2. package/dist/artifact/delegate.d.ts +2 -2
  3. package/dist/artifact/delegate.js +31 -73
  4. package/dist/atlas-client.js +52 -37
  5. package/dist/auth-cmds/commands.d.ts +1 -1
  6. package/dist/auth-cmds/commands.js +12 -7
  7. package/dist/auth.d.ts +97 -19
  8. package/dist/auth.js +376 -81
  9. package/dist/config.d.ts +13 -3
  10. package/dist/config.js +41 -14
  11. package/dist/env-catalog.js +13 -3
  12. package/dist/env-flags.d.ts +2 -0
  13. package/dist/env-flags.js +2 -0
  14. package/dist/env-registry.d.ts +27 -0
  15. package/dist/env-registry.js +204 -0
  16. package/dist/envs/commands.d.ts +1 -1
  17. package/dist/envs/commands.js +65 -10
  18. package/dist/file-lock.d.ts +5 -0
  19. package/dist/file-lock.js +187 -0
  20. package/dist/functions/commands.d.ts +9 -1
  21. package/dist/functions/commands.js +71 -29
  22. package/dist/functions/manifest.d.ts +1 -0
  23. package/dist/functions/manifest.js +36 -0
  24. package/dist/login.d.ts +14 -3
  25. package/dist/login.js +60 -40
  26. package/dist/main.d.ts +2 -1
  27. package/dist/main.js +36 -12
  28. package/dist/orm/delegate.d.ts +9 -0
  29. package/dist/orm/delegate.js +36 -4
  30. package/dist/pat-hints.js +2 -2
  31. package/dist/pipeline/commands.d.ts +58 -0
  32. package/dist/pipeline/commands.js +330 -0
  33. package/dist/pipeline/lifecycle.d.ts +58 -0
  34. package/dist/pipeline/lifecycle.js +348 -0
  35. package/dist/pipeline/pinning.d.ts +5 -0
  36. package/dist/pipeline/pinning.js +9 -0
  37. package/dist/pipeline/templates.d.ts +11 -0
  38. package/dist/pipeline/templates.js +166 -0
  39. package/dist/process/build.d.ts +12 -1
  40. package/dist/process/build.js +49 -2
  41. package/dist/process/codegen.js +21 -1
  42. package/dist/process/commands.d.ts +19 -0
  43. package/dist/process/commands.js +153 -40
  44. package/dist/process/compiler-subprocess.d.ts +29 -0
  45. package/dist/process/compiler-subprocess.js +99 -0
  46. package/dist/process/compiler-worker.d.ts +1 -0
  47. package/dist/process/compiler-worker.js +38 -0
  48. package/dist/process/discover.d.ts +4 -1
  49. package/dist/process/discover.js +5 -2
  50. package/dist/process/lint.d.ts +8 -0
  51. package/dist/process/lint.js +125 -29
  52. package/dist/process/repo-install.d.ts +21 -0
  53. package/dist/process/repo-install.js +99 -0
  54. package/dist/process/simulate.js +14 -1
  55. package/dist/repos/commands.d.ts +1 -1
  56. package/dist/repos/commands.js +17 -12
  57. package/dist/secrets/commands.d.ts +1 -1
  58. package/dist/secrets/commands.js +18 -18
  59. package/package.json +9 -4
@@ -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 ? ` run_as: { email: ${lit(process.run_as.email)} },` : null,
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>;
@@ -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 { buildBundleFromProcesses, summarizeBundle } from './build.js';
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 { loadProcessDefinitions } from './discover.js';
20
- import { lintProcesses, formatLintResult } from './lint.js';
19
+ import { findProcessRoot } from './discover.js';
20
+ import { formatLintResult, lintSerializedAgentContracts, } from './lint.js';
21
21
  import { diffBundleAgainstActive, formatPlanDiff, } from './plan-diff.js';
22
- import { simulateProcess, formatSimulateResult } from './simulate.js';
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 defs = await loadProcessDefinitions();
175
- const loadAgentSchemaEdgeIdEnum = await tryBuildAgentLoader(args);
176
- const result = await lintProcesses({ defs, loadAgentSchemaEdgeIdEnum });
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 — ${defs.length} process(es) checked, ${result.warnings.length} warning(s)`);
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, defs) {
203
- const hasSubprocess = defs.some((d) => d.process.nodes.some((n) => n.kind === 'subprocess'));
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 buildBundleFromProcesses(defs, { resolveProcessPin });
279
+ return await finalizeCompiledBundle({
280
+ bundle: compiled.bundle,
281
+ resolveProcessPin,
282
+ });
208
283
  }
209
284
  catch (err) {
210
- if (hasSubprocess) {
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 defs = await loadProcessDefinitions();
218
- const bundle = await buildBundleForPublish(args, defs);
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 defs = await loadProcessDefinitions();
271
- const loadAgentSchemaEdgeIdEnum = await tryBuildAgentLoader(args);
272
- const lintResult = await lintProcesses({ defs, loadAgentSchemaEdgeIdEnum });
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 — ${defs.length} process(es) checked, ${lintResult.warnings.length} warning(s)`);
281
- const bundle = await buildBundleForPublish(args, defs);
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
- const defs = await loadProcessDefinitions();
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(defs.map((d) => d.process.id)),
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 lintProcesses({ defs, loadAgentSchemaEdgeIdEnum });
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, defs);
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 defs = await loadProcessDefinitions();
510
- const bundle = await buildBundleForPublish(args, defs);
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 defs = await loadProcessDefinitions();
646
- const def = defs.find((d) => d.process.id === processId);
647
- if (!def) {
648
- console.error(`process "${processId}" not found in lattice process roots`);
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 processesById = new Map(defs.map((d) => [d.process.id, d.process]));
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
- ? ' — authenticate and run `seq-studio envs refresh` to discover more environments'
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: config.envs.staging.url,
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[]>;
@@ -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();
@@ -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;