@sequenceholdings/studio-cli 0.1.9
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 +258 -0
- package/dist/artifact/delegate.d.ts +25 -0
- package/dist/artifact/delegate.js +263 -0
- package/dist/atlas-client.d.ts +44 -0
- package/dist/atlas-client.js +173 -0
- package/dist/auth-cmds/commands.d.ts +15 -0
- package/dist/auth-cmds/commands.js +249 -0
- package/dist/auth.d.ts +26 -0
- package/dist/auth.js +171 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +8 -0
- package/dist/cli-errors.d.ts +5 -0
- package/dist/cli-errors.js +78 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +103 -0
- package/dist/env-flags.d.ts +8 -0
- package/dist/env-flags.js +47 -0
- package/dist/functions/bundle.d.ts +30 -0
- package/dist/functions/bundle.js +137 -0
- package/dist/functions/commands.d.ts +86 -0
- package/dist/functions/commands.js +999 -0
- package/dist/functions/egress-preview.d.ts +32 -0
- package/dist/functions/egress-preview.js +54 -0
- package/dist/functions/lockfile-origin.d.ts +16 -0
- package/dist/functions/lockfile-origin.js +45 -0
- package/dist/functions/manifest.d.ts +89 -0
- package/dist/functions/manifest.js +586 -0
- package/dist/functions/secret-reconcile.d.ts +79 -0
- package/dist/functions/secret-reconcile.js +86 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.js +129 -0
- package/dist/orm/delegate.d.ts +8 -0
- package/dist/orm/delegate.js +61 -0
- package/dist/pat-hints.d.ts +17 -0
- package/dist/pat-hints.js +28 -0
- package/dist/preview.d.ts +89 -0
- package/dist/preview.js +291 -0
- package/dist/process/agent-loader.d.ts +24 -0
- package/dist/process/agent-loader.js +57 -0
- package/dist/process/build.d.ts +14 -0
- package/dist/process/build.js +368 -0
- package/dist/process/codegen.d.ts +18 -0
- package/dist/process/codegen.js +270 -0
- package/dist/process/commands.d.ts +47 -0
- package/dist/process/commands.js +786 -0
- package/dist/process/discover.d.ts +32 -0
- package/dist/process/discover.js +131 -0
- package/dist/process/lint.d.ts +39 -0
- package/dist/process/lint.js +485 -0
- package/dist/process/local-bundle.d.ts +17 -0
- package/dist/process/local-bundle.js +65 -0
- package/dist/process/plan-diff.d.ts +82 -0
- package/dist/process/plan-diff.js +333 -0
- package/dist/process/resolve-process-pin.d.ts +11 -0
- package/dist/process/resolve-process-pin.js +63 -0
- package/dist/process/simulate.d.ts +50 -0
- package/dist/process/simulate.js +328 -0
- package/dist/prompt.d.ts +35 -0
- package/dist/prompt.js +65 -0
- package/dist/repos/commands.d.ts +49 -0
- package/dist/repos/commands.js +548 -0
- package/dist/repos/git-clone.d.ts +10 -0
- package/dist/repos/git-clone.js +49 -0
- package/dist/secrets/commands.d.ts +24 -0
- package/dist/secrets/commands.js +704 -0
- package/dist/templates/process/example-process/process.ts +43 -0
- package/dist/templates/process/package.json +23 -0
- package/dist/templates/process/pnpm-workspace.yaml +21 -0
- package/dist/templates/process/tsconfig.json +17 -0
- package/package.json +78 -0
- package/templates/process/example-process/process.ts +43 -0
- package/templates/process/package.json +23 -0
- package/templates/process/pnpm-workspace.yaml +21 -0
- package/templates/process/tsconfig.json +17 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle builder. Produces the canonical `bundle_hash` for a process
|
|
3
|
+
* source tree.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { finalizeBundleIdentity, forEachSerializedNode, } from '@sequenceholdings/lattice/bundle';
|
|
7
|
+
import { parseDurationMs, serializeRetryConfig, } from '@sequenceholdings/lattice/define';
|
|
8
|
+
export async function buildBundleFromProcesses(defs, options) {
|
|
9
|
+
if (defs.length === 0) {
|
|
10
|
+
throw new Error('no lattice process definitions found in configured roots');
|
|
11
|
+
}
|
|
12
|
+
const processes = defs.map((d) => serializeProcess({ process: d.process }));
|
|
13
|
+
const localProcessIds = new Set(processes.map((p) => p.id));
|
|
14
|
+
if (options?.resolveProcessPin) {
|
|
15
|
+
await resolveSubprocessPinsInProcesses(processes, async (processId, version) => {
|
|
16
|
+
// Child processes co-located in this bundle are pinned by
|
|
17
|
+
// finalizeBundleIdentity to this bundle's own version/hash.
|
|
18
|
+
if (!version && localProcessIds.has(processId))
|
|
19
|
+
return null;
|
|
20
|
+
return options.resolveProcessPin(processId, version);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// Manifest, hash, version, and co-located subprocess pins are all derived in
|
|
24
|
+
// the SDK so a CLI-built bundle is byte-identical to a server-applied one.
|
|
25
|
+
return finalizeBundleIdentity({ processes, metadata: collectGitMetadata() });
|
|
26
|
+
}
|
|
27
|
+
async function resolveSubprocessPinsInProcesses(processes, resolve) {
|
|
28
|
+
const pins = [];
|
|
29
|
+
for (const proc of processes) {
|
|
30
|
+
forEachSerializedNode(proc.nodes, (node) => {
|
|
31
|
+
if (node.kind !== 'subprocess')
|
|
32
|
+
return;
|
|
33
|
+
pins.push(pinSubprocessNode(proc.id, node, resolve, { allowUnresolved: true }));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
await Promise.all(pins);
|
|
37
|
+
}
|
|
38
|
+
async function pinSubprocessNode(processId, node, resolve, options) {
|
|
39
|
+
const meta = node.metadata;
|
|
40
|
+
const spec = meta.subprocess;
|
|
41
|
+
if (!spec?.process_id) {
|
|
42
|
+
throw new Error(`process "${processId}": subprocess node "${node.id}" is missing subprocess metadata`);
|
|
43
|
+
}
|
|
44
|
+
if (spec.bundle_hash && spec.version)
|
|
45
|
+
return;
|
|
46
|
+
const resolved = await resolve(spec.process_id, spec.version);
|
|
47
|
+
if (!resolved) {
|
|
48
|
+
if (options?.allowUnresolved)
|
|
49
|
+
return;
|
|
50
|
+
throw new Error(`process "${processId}": subprocess node "${node.id}" references unknown process ` +
|
|
51
|
+
`"${spec.process_id}"${spec.version ? ` version "${spec.version}"` : ''}`);
|
|
52
|
+
}
|
|
53
|
+
meta.subprocess = {
|
|
54
|
+
...spec,
|
|
55
|
+
version: resolved.version,
|
|
56
|
+
bundle_hash: resolved.bundleHash,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function serializeProcess(input) {
|
|
60
|
+
const nodes = input.process.nodes.map((node) => serializeNode(node));
|
|
61
|
+
return {
|
|
62
|
+
id: input.process.id,
|
|
63
|
+
name: input.process.name,
|
|
64
|
+
description: input.process.description ?? null,
|
|
65
|
+
entity_spec: { type: input.process.entity.type },
|
|
66
|
+
start_node_id: input.process.start_node_id,
|
|
67
|
+
nodes,
|
|
68
|
+
...(input.process.recommendations && input.process.recommendations.length > 0
|
|
69
|
+
? { recommendations: input.process.recommendations }
|
|
70
|
+
: {}),
|
|
71
|
+
...(input.process.tags && input.process.tags.length > 0
|
|
72
|
+
? { tags: input.process.tags }
|
|
73
|
+
: {}),
|
|
74
|
+
...(input.process.max_iterations !== undefined
|
|
75
|
+
? { max_iterations: input.process.max_iterations }
|
|
76
|
+
: {}),
|
|
77
|
+
...(input.process.max_revisits !== undefined
|
|
78
|
+
? { max_revisits: input.process.max_revisits }
|
|
79
|
+
: {}),
|
|
80
|
+
...(input.process.max_concurrent_runs !== undefined
|
|
81
|
+
? { max_concurrent_runs: input.process.max_concurrent_runs }
|
|
82
|
+
: {}),
|
|
83
|
+
...(input.process.supervisor_retry !== undefined
|
|
84
|
+
? { supervisor_retry: serializeRetryConfig(input.process.supervisor_retry) }
|
|
85
|
+
: {}),
|
|
86
|
+
...(input.process.run_as !== undefined
|
|
87
|
+
? { run_as: { email: input.process.run_as.email } }
|
|
88
|
+
: {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Serialize one node definition to its bundle form. Extracted so parallel
|
|
93
|
+
* sub-nodes round-trip through the exact same path as top-level nodes —
|
|
94
|
+
* including nested parallel blocks (recursion lands back here via
|
|
95
|
+
* `serializeNodeMetadata`'s `parallel` case).
|
|
96
|
+
*/
|
|
97
|
+
function serializeNode(node) {
|
|
98
|
+
const metadata = serializeNodeMetadata(node);
|
|
99
|
+
// Node-execution middleware (e.g. retry) — author-declared on the node
|
|
100
|
+
// definition, serialized verbatim into metadata.middleware. Resolved at
|
|
101
|
+
// runtime against the server-side node-middleware registry; the bundle
|
|
102
|
+
// registration step validates the referenced ids + kind-eligibility.
|
|
103
|
+
const middleware = 'middleware' in node ? node.middleware : undefined;
|
|
104
|
+
if (Array.isArray(middleware) && middleware.length > 0) {
|
|
105
|
+
metadata.middleware = middleware.map((m) => ({
|
|
106
|
+
id: m.id,
|
|
107
|
+
...(m.config !== undefined ? { config: m.config } : {}),
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
id: node.id,
|
|
112
|
+
kind: node.kind,
|
|
113
|
+
title: node.title,
|
|
114
|
+
description: node.description ?? null,
|
|
115
|
+
outgoing_edges: node.outgoing_edges.map((e) => ({ id: e.id, to: e.to })),
|
|
116
|
+
metadata,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function serializeNodeMetadata(node) {
|
|
120
|
+
switch (node.kind) {
|
|
121
|
+
case 'automation': {
|
|
122
|
+
// Automation nodes carry a native registry id instead of inline source —
|
|
123
|
+
// their code lives in the Atlas image and is never serialized here.
|
|
124
|
+
const native = node;
|
|
125
|
+
const meta = {
|
|
126
|
+
registered_function_id: native.registered_function_id,
|
|
127
|
+
execution: 'native',
|
|
128
|
+
};
|
|
129
|
+
if (native.config)
|
|
130
|
+
meta.config = native.config;
|
|
131
|
+
if (native.limits)
|
|
132
|
+
meta.limits = native.limits;
|
|
133
|
+
if (native.bindings)
|
|
134
|
+
meta.bindings = native.bindings;
|
|
135
|
+
const nativeInputMapper = native.input;
|
|
136
|
+
if (typeof nativeInputMapper === 'function') {
|
|
137
|
+
meta.input_mapper_source = nativeInputMapper.toString();
|
|
138
|
+
}
|
|
139
|
+
return meta;
|
|
140
|
+
}
|
|
141
|
+
case 'agent': {
|
|
142
|
+
const agent = node;
|
|
143
|
+
const meta = {
|
|
144
|
+
agent_id: agent.agent.id,
|
|
145
|
+
};
|
|
146
|
+
if (agent.timeout !== undefined)
|
|
147
|
+
meta.timeout = agent.timeout;
|
|
148
|
+
if (agent.workflowId !== undefined)
|
|
149
|
+
meta.workflow_id = agent.workflowId;
|
|
150
|
+
if (agent.on_reset_edge_id)
|
|
151
|
+
meta.on_reset_edge_id = agent.on_reset_edge_id;
|
|
152
|
+
if (agent.bindings)
|
|
153
|
+
meta.bindings = agent.bindings;
|
|
154
|
+
const inputMapper = agent.input;
|
|
155
|
+
if (typeof inputMapper === 'function') {
|
|
156
|
+
meta.input_mapper_source = inputMapper.toString();
|
|
157
|
+
}
|
|
158
|
+
return meta;
|
|
159
|
+
}
|
|
160
|
+
case 'human': {
|
|
161
|
+
const human = node;
|
|
162
|
+
const meta = {
|
|
163
|
+
metadata: human.metadata ?? {},
|
|
164
|
+
timeout: human.timeout ?? '7d',
|
|
165
|
+
};
|
|
166
|
+
if (human.on_timeout_edge_id)
|
|
167
|
+
meta.on_timeout_edge_id = human.on_timeout_edge_id;
|
|
168
|
+
if (human.completeLabel)
|
|
169
|
+
meta.complete_label = human.completeLabel;
|
|
170
|
+
if (human.hideTimeoutEdgeFromAdvance)
|
|
171
|
+
meta.hide_timeout_edge_from_advance = true;
|
|
172
|
+
// advance_allowed is either a static ACL object or a `(ctx) => ACL`
|
|
173
|
+
// mapper resolved per-step at entry (the per-branch fan-out assignment
|
|
174
|
+
// hook). Serialize the function form as source like the other mappers.
|
|
175
|
+
if (typeof human.advance_allowed === 'function') {
|
|
176
|
+
meta.advance_allowed_mapper_source = human.advance_allowed.toString();
|
|
177
|
+
}
|
|
178
|
+
else if (human.advance_allowed) {
|
|
179
|
+
meta.advance_allowed = human.advance_allowed;
|
|
180
|
+
}
|
|
181
|
+
if (human.assignable_to)
|
|
182
|
+
meta.assignable_to = human.assignable_to;
|
|
183
|
+
// Email notification: normalize the boolean / template-fn / config forms
|
|
184
|
+
// into metadata. Presence of `email_notification` = enabled; template fns
|
|
185
|
+
// are serialized to source like the other mappers.
|
|
186
|
+
const email = human.emailNotification;
|
|
187
|
+
if (email === true) {
|
|
188
|
+
meta.email_notification = true;
|
|
189
|
+
}
|
|
190
|
+
else if (typeof email === 'function') {
|
|
191
|
+
meta.email_notification = true;
|
|
192
|
+
meta.email_notification_template_source = email.toString();
|
|
193
|
+
}
|
|
194
|
+
else if (email && typeof email === 'object') {
|
|
195
|
+
meta.email_notification = true;
|
|
196
|
+
if (typeof email.template === 'function') {
|
|
197
|
+
meta.email_notification_template_source = email.template.toString();
|
|
198
|
+
}
|
|
199
|
+
if (email.timezone)
|
|
200
|
+
meta.email_notification_timezone = email.timezone;
|
|
201
|
+
if (email.reminders && email.reminders.length > 0) {
|
|
202
|
+
meta.email_notification_reminders = email.reminders.map((r) => ({
|
|
203
|
+
...(r.before !== undefined ? { before: r.before } : {}),
|
|
204
|
+
...(r.after !== undefined ? { after: r.after } : {}),
|
|
205
|
+
...(typeof r.template === 'function'
|
|
206
|
+
? { template_source: r.template.toString() }
|
|
207
|
+
: {}),
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
// Deploy-env allowlist (plain data, no sandboxed fn). Copied so the
|
|
211
|
+
// serialized bundle never aliases the author's readonly array. The
|
|
212
|
+
// worker interprets it against its own APP_ENV at dispatch time.
|
|
213
|
+
if (email.enabledEnvironments && email.enabledEnvironments.length > 0) {
|
|
214
|
+
meta.email_notification_enabled_environments = [...email.enabledEnvironments];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// dueDate `(ctx) => Date`, computed per-run; serialize as source.
|
|
218
|
+
if (typeof human.dueDate === 'function') {
|
|
219
|
+
meta.due_date_source = human.dueDate.toString();
|
|
220
|
+
}
|
|
221
|
+
if (human.bindings)
|
|
222
|
+
meta.bindings = human.bindings;
|
|
223
|
+
if (Array.isArray(human.artifact_refs)) {
|
|
224
|
+
meta.artifact_refs = human.artifact_refs;
|
|
225
|
+
}
|
|
226
|
+
else if (typeof human.artifact_refs === 'function') {
|
|
227
|
+
meta.artifact_refs_mapper_source = human.artifact_refs.toString();
|
|
228
|
+
}
|
|
229
|
+
// First-class comment-chain opt-in — packed alongside artifact_refs (NOT
|
|
230
|
+
// inside the author `metadata` bag), so it serializes as its own node
|
|
231
|
+
// field that the inbox/process-activity reads at `node.metadata.comments`.
|
|
232
|
+
if (human.comments)
|
|
233
|
+
meta.comments = human.comments;
|
|
234
|
+
const humanInputMapper = human.input;
|
|
235
|
+
if (typeof humanInputMapper === 'function') {
|
|
236
|
+
meta.input_mapper_source = humanInputMapper.toString();
|
|
237
|
+
}
|
|
238
|
+
return meta;
|
|
239
|
+
}
|
|
240
|
+
case 'subprocess': {
|
|
241
|
+
const subprocess = node;
|
|
242
|
+
const toSource = (fn) => fn.toString();
|
|
243
|
+
const smeta = {
|
|
244
|
+
process_id: subprocess.process,
|
|
245
|
+
version: subprocess.version ?? '',
|
|
246
|
+
bundle_hash: '',
|
|
247
|
+
on_child_error: subprocess.on_child_error,
|
|
248
|
+
output_mapper_source: toSource(subprocess.output),
|
|
249
|
+
...(subprocess.supervisor_retry !== undefined
|
|
250
|
+
? { supervisor_retry: serializeRetryConfig(subprocess.supervisor_retry) }
|
|
251
|
+
: {}),
|
|
252
|
+
};
|
|
253
|
+
const meta = { subprocess: smeta };
|
|
254
|
+
if (typeof subprocess.input === 'function') {
|
|
255
|
+
meta.input_mapper_source = toSource(subprocess.input);
|
|
256
|
+
}
|
|
257
|
+
if (subprocess.bindings)
|
|
258
|
+
meta.bindings = subprocess.bindings;
|
|
259
|
+
return meta;
|
|
260
|
+
}
|
|
261
|
+
case 'parallel': {
|
|
262
|
+
const parallel = node;
|
|
263
|
+
const toSource = (fn) => fn.toString();
|
|
264
|
+
const pmeta = {
|
|
265
|
+
mode: parallel.branches ? 'static' : 'dynamic',
|
|
266
|
+
on_branch_error: parallel.on_branch_error,
|
|
267
|
+
join_source: toSource(parallel.join),
|
|
268
|
+
};
|
|
269
|
+
if (parallel.max_concurrency !== undefined) {
|
|
270
|
+
pmeta.max_concurrency = parallel.max_concurrency;
|
|
271
|
+
}
|
|
272
|
+
if (parallel.supervisor_retry !== undefined) {
|
|
273
|
+
pmeta.supervisor_retry = serializeRetryConfig(parallel.supervisor_retry);
|
|
274
|
+
}
|
|
275
|
+
if (parallel.branches) {
|
|
276
|
+
pmeta.branches = parallel.branches.map((b) => ({
|
|
277
|
+
branch_id: b.branch_id,
|
|
278
|
+
node: serializeNode(b.node),
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
pmeta.fan_out_source = toSource(parallel.fan_out);
|
|
283
|
+
if (parallel.max_fanout !== undefined)
|
|
284
|
+
pmeta.max_fanout = parallel.max_fanout;
|
|
285
|
+
pmeta.branch = serializeNode(parallel.branch);
|
|
286
|
+
}
|
|
287
|
+
const meta = { parallel: pmeta };
|
|
288
|
+
// The parallel node's `input` mapper produces the optional rerun
|
|
289
|
+
// directive. It must live at TOP-LEVEL `input_mapper_source` — the same
|
|
290
|
+
// slot every other node kind uses — so the orchestrator's runInputMapper
|
|
291
|
+
// executes it and the runner reads the directive from its payload input.
|
|
292
|
+
// (Nesting it under `parallel` left it unread, disabling selective
|
|
293
|
+
// re-run + epoch invalidation entirely.)
|
|
294
|
+
if (typeof parallel.input === 'function') {
|
|
295
|
+
meta.input_mapper_source = toSource(parallel.input);
|
|
296
|
+
}
|
|
297
|
+
if (parallel.bindings)
|
|
298
|
+
meta.bindings = parallel.bindings;
|
|
299
|
+
return meta;
|
|
300
|
+
}
|
|
301
|
+
case 'delay': {
|
|
302
|
+
const delay = node;
|
|
303
|
+
// Pre-parse to ms so the runtime never re-implements the grammar. The
|
|
304
|
+
// string is kept for display; `duration_ms` is the authoritative wait.
|
|
305
|
+
const ms = parseDurationMs(delay.duration);
|
|
306
|
+
if (ms === null) {
|
|
307
|
+
throw new Error(`delay node "${delay.id}": invalid duration "${delay.duration}"`);
|
|
308
|
+
}
|
|
309
|
+
return { duration: delay.duration, duration_ms: ms };
|
|
310
|
+
}
|
|
311
|
+
case 'managed_function': {
|
|
312
|
+
const mf = node;
|
|
313
|
+
const meta = {
|
|
314
|
+
function_id: mf.function,
|
|
315
|
+
};
|
|
316
|
+
if (mf.version)
|
|
317
|
+
meta.version = mf.version;
|
|
318
|
+
if (mf.limits)
|
|
319
|
+
meta.limits = mf.limits;
|
|
320
|
+
if (mf.bindings)
|
|
321
|
+
meta.bindings = mf.bindings;
|
|
322
|
+
// NOTE: `middleware` (e.g. withRetry) is serialized generically for every
|
|
323
|
+
// node kind in `serializeNode` above — don't duplicate it here.
|
|
324
|
+
const inputMapper = mf.input;
|
|
325
|
+
if (typeof inputMapper === 'function') {
|
|
326
|
+
meta.input_mapper_source = inputMapper.toString();
|
|
327
|
+
}
|
|
328
|
+
return meta;
|
|
329
|
+
}
|
|
330
|
+
default:
|
|
331
|
+
return {};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function collectGitMetadata() {
|
|
335
|
+
const get = (cmd) => {
|
|
336
|
+
try {
|
|
337
|
+
return execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
created_at: new Date().toISOString(),
|
|
345
|
+
created_by: process.env['USER'] ?? null,
|
|
346
|
+
git_commit: get('git rev-parse HEAD'),
|
|
347
|
+
git_branch: get('git rev-parse --abbrev-ref HEAD'),
|
|
348
|
+
git_dirty: get('git status --porcelain') !== '',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
export function summarizeBundle(bundle) {
|
|
352
|
+
return {
|
|
353
|
+
name: bundle.manifest.bundle.name,
|
|
354
|
+
version: bundle.version,
|
|
355
|
+
bundle_hash: bundle.bundle_hash,
|
|
356
|
+
source_hash: bundle.source_hash,
|
|
357
|
+
created_at: bundle.metadata.created_at,
|
|
358
|
+
created_by: bundle.metadata.created_by,
|
|
359
|
+
git_commit: bundle.metadata.git_commit,
|
|
360
|
+
git_branch: bundle.metadata.git_branch,
|
|
361
|
+
git_dirty: bundle.metadata.git_dirty,
|
|
362
|
+
processes: bundle.processes.map((p) => ({
|
|
363
|
+
id: p.id,
|
|
364
|
+
nodes: p.nodes.length,
|
|
365
|
+
edges: p.nodes.reduce((acc, n) => acc + n.outgoing_edges.length, 0),
|
|
366
|
+
})),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle → source codegen. Regenerates the author-facing `process.ts` (and
|
|
3
|
+
* `recommendations.ts`) for a stored {@link SerializedProcess} so a process
|
|
4
|
+
* published from the UI can be pulled back into a local tree.
|
|
5
|
+
*
|
|
6
|
+
* Fidelity notes (the bundle is a compiled artifact, not the original source):
|
|
7
|
+
* - Mapper bodies are the build-time `Function.toString()` strings — valid and
|
|
8
|
+
* runnable, but type annotations were stripped at serialize time.
|
|
9
|
+
* - Automation bodies live in the Atlas image; only the registry id is
|
|
10
|
+
* emitted (`function: '<id>'`).
|
|
11
|
+
* - Co-located subprocess pins (version / bundle_hash) are dropped — they are
|
|
12
|
+
* re-resolved on the next `apply`. An EXTERNAL subprocess (not bundled here)
|
|
13
|
+
* keeps its explicit `version` pin so the rebuild resolves the same target.
|
|
14
|
+
* - `recommendations` are an opaque passthrough — emitted verbatim.
|
|
15
|
+
*/
|
|
16
|
+
import type { LatticeBundle, SerializedProcess } from '@sequenceholdings/lattice/bundle';
|
|
17
|
+
/** Files to write for one process, keyed by basename. */
|
|
18
|
+
export declare function generateProcessFiles(process: SerializedProcess, bundle: LatticeBundle): Record<string, string>;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle → source codegen. Regenerates the author-facing `process.ts` (and
|
|
3
|
+
* `recommendations.ts`) for a stored {@link SerializedProcess} so a process
|
|
4
|
+
* published from the UI can be pulled back into a local tree.
|
|
5
|
+
*
|
|
6
|
+
* Fidelity notes (the bundle is a compiled artifact, not the original source):
|
|
7
|
+
* - Mapper bodies are the build-time `Function.toString()` strings — valid and
|
|
8
|
+
* runnable, but type annotations were stripped at serialize time.
|
|
9
|
+
* - Automation bodies live in the Atlas image; only the registry id is
|
|
10
|
+
* emitted (`function: '<id>'`).
|
|
11
|
+
* - Co-located subprocess pins (version / bundle_hash) are dropped — they are
|
|
12
|
+
* re-resolved on the next `apply`. An EXTERNAL subprocess (not bundled here)
|
|
13
|
+
* keeps its explicit `version` pin so the rebuild resolves the same target.
|
|
14
|
+
* - `recommendations` are an opaque passthrough — emitted verbatim.
|
|
15
|
+
*/
|
|
16
|
+
/** ECMAScript reserved words (+ a few literals) that can't be bare identifiers. */
|
|
17
|
+
const RESERVED = new Set([
|
|
18
|
+
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|
|
19
|
+
'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false',
|
|
20
|
+
'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new',
|
|
21
|
+
'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try',
|
|
22
|
+
'typeof', 'var', 'void', 'while', 'with', 'yield', 'let', 'static',
|
|
23
|
+
'implements', 'interface', 'package', 'private', 'protected', 'public',
|
|
24
|
+
'await', 'async',
|
|
25
|
+
]);
|
|
26
|
+
/** Files to write for one process, keyed by basename. */
|
|
27
|
+
export function generateProcessFiles(process, bundle) {
|
|
28
|
+
const ctx = {
|
|
29
|
+
used: new Set(['defineProcess']),
|
|
30
|
+
colocated: new Set((bundle.processes ?? []).map((p) => p.id)),
|
|
31
|
+
};
|
|
32
|
+
const names = buildConstNames(process.nodes);
|
|
33
|
+
const consts = process.nodes.map((node) => {
|
|
34
|
+
const name = names.get(node.id);
|
|
35
|
+
return { name, code: `const ${name} = ${emitNode(node, ctx)}` };
|
|
36
|
+
});
|
|
37
|
+
const hasRecs = Array.isArray(process.recommendations) && process.recommendations.length > 0;
|
|
38
|
+
const imports = `import {\n${[...ctx.used].sort().map((n) => ` ${n},`).join('\n')}\n} from '@sequenceholdings/lattice'`;
|
|
39
|
+
const recImport = hasRecs ? `\nimport { recommendations } from './recommendations.js'` : '';
|
|
40
|
+
const entityType = process.entity_spec?.type ?? 'application';
|
|
41
|
+
const procFields = [
|
|
42
|
+
` id: ${lit(process.id)},`,
|
|
43
|
+
` name: ${lit(process.name)},`,
|
|
44
|
+
process.description != null ? ` description: ${lit(process.description)},` : null,
|
|
45
|
+
` entity: { type: ${lit(entityType)} },`,
|
|
46
|
+
` start_node_id: ${lit(process.start_node_id)},`,
|
|
47
|
+
process.max_iterations !== undefined ? ` max_iterations: ${process.max_iterations},` : null,
|
|
48
|
+
process.max_revisits !== undefined ? ` max_revisits: ${process.max_revisits},` : null,
|
|
49
|
+
process.max_concurrent_runs !== undefined
|
|
50
|
+
? ` max_concurrent_runs: ${process.max_concurrent_runs},`
|
|
51
|
+
: null,
|
|
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,
|
|
54
|
+
Array.isArray(process.tags) && process.tags.length > 0
|
|
55
|
+
? ` tags: ${pretty(process.tags, 1)},`
|
|
56
|
+
: null,
|
|
57
|
+
` nodes: [${consts.map((c) => c.name).join(', ')}],`,
|
|
58
|
+
hasRecs ? ' recommendations,' : null,
|
|
59
|
+
]
|
|
60
|
+
.filter((l) => l !== null)
|
|
61
|
+
.join('\n');
|
|
62
|
+
const processTs = `${imports}${recImport}\n\n` +
|
|
63
|
+
`${consts.map((c) => c.code).join('\n\n')}\n\n` +
|
|
64
|
+
`export default defineProcess({\n${procFields}\n})\n`;
|
|
65
|
+
const files = { 'process.ts': processTs };
|
|
66
|
+
if (hasRecs) {
|
|
67
|
+
files['recommendations.ts'] =
|
|
68
|
+
`// Pulled from bundle ${bundle.version} — opaque passthrough.\n` +
|
|
69
|
+
`export const recommendations = ${pretty(process.recommendations, 0)}\n`;
|
|
70
|
+
}
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
73
|
+
function emitNode(node, ctx) {
|
|
74
|
+
const edges = emitEdges(node.outgoing_edges);
|
|
75
|
+
const head = [
|
|
76
|
+
` id: ${lit(node.id)},`,
|
|
77
|
+
` title: ${lit(node.title)},`,
|
|
78
|
+
node.description != null ? ` description: ${lit(node.description)},` : null,
|
|
79
|
+
];
|
|
80
|
+
const m = node.metadata;
|
|
81
|
+
const tail = ` outgoing_edges: ${edges},`;
|
|
82
|
+
switch (node.kind) {
|
|
83
|
+
case 'agent': {
|
|
84
|
+
ctx.used.add('defineAgentNode');
|
|
85
|
+
return call('defineAgentNode', [
|
|
86
|
+
...head,
|
|
87
|
+
` agent: { id: ${lit(String(m.agent_id))} },`,
|
|
88
|
+
m.workflow_id != null ? ` workflowId: ${lit(String(m.workflow_id))},` : null,
|
|
89
|
+
m.on_reset_edge_id != null ? ` on_reset_edge_id: ${lit(String(m.on_reset_edge_id))},` : null,
|
|
90
|
+
m.timeout !== undefined ? ` timeout: ${value(m.timeout)},` : null,
|
|
91
|
+
bindings(m.bindings),
|
|
92
|
+
middleware(m.middleware),
|
|
93
|
+
mapper('input', m.input_mapper_source),
|
|
94
|
+
tail,
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
case 'human': {
|
|
98
|
+
ctx.used.add('defineHumanNode');
|
|
99
|
+
const metaObj = m.metadata;
|
|
100
|
+
return call('defineHumanNode', [
|
|
101
|
+
...head,
|
|
102
|
+
metaObj && Object.keys(metaObj).length > 0 ? ` metadata: ${pretty(metaObj, 1)},` : null,
|
|
103
|
+
m.complete_label != null ? ` completeLabel: ${lit(String(m.complete_label))},` : null,
|
|
104
|
+
m.timeout !== undefined ? ` timeout: ${value(m.timeout)},` : null,
|
|
105
|
+
m.on_timeout_edge_id != null ? ` on_timeout_edge_id: ${lit(String(m.on_timeout_edge_id))},` : null,
|
|
106
|
+
m.hide_timeout_edge_from_advance === true ? ` hideTimeoutEdgeFromAdvance: true,` : null,
|
|
107
|
+
m.advance_allowed !== undefined ? ` advance_allowed: ${value(m.advance_allowed)},` : null,
|
|
108
|
+
m.assignable_to !== undefined ? ` assignable_to: ${pretty(m.assignable_to, 1)},` : null,
|
|
109
|
+
bindings(m.bindings),
|
|
110
|
+
Array.isArray(m.artifact_refs) ? ` artifact_refs: ${pretty(m.artifact_refs, 1)},` : null,
|
|
111
|
+
mapper('artifact_refs', m.artifact_refs_mapper_source),
|
|
112
|
+
m.comments !== undefined ? ` comments: ${pretty(m.comments, 1)},` : null,
|
|
113
|
+
mapper('input', m.input_mapper_source),
|
|
114
|
+
tail,
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
117
|
+
case 'automation': {
|
|
118
|
+
if (m.registered_function_id == null) {
|
|
119
|
+
throw new Error(`codegen: automation node "${node.id}" has no registered_function_id — ` +
|
|
120
|
+
`sandboxed automation nodes are no longer supported`);
|
|
121
|
+
}
|
|
122
|
+
ctx.used.add('defineNativeAutomationNode');
|
|
123
|
+
return call('defineNativeAutomationNode', [
|
|
124
|
+
...head,
|
|
125
|
+
` function: ${lit(String(m.registered_function_id))},`,
|
|
126
|
+
m.config !== undefined ? ` config: ${pretty(m.config, 1)},` : null,
|
|
127
|
+
m.limits !== undefined ? ` limits: ${pretty(m.limits, 1)},` : null,
|
|
128
|
+
bindings(m.bindings),
|
|
129
|
+
middleware(m.middleware),
|
|
130
|
+
mapper('input', m.input_mapper_source),
|
|
131
|
+
tail,
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
case 'subprocess': {
|
|
135
|
+
ctx.used.add('defineSubprocessNode');
|
|
136
|
+
const sub = m.subprocess;
|
|
137
|
+
return call('defineSubprocessNode', [
|
|
138
|
+
...head,
|
|
139
|
+
` process: ${lit(sub.process_id)},`,
|
|
140
|
+
// Colocated pins re-resolve on rebuild; keep an external target's pin.
|
|
141
|
+
ctx.colocated.has(sub.process_id) ? null : ` version: ${lit(sub.version)},`,
|
|
142
|
+
` on_child_error: ${lit(sub.on_child_error)},`,
|
|
143
|
+
sub.supervisor_retry !== undefined ? ` supervisor_retry: ${pretty(sub.supervisor_retry, 1)},` : null,
|
|
144
|
+
bindings(m.bindings),
|
|
145
|
+
mapper('input', m.input_mapper_source),
|
|
146
|
+
` output: ${sub.output_mapper_source},`,
|
|
147
|
+
tail,
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
case 'parallel': {
|
|
151
|
+
ctx.used.add('defineParallelNode');
|
|
152
|
+
const par = m.parallel;
|
|
153
|
+
const branchFields = [];
|
|
154
|
+
if (par.mode === 'static' && par.branches) {
|
|
155
|
+
branchFields.push(` branches: [\n${par.branches
|
|
156
|
+
.map((b) => ` { branch_id: ${lit(b.branch_id)}, node: ${emitNode(b.node, ctx)} },`)
|
|
157
|
+
.join('\n')}\n ],`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
if (par.fan_out_source)
|
|
161
|
+
branchFields.push(` fan_out: ${par.fan_out_source},`);
|
|
162
|
+
if (par.max_fanout !== undefined)
|
|
163
|
+
branchFields.push(` max_fanout: ${par.max_fanout},`);
|
|
164
|
+
if (par.branch)
|
|
165
|
+
branchFields.push(` branch: ${emitNode(par.branch, ctx)},`);
|
|
166
|
+
}
|
|
167
|
+
return call('defineParallelNode', [
|
|
168
|
+
...head,
|
|
169
|
+
...branchFields,
|
|
170
|
+
par.max_concurrency !== undefined ? ` max_concurrency: ${par.max_concurrency},` : null,
|
|
171
|
+
par.supervisor_retry !== undefined ? ` supervisor_retry: ${pretty(par.supervisor_retry, 1)},` : null,
|
|
172
|
+
bindings(m.bindings),
|
|
173
|
+
` on_branch_error: ${lit(par.on_branch_error)},`,
|
|
174
|
+
` join: ${par.join_source},`,
|
|
175
|
+
mapper('input', m.input_mapper_source),
|
|
176
|
+
tail,
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
case 'delay': {
|
|
180
|
+
ctx.used.add('defineDelayNode');
|
|
181
|
+
return call('defineDelayNode', [...head, ` duration: ${lit(String(m.duration))},`, tail]);
|
|
182
|
+
}
|
|
183
|
+
case 'managed_function': {
|
|
184
|
+
ctx.used.add('defineManagedFunctionNode');
|
|
185
|
+
const fields = [
|
|
186
|
+
...head,
|
|
187
|
+
` function: ${lit(String(m.function_id))},`,
|
|
188
|
+
m.version ? ` version: ${lit(String(m.version))},` : null,
|
|
189
|
+
m.limits !== undefined ? ` limits: ${pretty(m.limits, 1)},` : null,
|
|
190
|
+
bindings(m.bindings),
|
|
191
|
+
middleware(m.middleware),
|
|
192
|
+
mapper('input', m.input_mapper_source),
|
|
193
|
+
tail,
|
|
194
|
+
];
|
|
195
|
+
return call('defineManagedFunctionNode', fields);
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
throw new Error(`codegen: unsupported node kind "${node.kind}" (node "${node.id}")`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function call(fn, fields) {
|
|
202
|
+
return `${fn}({\n${fields.filter((f) => f !== null).join('\n')}\n})`;
|
|
203
|
+
}
|
|
204
|
+
function emitEdges(edges) {
|
|
205
|
+
if (edges.length === 0)
|
|
206
|
+
return '[] as const';
|
|
207
|
+
const items = edges.map((e) => `{ id: ${lit(e.id)}, to: ${lit(e.to)} }`).join(', ');
|
|
208
|
+
return `[${items}] as const`;
|
|
209
|
+
}
|
|
210
|
+
/** A mapper field whose value is a serialized `Function.toString()` string. */
|
|
211
|
+
function mapper(field, source) {
|
|
212
|
+
if (typeof source !== 'string' || source.length === 0)
|
|
213
|
+
return null;
|
|
214
|
+
return ` ${field}: ${source},`;
|
|
215
|
+
}
|
|
216
|
+
/** A safely-escaped TS string literal. */
|
|
217
|
+
function lit(value) {
|
|
218
|
+
return JSON.stringify(value);
|
|
219
|
+
}
|
|
220
|
+
/** A primitive value as TS source (string → quoted; number/boolean → as-is). */
|
|
221
|
+
function value(v) {
|
|
222
|
+
return typeof v === 'string' ? lit(v) : String(v);
|
|
223
|
+
}
|
|
224
|
+
/** A plain JSON value as an indented TS object/array literal. `depth` is the
|
|
225
|
+
* indent level of the line the value starts on. */
|
|
226
|
+
function pretty(v, depth) {
|
|
227
|
+
const json = JSON.stringify(v, null, 2);
|
|
228
|
+
if (!json.includes('\n'))
|
|
229
|
+
return json;
|
|
230
|
+
const pad = ' '.repeat(depth);
|
|
231
|
+
return json
|
|
232
|
+
.split('\n')
|
|
233
|
+
.map((line, i) => (i === 0 ? line : pad + line))
|
|
234
|
+
.join('\n');
|
|
235
|
+
}
|
|
236
|
+
/** An optional `bindings: [...]` field (descriptive node→lineage links). */
|
|
237
|
+
function bindings(v) {
|
|
238
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
239
|
+
return null;
|
|
240
|
+
return ` bindings: ${pretty(v, 1)},`;
|
|
241
|
+
}
|
|
242
|
+
/** An optional `middleware: [...]` field (node-execution middleware refs). */
|
|
243
|
+
function middleware(v) {
|
|
244
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
245
|
+
return null;
|
|
246
|
+
return ` middleware: ${pretty(v, 1)},`;
|
|
247
|
+
}
|
|
248
|
+
/** A node id → a valid, readable JS const identifier (camelCase, never a
|
|
249
|
+
* reserved word, always starting with a letter/underscore). */
|
|
250
|
+
function constName(id) {
|
|
251
|
+
const safe = id.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
252
|
+
const camel = safe.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
253
|
+
return /^[A-Za-z_]/.test(camel) && !RESERVED.has(camel) ? camel : `n_${camel}`;
|
|
254
|
+
}
|
|
255
|
+
/** Assign each node a collision-free const name in node order: derive a safe
|
|
256
|
+
* base via {@link constName}, then suffix `_2`, `_3`, … on any clash so two
|
|
257
|
+
* ids that camelCase to the same identifier stay distinct. */
|
|
258
|
+
function buildConstNames(nodes) {
|
|
259
|
+
const names = new Map();
|
|
260
|
+
const used = new Set();
|
|
261
|
+
for (const node of nodes) {
|
|
262
|
+
const base = constName(node.id);
|
|
263
|
+
let name = base;
|
|
264
|
+
for (let i = 2; used.has(name); i++)
|
|
265
|
+
name = `${base}_${i}`;
|
|
266
|
+
used.add(name);
|
|
267
|
+
names.set(node.id, name);
|
|
268
|
+
}
|
|
269
|
+
return names;
|
|
270
|
+
}
|