@assemble-dev/cli 0.0.1

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 (2) hide show
  1. package/dist/index.js +2106 -0
  2. package/package.json +34 -0
package/dist/index.js ADDED
@@ -0,0 +1,2106 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { AppError } from "@assemble-dev/shared-utils";
5
+
6
+ // src/commands/dev.ts
7
+ import { existsSync as existsSync2 } from "fs";
8
+ import { join as join2, resolve as resolve2 } from "path";
9
+ import { parseArgs as parseArgs2 } from "util";
10
+
11
+ // src/output/cli-output.ts
12
+ var ansiCodes = {
13
+ reset: "\x1B[0m",
14
+ green: "\x1B[32m",
15
+ red: "\x1B[31m"
16
+ };
17
+ function resolveUseColor(environmentVariables) {
18
+ const noColorValue = environmentVariables.NO_COLOR;
19
+ return noColorValue === void 0 || noColorValue === "";
20
+ }
21
+ function createCliOutput(writeLine, useColor) {
22
+ return {
23
+ writeSuccessLine: (message) => {
24
+ writeLine(
25
+ useColor ? `${ansiCodes.green}\u2713${ansiCodes.reset} ${message}` : `\u2713 ${message}`
26
+ );
27
+ },
28
+ writeFailureLine: (message) => {
29
+ writeLine(
30
+ useColor ? `${ansiCodes.red}\u2717${ansiCodes.reset} ${message}` : `\u2717 ${message}`
31
+ );
32
+ },
33
+ writeInfoLine: (message) => {
34
+ writeLine(message);
35
+ }
36
+ };
37
+ }
38
+
39
+ // src/commands/dev-watcher.ts
40
+ import { mkdirSync, statSync, watch, writeFileSync } from "fs";
41
+ import { join } from "path";
42
+ function createFileSystemWatcherFactory() {
43
+ return (watchedPaths, onFileSystemChange) => {
44
+ const watchers = watchedPaths.map(
45
+ (watchedPath) => watch(
46
+ watchedPath,
47
+ { recursive: statSync(watchedPath).isDirectory() },
48
+ () => {
49
+ onFileSystemChange();
50
+ }
51
+ )
52
+ );
53
+ return {
54
+ close: () => {
55
+ for (const watcher of watchers) {
56
+ watcher.close();
57
+ }
58
+ }
59
+ };
60
+ };
61
+ }
62
+ function createTimeoutDebounceScheduler() {
63
+ return (timerCallback, delayMs) => {
64
+ const timer = setTimeout(timerCallback, delayMs);
65
+ return () => {
66
+ clearTimeout(timer);
67
+ };
68
+ };
69
+ }
70
+ function createDebouncedWorkflowChangeQueue(debounceDelayMs, scheduleDebounceTimer) {
71
+ let cancelPendingDebounceTimer = null;
72
+ let hasBufferedChange = false;
73
+ let deliverChangeToWaiter = null;
74
+ const markWorkflowChangeReady = () => {
75
+ cancelPendingDebounceTimer = null;
76
+ if (deliverChangeToWaiter !== null) {
77
+ const deliverChange = deliverChangeToWaiter;
78
+ deliverChangeToWaiter = null;
79
+ deliverChange();
80
+ return;
81
+ }
82
+ hasBufferedChange = true;
83
+ };
84
+ return {
85
+ notifyWorkflowChange: () => {
86
+ if (cancelPendingDebounceTimer !== null) {
87
+ cancelPendingDebounceTimer();
88
+ }
89
+ cancelPendingDebounceTimer = scheduleDebounceTimer(
90
+ markWorkflowChangeReady,
91
+ debounceDelayMs
92
+ );
93
+ },
94
+ waitForWorkflowChangeOrStop: async (stopSignal) => {
95
+ if (hasBufferedChange) {
96
+ hasBufferedChange = false;
97
+ return "change";
98
+ }
99
+ if (stopSignal.aborted) {
100
+ return "stop";
101
+ }
102
+ return await new Promise(
103
+ (resolveWaitOutcome) => {
104
+ const resolveWithStop = () => {
105
+ deliverChangeToWaiter = null;
106
+ resolveWaitOutcome("stop");
107
+ };
108
+ stopSignal.addEventListener("abort", resolveWithStop, { once: true });
109
+ deliverChangeToWaiter = () => {
110
+ stopSignal.removeEventListener("abort", resolveWithStop);
111
+ resolveWaitOutcome("change");
112
+ };
113
+ }
114
+ );
115
+ }
116
+ };
117
+ }
118
+ var lastRunFileName = "last-run";
119
+ function writeLastRunIdFile(lastRunDirectory, runId) {
120
+ mkdirSync(lastRunDirectory, { recursive: true });
121
+ writeFileSync(join(lastRunDirectory, lastRunFileName), `${runId}
122
+ `, {
123
+ encoding: "utf8",
124
+ mode: 420
125
+ });
126
+ }
127
+
128
+ // src/commands/run-input.ts
129
+ import { readFileSync } from "fs";
130
+ import { JsonValueSchema } from "@assemble-dev/shared-types/json";
131
+ function parseRunInputJsonText(jsonText, sourceLabel) {
132
+ try {
133
+ return {
134
+ outcome: "resolved",
135
+ inputValue: JsonValueSchema.parse(JSON.parse(jsonText))
136
+ };
137
+ } catch (caughtError) {
138
+ const reason = caughtError instanceof Error ? caughtError.message : String(caughtError);
139
+ return {
140
+ outcome: "failed",
141
+ message: `Could not parse ${sourceLabel} as JSON: ${reason}`
142
+ };
143
+ }
144
+ }
145
+ function readRunInputFile(inputFilePath) {
146
+ try {
147
+ return parseRunInputJsonText(
148
+ readFileSync(inputFilePath, "utf8"),
149
+ `--input-file ${inputFilePath}`
150
+ );
151
+ } catch (caughtError) {
152
+ const reason = caughtError instanceof Error ? caughtError.message : String(caughtError);
153
+ return {
154
+ outcome: "failed",
155
+ message: `Could not read --input-file ${inputFilePath}: ${reason}`
156
+ };
157
+ }
158
+ }
159
+ function resolveRunInputValue(input) {
160
+ if (input.inputJson !== void 0 && input.inputFilePath !== void 0) {
161
+ return {
162
+ outcome: "failed",
163
+ message: "Pass either --input or --input-file, not both."
164
+ };
165
+ }
166
+ if (input.inputJson !== void 0) {
167
+ return parseRunInputJsonText(input.inputJson, "--input");
168
+ }
169
+ if (input.inputFilePath !== void 0) {
170
+ return readRunInputFile(input.inputFilePath);
171
+ }
172
+ return { outcome: "resolved", inputValue: {} };
173
+ }
174
+
175
+ // src/commands/run-workflow-loader.ts
176
+ import { existsSync } from "fs";
177
+ import { resolve } from "path";
178
+ import { createJiti } from "jiti";
179
+ function isRunnableWorkflowDefinition(candidate) {
180
+ return typeof candidate === "object" && candidate !== null && typeof candidate.name === "string" && typeof candidate.run === "function";
181
+ }
182
+ function listModuleExportNames(loadedModule) {
183
+ const exportNames = Object.keys(loadedModule);
184
+ return exportNames.length === 0 ? "(none)" : exportNames.join(", ");
185
+ }
186
+ function listWorkflowExportNames(loadedModule) {
187
+ return Object.entries(loadedModule).filter(([, candidate]) => isRunnableWorkflowDefinition(candidate)).map(([exportName]) => exportName);
188
+ }
189
+ async function importWorkflowModule(absoluteFilePath, freshImport) {
190
+ const jiti = createJiti(import.meta.url, {
191
+ interopDefault: false,
192
+ moduleCache: !freshImport
193
+ });
194
+ try {
195
+ return await jiti.import(absoluteFilePath);
196
+ } catch (caughtError) {
197
+ const reason = caughtError instanceof Error ? caughtError.message : String(caughtError);
198
+ return `Could not load workflow file ${absoluteFilePath}: ${reason}`;
199
+ }
200
+ }
201
+ function resolveNamedWorkflowExport(loadedModule, exportName, absoluteFilePath) {
202
+ const candidate = loadedModule[exportName];
203
+ if (isRunnableWorkflowDefinition(candidate)) {
204
+ return { outcome: "loaded", workflowDefinition: candidate };
205
+ }
206
+ return {
207
+ outcome: "failed",
208
+ message: `Export "${exportName}" in ${absoluteFilePath} is not a workflow. Available exports: ${listModuleExportNames(loadedModule)}`
209
+ };
210
+ }
211
+ function resolveDiscoveredWorkflowExport(loadedModule, absoluteFilePath) {
212
+ const defaultExport = loadedModule.default;
213
+ if (isRunnableWorkflowDefinition(defaultExport)) {
214
+ return { outcome: "loaded", workflowDefinition: defaultExport };
215
+ }
216
+ const workflowExportNames = listWorkflowExportNames(loadedModule);
217
+ const singleWorkflowExportName = workflowExportNames[0];
218
+ if (workflowExportNames.length === 1 && singleWorkflowExportName !== void 0) {
219
+ return resolveNamedWorkflowExport(
220
+ loadedModule,
221
+ singleWorkflowExportName,
222
+ absoluteFilePath
223
+ );
224
+ }
225
+ if (workflowExportNames.length === 0) {
226
+ return {
227
+ outcome: "failed",
228
+ message: `No workflow export found in ${absoluteFilePath}. Available exports: ${listModuleExportNames(loadedModule)}. Export a workflow as the default export or pass --workflow <exportName>.`
229
+ };
230
+ }
231
+ return {
232
+ outcome: "failed",
233
+ message: `Multiple workflow exports found in ${absoluteFilePath}: ${workflowExportNames.join(", ")}. Pass --workflow <exportName> to choose one.`
234
+ };
235
+ }
236
+ async function loadWorkflowFromFile(input) {
237
+ const absoluteFilePath = resolve(input.filePath);
238
+ if (!existsSync(absoluteFilePath)) {
239
+ return {
240
+ outcome: "failed",
241
+ message: `Workflow file not found: ${absoluteFilePath}`
242
+ };
243
+ }
244
+ const importResult = await importWorkflowModule(
245
+ absoluteFilePath,
246
+ input.freshImport ?? false
247
+ );
248
+ if (typeof importResult === "string") {
249
+ return { outcome: "failed", message: importResult };
250
+ }
251
+ if (input.exportName !== void 0) {
252
+ return resolveNamedWorkflowExport(
253
+ importResult,
254
+ input.exportName,
255
+ absoluteFilePath
256
+ );
257
+ }
258
+ return resolveDiscoveredWorkflowExport(importResult, absoluteFilePath);
259
+ }
260
+
261
+ // src/commands/run.ts
262
+ import { parseArgs } from "util";
263
+ import {
264
+ buildHostedTraceUrl as buildHostedTraceUrl2,
265
+ buildMissingKeyMessage,
266
+ createConsoleSink,
267
+ createJsonlSink,
268
+ createPlatformTraceSink,
269
+ defaultTraceDirectory,
270
+ resolvePlatformConfig,
271
+ resolveTraceFilePath as resolveTraceFilePath2
272
+ } from "@assemble-dev/sdk";
273
+ import { generateRunId } from "@assemble-dev/shared-utils/ids";
274
+
275
+ // src/commands/run-wait-approval.ts
276
+ import {
277
+ buildHostedTraceUrl,
278
+ resolveTraceFilePath,
279
+ resumeRun
280
+ } from "@assemble-dev/sdk";
281
+ import { ApprovalStatusSchema } from "@assemble-dev/shared-types/runs";
282
+ import { z } from "zod";
283
+ var defaultWaitApprovalTimeoutSeconds = 300;
284
+ var waitApprovalPollIntervalMs = 5e3;
285
+ var ApprovalStatusPollResponseSchema = z.object({
286
+ approvalId: z.string(),
287
+ status: ApprovalStatusSchema,
288
+ decidedBy: z.string().nullable(),
289
+ reason: z.string().nullable(),
290
+ runId: z.string()
291
+ });
292
+ function extractWaitApprovalFlag(args) {
293
+ const remainingArgs = [];
294
+ let waitApprovalTimeoutSeconds = null;
295
+ for (let index = 0; index < args.length; index += 1) {
296
+ const argument = args[index];
297
+ if (argument === void 0) {
298
+ continue;
299
+ }
300
+ if (argument === "--wait-approval") {
301
+ const nextArgument = args[index + 1];
302
+ if (nextArgument !== void 0 && /^\d+$/.test(nextArgument)) {
303
+ waitApprovalTimeoutSeconds = Number(nextArgument);
304
+ index += 1;
305
+ } else {
306
+ waitApprovalTimeoutSeconds = defaultWaitApprovalTimeoutSeconds;
307
+ }
308
+ continue;
309
+ }
310
+ if (argument.startsWith("--wait-approval=")) {
311
+ const flagValue = argument.slice("--wait-approval=".length);
312
+ if (!/^\d+$/.test(flagValue)) {
313
+ return null;
314
+ }
315
+ waitApprovalTimeoutSeconds = Number(flagValue);
316
+ continue;
317
+ }
318
+ remainingArgs.push(argument);
319
+ }
320
+ return { remainingArgs, waitApprovalTimeoutSeconds };
321
+ }
322
+ function buildMissingKeyForApprovalWaitMessage() {
323
+ return "Waiting for a hosted approval needs a platform key. Run `asm login` or set ASSEMBLE_API_KEY, approve from the dashboard, and the run will resume.";
324
+ }
325
+ function writeTraceLocationLine(context) {
326
+ if (context.platformSink !== null) {
327
+ context.runtime.writeLine(
328
+ `Trace: ${buildHostedTraceUrl({
329
+ dashboardUrl: context.platformConfig.dashboardUrl,
330
+ runId: context.runId
331
+ })}`
332
+ );
333
+ return;
334
+ }
335
+ context.runtime.writeLine(
336
+ `Trace: ${resolveTraceFilePath(context.runId, context.traceDirectory)}`
337
+ );
338
+ }
339
+ async function fetchApprovalStatusOnce(context, apiKey) {
340
+ try {
341
+ const response = await context.runtime.fetchImplementation(
342
+ `${context.platformConfig.baseUrl}/v1/approvals/${context.approvalId}`,
343
+ { method: "GET", headers: { authorization: `Bearer ${apiKey}` } }
344
+ );
345
+ if (!response.ok) {
346
+ return null;
347
+ }
348
+ return ApprovalStatusPollResponseSchema.parse(
349
+ JSON.parse(await response.text())
350
+ );
351
+ } catch {
352
+ return null;
353
+ }
354
+ }
355
+ async function pollApprovalUntilDecidedOrTimeout(context, apiKey) {
356
+ const now = context.runtime.now ?? (() => /* @__PURE__ */ new Date());
357
+ const sleep = context.runtime.sleep ?? (async (milliseconds) => {
358
+ await new Promise((resolveSleep) => {
359
+ setTimeout(resolveSleep, milliseconds);
360
+ });
361
+ });
362
+ const deadlineMs = now().getTime() + context.timeoutSeconds * 1e3;
363
+ while (now().getTime() < deadlineMs) {
364
+ const approvalStatus = await fetchApprovalStatusOnce(context, apiKey);
365
+ if (approvalStatus !== null && approvalStatus.status !== "pending") {
366
+ return approvalStatus;
367
+ }
368
+ await sleep(waitApprovalPollIntervalMs);
369
+ }
370
+ return null;
371
+ }
372
+ function toResumableWorkflowDefinition(workflowDefinition) {
373
+ return workflowDefinition;
374
+ }
375
+ async function resumeRunWithDecision(context, decision) {
376
+ const result = await resumeRun({
377
+ definition: toResumableWorkflowDefinition(context.workflowDefinition),
378
+ runId: context.runId,
379
+ decision: {
380
+ approved: decision.status === "approved",
381
+ approvedBy: decision.decidedBy ?? "dashboard",
382
+ ...decision.reason === null ? {} : { reason: decision.reason }
383
+ },
384
+ options: {
385
+ sinks: context.sinks,
386
+ runStateDirectory: context.traceDirectory
387
+ }
388
+ });
389
+ if (context.platformSink !== null) {
390
+ await context.platformSink.flush();
391
+ }
392
+ return result;
393
+ }
394
+ function printResumedRunOutcome(context, result) {
395
+ context.runtime.writeLine("");
396
+ if (result.status === "completed") {
397
+ context.runtime.writeLine("Workflow completed after approval.");
398
+ }
399
+ if (result.status === "rejected") {
400
+ context.runtime.writeLine("Workflow rejected. The tool call did not run.");
401
+ }
402
+ if (result.status === "awaiting_approval") {
403
+ context.runtime.writeLine(
404
+ `Workflow paused again: another tool call needs approval (Approval ID: ${result.approvalId}).`
405
+ );
406
+ }
407
+ context.runtime.writeLine("");
408
+ context.runtime.writeLine(`Run ID: ${context.runId}`);
409
+ context.runtime.writeLine(`Status: ${result.status}`);
410
+ writeTraceLocationLine(context);
411
+ context.runtime.writeLine("");
412
+ }
413
+ function printWaitTimeoutNotice(context) {
414
+ context.runtime.writeLine("");
415
+ context.runtime.writeLine(
416
+ `No decision arrived within ${context.timeoutSeconds}s. The run stays paused \u2014 approve it from the dashboard and resume with resumeRun from @assemble-dev/sdk.`
417
+ );
418
+ context.runtime.writeLine(`Approval ID: ${context.approvalId}`);
419
+ context.runtime.writeLine("");
420
+ }
421
+ function printWaitingNotice(context) {
422
+ context.runtime.writeLine("");
423
+ context.runtime.writeLine("Workflow paused: a tool call needs approval.");
424
+ context.runtime.writeLine(`Run ID: ${context.runId}`);
425
+ context.runtime.writeLine(`Approval ID: ${context.approvalId}`);
426
+ context.runtime.writeLine(
427
+ `Waiting up to ${context.timeoutSeconds}s for a decision from the dashboard...`
428
+ );
429
+ }
430
+ async function waitForApprovalDecisionAndResume(context) {
431
+ if (context.platformConfig.apiKey === null) {
432
+ context.runtime.writeLine("");
433
+ context.runtime.writeLine(buildMissingKeyForApprovalWaitMessage());
434
+ context.runtime.writeLine(`Approval ID: ${context.approvalId}`);
435
+ context.runtime.writeLine("");
436
+ return 0;
437
+ }
438
+ printWaitingNotice(context);
439
+ const decision = await pollApprovalUntilDecidedOrTimeout(
440
+ context,
441
+ context.platformConfig.apiKey
442
+ );
443
+ if (decision === null) {
444
+ printWaitTimeoutNotice(context);
445
+ return 0;
446
+ }
447
+ context.runtime.writeLine(
448
+ `Decision received: ${decision.status} by ${decision.decidedBy ?? "dashboard"}. Resuming run locally...`
449
+ );
450
+ const result = await resumeRunWithDecision(context, decision);
451
+ printResumedRunOutcome(context, result);
452
+ return 0;
453
+ }
454
+
455
+ // src/commands/run.ts
456
+ var runCommandUsage = "Usage: asm run <file> [--input <json>] [--input-file <path>] [--workflow <exportName>] [--trace-dir <dir>] [--no-upload] [--wait-approval [seconds]]";
457
+ var inputValidationMarker = "input failed schema validation:";
458
+ function parseRunFlags(args) {
459
+ const waitApprovalExtraction = extractWaitApprovalFlag(args);
460
+ if (waitApprovalExtraction === null) {
461
+ return null;
462
+ }
463
+ try {
464
+ const { values, positionals } = parseArgs({
465
+ args: waitApprovalExtraction.remainingArgs,
466
+ options: {
467
+ input: { type: "string" },
468
+ "input-file": { type: "string" },
469
+ workflow: { type: "string" },
470
+ "trace-dir": { type: "string" },
471
+ "no-upload": { type: "boolean" }
472
+ },
473
+ strict: true,
474
+ allowPositionals: true
475
+ });
476
+ const filePath = positionals[0];
477
+ if (filePath === void 0 || positionals.length > 1) {
478
+ return null;
479
+ }
480
+ return {
481
+ filePath,
482
+ inputJson: values.input,
483
+ inputFilePath: values["input-file"],
484
+ workflowExportName: values.workflow,
485
+ traceDirectory: values["trace-dir"],
486
+ noUpload: values["no-upload"] ?? false,
487
+ waitApprovalTimeoutSeconds: waitApprovalExtraction.waitApprovalTimeoutSeconds
488
+ };
489
+ } catch {
490
+ return null;
491
+ }
492
+ }
493
+ function buildWorkflowRunExecutionContext(runtime, output, flags, workflowDefinition, inputValue) {
494
+ const runId = generateRunId();
495
+ const traceDirectory = flags.traceDirectory ?? defaultTraceDirectory;
496
+ const platformConfig = resolvePlatformConfig({
497
+ homeDirectory: runtime.homeDir,
498
+ environmentVariables: runtime.env
499
+ });
500
+ const sinks = [
501
+ createConsoleSink({
502
+ writeLine: runtime.writeLine,
503
+ useColor: resolveUseColor(runtime.env)
504
+ }),
505
+ createJsonlSink({ runId, directory: traceDirectory })
506
+ ];
507
+ const platformSink = flags.noUpload || platformConfig.apiKey === null ? null : createPlatformTraceSink({
508
+ apiKey: platformConfig.apiKey,
509
+ baseUrl: platformConfig.baseUrl,
510
+ runId,
511
+ workflowName: workflowDefinition.name,
512
+ fetchImplementation: runtime.fetchImplementation
513
+ });
514
+ if (platformSink !== null) {
515
+ sinks.push(platformSink);
516
+ }
517
+ return {
518
+ runtime,
519
+ output,
520
+ workflowDefinition,
521
+ inputValue,
522
+ runId,
523
+ traceDirectory,
524
+ platformConfig,
525
+ platformSink,
526
+ sinks,
527
+ waitApprovalTimeoutSeconds: flags.waitApprovalTimeoutSeconds ?? null
528
+ };
529
+ }
530
+ function formatRunLatency(latencyMs) {
531
+ if (latencyMs >= 1e3) {
532
+ return `${(latencyMs / 1e3).toFixed(1)}s`;
533
+ }
534
+ return `${Math.round(latencyMs)}ms`;
535
+ }
536
+ function writeTraceLocationLines(context) {
537
+ if (context.platformSink !== null) {
538
+ context.runtime.writeLine(
539
+ `Trace: ${buildHostedTraceUrl2({
540
+ dashboardUrl: context.platformConfig.dashboardUrl,
541
+ runId: context.runId
542
+ })}`
543
+ );
544
+ return;
545
+ }
546
+ context.runtime.writeLine(
547
+ `Trace: ${resolveTraceFilePath2(context.runId, context.traceDirectory)}`
548
+ );
549
+ if (context.platformConfig.apiKey === null) {
550
+ context.runtime.writeLine(buildMissingKeyMessage());
551
+ }
552
+ }
553
+ function printCompletedWorkflowRunSummary(context, latencyMs) {
554
+ context.runtime.writeLine("");
555
+ context.runtime.writeLine("Workflow completed.");
556
+ context.runtime.writeLine("");
557
+ context.runtime.writeLine(`Run ID: ${context.runId}`);
558
+ context.runtime.writeLine("Status: completed");
559
+ context.runtime.writeLine(`Latency: ${formatRunLatency(latencyMs)}`);
560
+ writeTraceLocationLines(context);
561
+ context.runtime.writeLine("");
562
+ }
563
+ function printAwaitingApprovalWorkflowRunSummary(context, approvalId) {
564
+ context.runtime.writeLine("");
565
+ context.runtime.writeLine("Workflow paused: a tool call needs approval.");
566
+ context.runtime.writeLine("");
567
+ context.runtime.writeLine(`Run ID: ${context.runId}`);
568
+ context.runtime.writeLine("Status: awaiting_approval");
569
+ context.runtime.writeLine(`Approval ID: ${approvalId}`);
570
+ writeTraceLocationLines(context);
571
+ context.runtime.writeLine(
572
+ "Resume this run with resumeRun from @assemble-dev/sdk (asm resume is coming soon)."
573
+ );
574
+ context.runtime.writeLine("");
575
+ }
576
+ function printFailedWorkflowRun(context, runError) {
577
+ context.runtime.writeLine("");
578
+ const markerIndex = runError.message.indexOf(inputValidationMarker);
579
+ if (markerIndex >= 0) {
580
+ context.output.writeFailureLine(
581
+ "Workflow failed: the input didn't match the workflow's input schema."
582
+ );
583
+ context.runtime.writeLine(
584
+ runError.message.slice(markerIndex + inputValidationMarker.length).trim()
585
+ );
586
+ return 1;
587
+ }
588
+ context.output.writeFailureLine("Workflow failed.");
589
+ context.runtime.writeLine(runError.message);
590
+ context.runtime.writeLine(`Run ID: ${context.runId}`);
591
+ context.runtime.writeLine(
592
+ `Trace: ${resolveTraceFilePath2(context.runId, context.traceDirectory)}`
593
+ );
594
+ return 1;
595
+ }
596
+ async function flushPlatformTraceSink(context) {
597
+ if (context.platformSink !== null) {
598
+ await context.platformSink.flush();
599
+ }
600
+ }
601
+ function printSettledWorkflowRunSummary(context, result, latencyMs) {
602
+ if (result.status === "awaiting_approval") {
603
+ printAwaitingApprovalWorkflowRunSummary(context, result.approvalId);
604
+ return 0;
605
+ }
606
+ printCompletedWorkflowRunSummary(context, latencyMs);
607
+ return 0;
608
+ }
609
+ async function settleWorkflowRunResult(context, result, latencyMs) {
610
+ if (result.status === "awaiting_approval" && context.waitApprovalTimeoutSeconds !== null) {
611
+ return await waitForApprovalDecisionAndResume({
612
+ runtime: context.runtime,
613
+ workflowDefinition: context.workflowDefinition,
614
+ runId: context.runId,
615
+ approvalId: result.approvalId,
616
+ traceDirectory: context.traceDirectory,
617
+ platformConfig: context.platformConfig,
618
+ platformSink: context.platformSink,
619
+ sinks: context.sinks,
620
+ timeoutSeconds: context.waitApprovalTimeoutSeconds
621
+ });
622
+ }
623
+ return printSettledWorkflowRunSummary(context, result, latencyMs);
624
+ }
625
+ async function executeWorkflowRun(context) {
626
+ const now = context.runtime.now ?? (() => /* @__PURE__ */ new Date());
627
+ const startedAtMs = now().getTime();
628
+ try {
629
+ const result = await context.workflowDefinition.run(context.inputValue, {
630
+ sinks: context.sinks,
631
+ runId: context.runId,
632
+ runStateDirectory: context.traceDirectory
633
+ });
634
+ const latencyMs = now().getTime() - startedAtMs;
635
+ await flushPlatformTraceSink(context);
636
+ return await settleWorkflowRunResult(context, result, latencyMs);
637
+ } catch (caughtError) {
638
+ await flushPlatformTraceSink(context);
639
+ return printFailedWorkflowRun(
640
+ context,
641
+ caughtError instanceof Error ? caughtError : new Error(String(caughtError))
642
+ );
643
+ }
644
+ }
645
+ async function runRunCommand(args, runtime) {
646
+ const output = createCliOutput(runtime.writeLine, resolveUseColor(runtime.env));
647
+ const flags = parseRunFlags(args);
648
+ if (flags === null) {
649
+ output.writeFailureLine(`Unrecognized run arguments. ${runCommandUsage}`);
650
+ return 1;
651
+ }
652
+ const inputResult = resolveRunInputValue({
653
+ inputJson: flags.inputJson,
654
+ inputFilePath: flags.inputFilePath
655
+ });
656
+ if (inputResult.outcome === "failed") {
657
+ output.writeFailureLine(inputResult.message);
658
+ return 1;
659
+ }
660
+ const loadResult = await loadWorkflowFromFile({
661
+ filePath: flags.filePath,
662
+ exportName: flags.workflowExportName
663
+ });
664
+ if (loadResult.outcome === "failed") {
665
+ output.writeFailureLine(loadResult.message);
666
+ return 1;
667
+ }
668
+ return await executeWorkflowRun(
669
+ buildWorkflowRunExecutionContext(
670
+ runtime,
671
+ output,
672
+ flags,
673
+ loadResult.workflowDefinition,
674
+ inputResult.inputValue
675
+ )
676
+ );
677
+ }
678
+
679
+ // src/commands/dev.ts
680
+ var devCommandUsage = "Usage: asm dev <file> [--input <json>] [--input-file <path>] [--workflow <exportName>] [--trace-dir <dir>] [--watch-dir <dir>] [--no-upload]";
681
+ var defaultDebounceDelayMs = 150;
682
+ function parseDevFlags(args) {
683
+ try {
684
+ const { values, positionals } = parseArgs2({
685
+ args,
686
+ options: {
687
+ input: { type: "string" },
688
+ "input-file": { type: "string" },
689
+ workflow: { type: "string" },
690
+ "trace-dir": { type: "string" },
691
+ "watch-dir": { type: "string" },
692
+ "no-upload": { type: "boolean" }
693
+ },
694
+ strict: true,
695
+ allowPositionals: true
696
+ });
697
+ const filePath = positionals[0];
698
+ if (filePath === void 0 || positionals.length > 1) {
699
+ return null;
700
+ }
701
+ return {
702
+ filePath,
703
+ inputJson: values.input,
704
+ inputFilePath: values["input-file"],
705
+ workflowExportName: values.workflow,
706
+ traceDirectory: values["trace-dir"],
707
+ watchDirectory: values["watch-dir"],
708
+ noUpload: values["no-upload"] ?? false
709
+ };
710
+ } catch {
711
+ return null;
712
+ }
713
+ }
714
+ function resolveWatchedPaths(flags) {
715
+ const watchedPaths = [resolve2(flags.filePath)];
716
+ if (flags.watchDirectory !== void 0) {
717
+ watchedPaths.push(resolve2(flags.watchDirectory));
718
+ }
719
+ return watchedPaths;
720
+ }
721
+ function findMissingWatchedPath(flags) {
722
+ return resolveWatchedPaths(flags).find((path) => !existsSync2(path)) ?? null;
723
+ }
724
+ function createSigintStopSignal() {
725
+ const sigintController = new AbortController();
726
+ process.once("SIGINT", () => {
727
+ sigintController.abort();
728
+ });
729
+ return sigintController.signal;
730
+ }
731
+ function printChangeDetectedSeparator(runtime, runNumber) {
732
+ runtime.writeLine("");
733
+ runtime.writeLine(`\u2500\u2500\u2500\u2500\u2500\u2500 change detected \xB7 run #${runNumber} \u2500\u2500\u2500\u2500\u2500\u2500`);
734
+ runtime.writeLine("");
735
+ }
736
+ async function executeDevWorkflowIteration(session) {
737
+ const loadResult = await loadWorkflowFromFile({
738
+ filePath: session.flags.filePath,
739
+ exportName: session.flags.workflowExportName,
740
+ freshImport: true
741
+ });
742
+ if (loadResult.outcome === "failed") {
743
+ session.output.writeFailureLine(loadResult.message);
744
+ return;
745
+ }
746
+ const executionContext = buildWorkflowRunExecutionContext(
747
+ session.runtime,
748
+ session.output,
749
+ session.flags,
750
+ loadResult.workflowDefinition,
751
+ session.inputValue
752
+ );
753
+ await executeWorkflowRun(executionContext);
754
+ writeLastRunIdFile(session.lastRunDirectory, executionContext.runId);
755
+ session.runtime.writeLine(`Last run: ${executionContext.runId}`);
756
+ }
757
+ async function watchAndRerunWorkflow(session) {
758
+ let runNumber = 1;
759
+ await executeDevWorkflowIteration(session);
760
+ session.runtime.writeLine("Watching for changes. Press Ctrl+C to stop.");
761
+ for (; ; ) {
762
+ const waitOutcome = await session.changeQueue.waitForWorkflowChangeOrStop(
763
+ session.stopSignal
764
+ );
765
+ if (waitOutcome === "stop") {
766
+ return;
767
+ }
768
+ runNumber += 1;
769
+ printChangeDetectedSeparator(session.runtime, runNumber);
770
+ await executeDevWorkflowIteration(session);
771
+ session.runtime.writeLine("Watching for changes. Press Ctrl+C to stop.");
772
+ }
773
+ }
774
+ async function runDevCommand(args, runtime, options = {}) {
775
+ const output = createCliOutput(runtime.writeLine, resolveUseColor(runtime.env));
776
+ const flags = parseDevFlags(args);
777
+ if (flags === null) {
778
+ output.writeFailureLine(`Unrecognized dev arguments. ${devCommandUsage}`);
779
+ return 1;
780
+ }
781
+ const inputResult = resolveRunInputValue({
782
+ inputJson: flags.inputJson,
783
+ inputFilePath: flags.inputFilePath
784
+ });
785
+ if (inputResult.outcome === "failed") {
786
+ output.writeFailureLine(inputResult.message);
787
+ return 1;
788
+ }
789
+ const missingWatchedPath = findMissingWatchedPath(flags);
790
+ if (missingWatchedPath !== null) {
791
+ output.writeFailureLine(`Watched path not found: ${missingWatchedPath}`);
792
+ return 1;
793
+ }
794
+ return await startDevWatchSession(runtime, output, flags, inputResult.inputValue, options);
795
+ }
796
+ async function startDevWatchSession(runtime, output, flags, inputValue, options) {
797
+ const changeQueue = createDebouncedWorkflowChangeQueue(
798
+ options.debounceDelayMs ?? defaultDebounceDelayMs,
799
+ options.debounceScheduler ?? createTimeoutDebounceScheduler()
800
+ );
801
+ const watcherFactory = options.watcherFactory ?? createFileSystemWatcherFactory();
802
+ const watchHandle = watcherFactory(
803
+ resolveWatchedPaths(flags),
804
+ changeQueue.notifyWorkflowChange
805
+ );
806
+ try {
807
+ await watchAndRerunWorkflow({
808
+ runtime,
809
+ output,
810
+ flags,
811
+ inputValue,
812
+ lastRunDirectory: options.lastRunDirectory ?? join2(process.cwd(), ".assemble"),
813
+ changeQueue,
814
+ stopSignal: options.stopSignal ?? createSigintStopSignal()
815
+ });
816
+ } finally {
817
+ watchHandle.close();
818
+ }
819
+ runtime.writeLine("Stopped watching.");
820
+ return 0;
821
+ }
822
+
823
+ // src/commands/eval.ts
824
+ import { tmpdir } from "os";
825
+ import { join as join3 } from "path";
826
+ import { parseArgs as parseArgs3 } from "util";
827
+ import { runEvalSuite } from "@assemble-dev/evals";
828
+ import { resolvePlatformConfig as resolvePlatformConfig2 } from "@assemble-dev/sdk";
829
+
830
+ // src/commands/eval-metric-table.ts
831
+ function buildEvalMetricTableLines(input) {
832
+ return [
833
+ { tone: "info", text: "" },
834
+ { tone: "info", text: `Suite: ${input.suiteName}` },
835
+ { tone: "info", text: "" },
836
+ ...buildEvalCaseRows(input.caseResults),
837
+ { tone: "info", text: "" },
838
+ ...buildEvalSummaryLines(input.summary)
839
+ ];
840
+ }
841
+ function buildEvalCaseRows(caseResults) {
842
+ const caseNameWidth = Math.max(
843
+ ...caseResults.map((caseResult) => caseResult.caseName.length),
844
+ 0
845
+ );
846
+ const lines = [];
847
+ for (const caseResult of caseResults) {
848
+ lines.push({
849
+ tone: caseResult.passed ? "success" : "failure",
850
+ text: `${caseResult.caseName.padEnd(caseNameWidth)} ${formatMetricCells(caseResult.metrics)}`
851
+ });
852
+ if (!caseResult.passed) {
853
+ lines.push(...buildFailedMetricReasonLines(caseResult.metrics));
854
+ }
855
+ }
856
+ return lines;
857
+ }
858
+ function formatMetricCells(metrics) {
859
+ return metrics.map(
860
+ (metric) => metric.skipped ? `${metric.name}:skipped` : `${metric.name}:${metric.score.toFixed(2)}`
861
+ ).join(" ");
862
+ }
863
+ function buildFailedMetricReasonLines(metrics) {
864
+ return metrics.filter((metric) => !metric.skipped && !metric.passed).map((metric) => ({
865
+ tone: "info",
866
+ text: ` ${metric.name}: ${metric.reason}`
867
+ }));
868
+ }
869
+ function buildEvalSummaryLines(summary) {
870
+ const passRatePercent = (summary.passRate * 100).toFixed(1);
871
+ return [
872
+ {
873
+ tone: summary.failedCases === 0 ? "success" : "failure",
874
+ text: `Cases: ${summary.passedCases} passed, ${summary.failedCases} failed (${passRatePercent}% pass rate)`
875
+ },
876
+ { tone: "info", text: buildEvalLatencyLine(summary) },
877
+ { tone: "info", text: buildEvalTokenLine(summary.performance.tokenUsage) }
878
+ ];
879
+ }
880
+ function buildEvalLatencyLine(summary) {
881
+ const { totalLatencyMs, averageLatencyMs } = summary.performance;
882
+ if (totalLatencyMs === null || averageLatencyMs === null) {
883
+ return "Latency: n/a";
884
+ }
885
+ return `Latency: total ${formatEvalLatencyMs(totalLatencyMs)}, avg ${formatEvalLatencyMs(averageLatencyMs)}`;
886
+ }
887
+ function formatEvalLatencyMs(latencyMs) {
888
+ if (latencyMs >= 1e3) {
889
+ return `${(latencyMs / 1e3).toFixed(1)}s`;
890
+ }
891
+ return `${Math.round(latencyMs)}ms`;
892
+ }
893
+ function buildEvalTokenLine(tokenUsage) {
894
+ if (tokenUsage === null) {
895
+ return "Tokens: n/a";
896
+ }
897
+ return `Tokens: ${tokenUsage.totalTokens} total (${tokenUsage.inputTokens} in / ${tokenUsage.outputTokens} out)`;
898
+ }
899
+
900
+ // src/commands/eval-suite-loader.ts
901
+ import { existsSync as existsSync3 } from "fs";
902
+ import { resolve as resolve3 } from "path";
903
+ import { createJiti as createJiti2 } from "jiti";
904
+ function isRunnableEvalSuiteDefinition(candidate) {
905
+ return typeof candidate === "object" && candidate !== null && typeof candidate.name === "string" && typeof candidate.workflow === "object" && candidate.workflow !== null && Array.isArray(candidate.cases);
906
+ }
907
+ function listEvalModuleExportNames(loadedModule) {
908
+ const exportNames = Object.keys(loadedModule);
909
+ return exportNames.length === 0 ? "(none)" : exportNames.join(", ");
910
+ }
911
+ function listEvalSuiteExportNames(loadedModule) {
912
+ return Object.entries(loadedModule).filter(([, candidate]) => isRunnableEvalSuiteDefinition(candidate)).map(([exportName]) => exportName);
913
+ }
914
+ async function importEvalSuiteModule(absoluteFilePath) {
915
+ const jiti = createJiti2(import.meta.url, { interopDefault: false });
916
+ try {
917
+ return await jiti.import(absoluteFilePath);
918
+ } catch (caughtError) {
919
+ const reason = caughtError instanceof Error ? caughtError.message : String(caughtError);
920
+ return `Could not load eval suite file ${absoluteFilePath}: ${reason}`;
921
+ }
922
+ }
923
+ function resolveNamedEvalSuiteExport(loadedModule, exportName, absoluteFilePath) {
924
+ const candidate = loadedModule[exportName];
925
+ if (isRunnableEvalSuiteDefinition(candidate)) {
926
+ return { outcome: "loaded", suiteDefinition: candidate };
927
+ }
928
+ return {
929
+ outcome: "failed",
930
+ message: `Export "${exportName}" in ${absoluteFilePath} is not an eval suite. Available exports: ${listEvalModuleExportNames(loadedModule)}`
931
+ };
932
+ }
933
+ function resolveDiscoveredEvalSuiteExport(loadedModule, absoluteFilePath) {
934
+ const defaultExport = loadedModule.default;
935
+ if (isRunnableEvalSuiteDefinition(defaultExport)) {
936
+ return { outcome: "loaded", suiteDefinition: defaultExport };
937
+ }
938
+ const suiteExportNames = listEvalSuiteExportNames(loadedModule);
939
+ const singleSuiteExportName = suiteExportNames[0];
940
+ if (suiteExportNames.length === 1 && singleSuiteExportName !== void 0) {
941
+ return resolveNamedEvalSuiteExport(
942
+ loadedModule,
943
+ singleSuiteExportName,
944
+ absoluteFilePath
945
+ );
946
+ }
947
+ if (suiteExportNames.length === 0) {
948
+ return {
949
+ outcome: "failed",
950
+ message: `No eval suite export found in ${absoluteFilePath}. Available exports: ${listEvalModuleExportNames(loadedModule)}. Export a suite as the default export or pass --suite <exportName>.`
951
+ };
952
+ }
953
+ return {
954
+ outcome: "failed",
955
+ message: `Multiple eval suite exports found in ${absoluteFilePath}: ${suiteExportNames.join(", ")}. Pass --suite <exportName> to choose one.`
956
+ };
957
+ }
958
+ async function loadEvalSuiteFromFile(input) {
959
+ const absoluteFilePath = resolve3(input.filePath);
960
+ if (!existsSync3(absoluteFilePath)) {
961
+ return {
962
+ outcome: "failed",
963
+ message: `Eval suite file not found: ${absoluteFilePath}`
964
+ };
965
+ }
966
+ const importResult = await importEvalSuiteModule(absoluteFilePath);
967
+ if (typeof importResult === "string") {
968
+ return { outcome: "failed", message: importResult };
969
+ }
970
+ if (input.exportName !== void 0) {
971
+ return resolveNamedEvalSuiteExport(
972
+ importResult,
973
+ input.exportName,
974
+ absoluteFilePath
975
+ );
976
+ }
977
+ return resolveDiscoveredEvalSuiteExport(importResult, absoluteFilePath);
978
+ }
979
+
980
+ // src/commands/eval.ts
981
+ var evalCommandUsage = "Usage: asm eval <suite-file> [--suite <exportName>] [--results-dir <dir>] [--no-upload]";
982
+ function parseEvalFlags(args) {
983
+ try {
984
+ const { values, positionals } = parseArgs3({
985
+ args,
986
+ options: {
987
+ suite: { type: "string" },
988
+ "results-dir": { type: "string" },
989
+ "no-upload": { type: "boolean" }
990
+ },
991
+ strict: true,
992
+ allowPositionals: true
993
+ });
994
+ const filePath = positionals[0];
995
+ if (filePath === void 0 || positionals.length > 1) {
996
+ return null;
997
+ }
998
+ return {
999
+ filePath,
1000
+ suiteExportName: values.suite,
1001
+ resultsDirectory: values["results-dir"],
1002
+ noUpload: values["no-upload"] ?? false
1003
+ };
1004
+ } catch {
1005
+ return null;
1006
+ }
1007
+ }
1008
+ function buildEvalPlatformOptions(runtime, noUpload) {
1009
+ if (noUpload) {
1010
+ return {
1011
+ environmentVariables: {},
1012
+ homeDirectory: join3(tmpdir(), "assemble-cli-no-upload-home")
1013
+ };
1014
+ }
1015
+ return {
1016
+ environmentVariables: runtime.env,
1017
+ homeDirectory: runtime.homeDir,
1018
+ fetchImplementation: runtime.fetchImplementation
1019
+ };
1020
+ }
1021
+ function buildRunEvalSuiteOptions(runtime, flags) {
1022
+ const options = {
1023
+ platform: buildEvalPlatformOptions(runtime, flags.noUpload),
1024
+ writeLine: flags.noUpload ? () => void 0 : runtime.writeLine
1025
+ };
1026
+ if (flags.resultsDirectory !== void 0) {
1027
+ options.resultsDirectory = flags.resultsDirectory;
1028
+ }
1029
+ return options;
1030
+ }
1031
+ function writeEvalTableLines(runtime, output, lines) {
1032
+ for (const line of lines) {
1033
+ if (line.tone === "success") {
1034
+ output.writeSuccessLine(line.text);
1035
+ } else if (line.tone === "failure") {
1036
+ output.writeFailureLine(line.text);
1037
+ } else {
1038
+ runtime.writeLine(line.text);
1039
+ }
1040
+ }
1041
+ }
1042
+ function writeEvalResultLocationLines(runtime, result) {
1043
+ runtime.writeLine(`Results: ${result.resultsFilePath}`);
1044
+ if (result.uploadStatus === "uploaded") {
1045
+ const platformConfig = resolvePlatformConfig2({
1046
+ homeDirectory: runtime.homeDir,
1047
+ environmentVariables: runtime.env
1048
+ });
1049
+ runtime.writeLine(
1050
+ `Eval: ${platformConfig.dashboardUrl}/app/evals/${result.evalRunId}`
1051
+ );
1052
+ }
1053
+ if (result.uploadStatus === "failed") {
1054
+ runtime.writeLine(
1055
+ "Upload failed; the results file above is still saved locally."
1056
+ );
1057
+ }
1058
+ runtime.writeLine("");
1059
+ }
1060
+ async function executeEvalSuiteRun(runtime, output, flags, suiteDefinition) {
1061
+ try {
1062
+ const result = await runEvalSuite({
1063
+ suite: suiteDefinition,
1064
+ options: buildRunEvalSuiteOptions(runtime, flags)
1065
+ });
1066
+ writeEvalTableLines(
1067
+ runtime,
1068
+ output,
1069
+ buildEvalMetricTableLines({
1070
+ suiteName: suiteDefinition.name,
1071
+ caseResults: result.caseResults,
1072
+ summary: result.summary
1073
+ })
1074
+ );
1075
+ writeEvalResultLocationLines(runtime, result);
1076
+ return result.summary.failedCases > 0 ? 1 : 0;
1077
+ } catch (caughtError) {
1078
+ const reason = caughtError instanceof Error ? caughtError.message : String(caughtError);
1079
+ output.writeFailureLine(`Eval run failed: ${reason}`);
1080
+ return 1;
1081
+ }
1082
+ }
1083
+ async function runEvalCommand(args, runtime) {
1084
+ const output = createCliOutput(runtime.writeLine, resolveUseColor(runtime.env));
1085
+ const flags = parseEvalFlags(args);
1086
+ if (flags === null) {
1087
+ output.writeFailureLine(`Unrecognized eval arguments. ${evalCommandUsage}`);
1088
+ return 1;
1089
+ }
1090
+ const loadResult = await loadEvalSuiteFromFile({
1091
+ filePath: flags.filePath,
1092
+ exportName: flags.suiteExportName
1093
+ });
1094
+ if (loadResult.outcome === "failed") {
1095
+ output.writeFailureLine(loadResult.message);
1096
+ return 1;
1097
+ }
1098
+ return await executeEvalSuiteRun(
1099
+ runtime,
1100
+ output,
1101
+ flags,
1102
+ loadResult.suiteDefinition
1103
+ );
1104
+ }
1105
+
1106
+ // src/commands/init.ts
1107
+ import {
1108
+ existsSync as existsSync4,
1109
+ mkdirSync as mkdirSync2,
1110
+ readdirSync,
1111
+ statSync as statSync2,
1112
+ writeFileSync as writeFileSync2
1113
+ } from "fs";
1114
+ import { dirname, join as join4, resolve as resolve4 } from "path";
1115
+ import { parseArgs as parseArgs4 } from "util";
1116
+
1117
+ // src/templates/starter-template.ts
1118
+ var defaultStarterDirectoryName = "my-assemble-app";
1119
+ var starterPackageJson = {
1120
+ name: "my-assemble-app",
1121
+ version: "0.1.0",
1122
+ private: true,
1123
+ type: "module",
1124
+ scripts: {
1125
+ run: "asm run src/workflow.ts"
1126
+ },
1127
+ dependencies: {
1128
+ "@assemble-dev/providers": "^0.0.0",
1129
+ "@assemble-dev/sdk": "^0.0.0",
1130
+ zod: "^4.1.13"
1131
+ }
1132
+ };
1133
+ var starterTsconfig = {
1134
+ compilerOptions: {
1135
+ target: "ES2022",
1136
+ module: "ESNext",
1137
+ moduleResolution: "bundler",
1138
+ strict: true,
1139
+ skipLibCheck: true,
1140
+ noEmit: true
1141
+ },
1142
+ include: ["src"]
1143
+ };
1144
+ var starterEnvExampleContents = `# Provider API keys are optional for the starter workflow.
1145
+ # The sample workflow in src/workflow.ts uses mockModel() and needs no keys.
1146
+ # Add your own keys here when you switch an agent to a real model.
1147
+ OPENAI_API_KEY=
1148
+ ANTHROPIC_API_KEY=
1149
+ GOOGLE_API_KEY=
1150
+ ASSEMBLE_API_KEY=
1151
+ `;
1152
+ var starterGitignoreContents = `node_modules
1153
+ .env
1154
+ .assemble
1155
+ `;
1156
+ var starterWorkflowTemplateContents = `import { mockModel } from "@assemble-dev/providers";
1157
+ import { agent, workflow } from "@assemble-dev/sdk";
1158
+ import { z } from "zod";
1159
+
1160
+ const StoryInputSchema = z.object({ topic: z.string().min(1) });
1161
+
1162
+ const OutlineSchema = z.object({ outline: z.array(z.string()).min(1) });
1163
+
1164
+ const StorySchema = z.object({ story: z.string().min(1) });
1165
+
1166
+ const outlinerAgent = agent({
1167
+ name: "outliner",
1168
+ role: "Plans a short story outline",
1169
+ model: mockModel({
1170
+ scriptedResponses: [
1171
+ '{"outline":["Introduce the hero","Resolve the quest"]}',
1172
+ ],
1173
+ }),
1174
+ instructions: "Turn the topic into a two-beat story outline.",
1175
+ outputSchema: OutlineSchema,
1176
+ });
1177
+
1178
+ const writerAgent = agent({
1179
+ name: "writer",
1180
+ role: "Writes a short story from the outline",
1181
+ model: mockModel({
1182
+ scriptedResponses: [
1183
+ '{"story":"The hero set out, faced the quest, and returned home changed."}',
1184
+ ],
1185
+ }),
1186
+ instructions: "Write a short story that follows the outline.",
1187
+ outputSchema: StorySchema,
1188
+ });
1189
+
1190
+ export const starterWorkflow = workflow({
1191
+ name: "starter-story",
1192
+ inputSchema: StoryInputSchema,
1193
+ outputSchema: StorySchema,
1194
+ agents: [outlinerAgent, writerAgent],
1195
+ topology: { kind: "pipeline" },
1196
+ });
1197
+
1198
+ export default starterWorkflow;
1199
+ `;
1200
+ var starterTemplateFiles = [
1201
+ {
1202
+ relativePath: "package.json",
1203
+ contents: `${JSON.stringify(starterPackageJson, null, 2)}
1204
+ `
1205
+ },
1206
+ {
1207
+ relativePath: "tsconfig.json",
1208
+ contents: `${JSON.stringify(starterTsconfig, null, 2)}
1209
+ `
1210
+ },
1211
+ {
1212
+ relativePath: ".env.example",
1213
+ contents: starterEnvExampleContents
1214
+ },
1215
+ {
1216
+ relativePath: ".gitignore",
1217
+ contents: starterGitignoreContents
1218
+ },
1219
+ {
1220
+ relativePath: "src/workflow.ts",
1221
+ contents: starterWorkflowTemplateContents
1222
+ }
1223
+ ];
1224
+
1225
+ // src/commands/init.ts
1226
+ function parseInitFlags(args) {
1227
+ try {
1228
+ const { values, positionals } = parseArgs4({
1229
+ args,
1230
+ options: {
1231
+ force: { type: "boolean", default: false }
1232
+ },
1233
+ strict: true,
1234
+ allowPositionals: true
1235
+ });
1236
+ if (positionals.length > 1) {
1237
+ return null;
1238
+ }
1239
+ return {
1240
+ force: values.force,
1241
+ targetDirectoryArg: positionals[0] ?? defaultStarterDirectoryName
1242
+ };
1243
+ } catch {
1244
+ return null;
1245
+ }
1246
+ }
1247
+ function resolveTargetDirectoryState(targetDirectory) {
1248
+ if (!existsSync4(targetDirectory)) {
1249
+ return "available";
1250
+ }
1251
+ if (!statSync2(targetDirectory).isDirectory()) {
1252
+ return "existing-file";
1253
+ }
1254
+ if (readdirSync(targetDirectory).length > 0) {
1255
+ return "non-empty-directory";
1256
+ }
1257
+ return "available";
1258
+ }
1259
+ function writeStarterTemplateFiles(targetDirectory) {
1260
+ for (const templateFile of starterTemplateFiles) {
1261
+ const filePath = join4(targetDirectory, templateFile.relativePath);
1262
+ mkdirSync2(dirname(filePath), { recursive: true });
1263
+ writeFileSync2(filePath, templateFile.contents);
1264
+ }
1265
+ }
1266
+ function writeInitSuccessOutput(output, targetDirectoryArg) {
1267
+ output.writeSuccessLine(
1268
+ `Scaffolded Assemble starter project in ${targetDirectoryArg}`
1269
+ );
1270
+ output.writeInfoLine("");
1271
+ output.writeInfoLine("Created files:");
1272
+ for (const templateFile of starterTemplateFiles) {
1273
+ output.writeInfoLine(` ${templateFile.relativePath}`);
1274
+ }
1275
+ output.writeInfoLine("");
1276
+ output.writeInfoLine("Next steps:");
1277
+ output.writeInfoLine(` cd ${targetDirectoryArg}`);
1278
+ output.writeInfoLine(" pnpm install");
1279
+ output.writeInfoLine(" asm run src/workflow.ts");
1280
+ output.writeInfoLine("");
1281
+ output.writeInfoLine(
1282
+ "The sample workflow runs on mockModel and needs no API keys. Copy .env.example to .env when you switch to real provider models."
1283
+ );
1284
+ }
1285
+ function runInitCommand(args, runtime, options = {}) {
1286
+ const output = createCliOutput(runtime.writeLine, resolveUseColor(runtime.env));
1287
+ const flags = parseInitFlags(args);
1288
+ if (flags === null) {
1289
+ output.writeFailureLine(
1290
+ "Unrecognized init arguments. Usage: asm init [dir] [--force]"
1291
+ );
1292
+ return 1;
1293
+ }
1294
+ const workingDirectory = options.cwd ?? process.cwd();
1295
+ const targetDirectory = resolve4(workingDirectory, flags.targetDirectoryArg);
1296
+ const targetDirectoryState = resolveTargetDirectoryState(targetDirectory);
1297
+ if (targetDirectoryState === "existing-file") {
1298
+ output.writeFailureLine(
1299
+ `${flags.targetDirectoryArg} already exists and is not a directory. Pick a different directory name.`
1300
+ );
1301
+ return 1;
1302
+ }
1303
+ if (targetDirectoryState === "non-empty-directory" && !flags.force) {
1304
+ output.writeFailureLine(
1305
+ `${flags.targetDirectoryArg} already exists and is not empty. Pass --force to write the starter files into it anyway.`
1306
+ );
1307
+ return 1;
1308
+ }
1309
+ writeStarterTemplateFiles(targetDirectory);
1310
+ writeInitSuccessOutput(output, flags.targetDirectoryArg);
1311
+ return 0;
1312
+ }
1313
+
1314
+ // src/commands/login.ts
1315
+ import { parseArgs as parseArgs5 } from "util";
1316
+
1317
+ // src/config/api-key-format.ts
1318
+ var rawApiKeyPattern = /^asm_(dev|prod|ci)_[0-9a-f]{32}_[0-9a-f]{64}$/;
1319
+ var expectedRawApiKeyFormat = "asm_<dev|prod|ci>_<32 hex characters>_<64 hex characters>";
1320
+ var maskedKeyLast4Length = 4;
1321
+ function validateRawApiKeyFormat(rawKey) {
1322
+ return rawApiKeyPattern.test(rawKey);
1323
+ }
1324
+ function maskRawApiKey(rawKey) {
1325
+ const last4 = rawKey.slice(-maskedKeyLast4Length);
1326
+ const match = rawApiKeyPattern.exec(rawKey);
1327
+ if (match === null) {
1328
+ return `\u2026${last4}`;
1329
+ }
1330
+ const environment = match[1];
1331
+ return `asm_${environment}_\u2026${last4}`;
1332
+ }
1333
+
1334
+ // src/config/cli-config.ts
1335
+ import {
1336
+ chmodSync,
1337
+ existsSync as existsSync5,
1338
+ mkdirSync as mkdirSync3,
1339
+ readFileSync as readFileSync2,
1340
+ writeFileSync as writeFileSync3
1341
+ } from "fs";
1342
+ import { dirname as dirname2, join as join5 } from "path";
1343
+ import { z as z2 } from "zod";
1344
+ var defaultCliBaseUrl = "https://api.assemble.dev";
1345
+ var cliDashboardKeysUrl = "https://app.assemble.dev/app/keys";
1346
+ var CliConfigFileSchema = z2.object({
1347
+ apiKey: z2.string().min(1).optional(),
1348
+ baseUrl: z2.string().min(1).optional(),
1349
+ workspaceId: z2.string().min(1).optional()
1350
+ });
1351
+ var configDirectoryMode = 448;
1352
+ var configFileMode = 384;
1353
+ function resolveCliConfigFilePath(homeDir) {
1354
+ return join5(homeDir, ".assemble", "config.json");
1355
+ }
1356
+ function readCliConfigFile(homeDir) {
1357
+ const configFilePath = resolveCliConfigFilePath(homeDir);
1358
+ if (!existsSync5(configFilePath)) {
1359
+ return { config: {}, warning: null };
1360
+ }
1361
+ try {
1362
+ const rawContents = readFileSync2(configFilePath, "utf8");
1363
+ return {
1364
+ config: CliConfigFileSchema.parse(JSON.parse(rawContents)),
1365
+ warning: null
1366
+ };
1367
+ } catch {
1368
+ return {
1369
+ config: {},
1370
+ warning: `Warning: could not parse ${configFilePath}; treating it as empty.`
1371
+ };
1372
+ }
1373
+ }
1374
+ function writeCliConfigFile(homeDir, config) {
1375
+ const configFilePath = resolveCliConfigFilePath(homeDir);
1376
+ const configDirectoryPath = dirname2(configFilePath);
1377
+ mkdirSync3(configDirectoryPath, {
1378
+ recursive: true,
1379
+ mode: configDirectoryMode
1380
+ });
1381
+ chmodSync(configDirectoryPath, configDirectoryMode);
1382
+ writeFileSync3(configFilePath, `${JSON.stringify(config, null, 2)}
1383
+ `, {
1384
+ encoding: "utf8",
1385
+ mode: configFileMode
1386
+ });
1387
+ chmodSync(configFilePath, configFileMode);
1388
+ return configFilePath;
1389
+ }
1390
+
1391
+ // src/config/cli-env.ts
1392
+ import { z as z3 } from "zod";
1393
+ var CliEnvSchema = z3.object({
1394
+ ASSEMBLE_API_KEY: z3.string().min(1).optional(),
1395
+ ASSEMBLE_BASE_URL: z3.string().min(1).optional(),
1396
+ NO_COLOR: z3.string().optional()
1397
+ });
1398
+ function parseCliEnvironment(environmentVariables) {
1399
+ const parsedEnvironment = CliEnvSchema.safeParse(environmentVariables);
1400
+ if (parsedEnvironment.success) {
1401
+ return parsedEnvironment.data;
1402
+ }
1403
+ return {};
1404
+ }
1405
+
1406
+ // src/platform/auth-check.ts
1407
+ import { AppErrorCode } from "@assemble-dev/shared-types";
1408
+ import { z as z4 } from "zod";
1409
+ var AuthCheckResponseSchema = z4.object({
1410
+ workspaceId: z4.string().min(1)
1411
+ });
1412
+ var AuthCheckErrorResponseSchema = z4.object({
1413
+ error: z4.object({
1414
+ code: z4.enum(AppErrorCode),
1415
+ message: z4.string()
1416
+ })
1417
+ });
1418
+ function resolveAuthCheckUrl(baseUrl) {
1419
+ return `${baseUrl.replace(/\/+$/, "")}/v1/auth-check`;
1420
+ }
1421
+ function parseUnauthorizedServerMessage(rawBody) {
1422
+ try {
1423
+ const parsedBody = AuthCheckErrorResponseSchema.parse(JSON.parse(rawBody));
1424
+ return parsedBody.error.message;
1425
+ } catch {
1426
+ return null;
1427
+ }
1428
+ }
1429
+ async function checkApiKeyWithPlatform(input) {
1430
+ try {
1431
+ const response = await input.fetchImplementation(
1432
+ resolveAuthCheckUrl(input.baseUrl),
1433
+ {
1434
+ method: "GET",
1435
+ headers: { authorization: `Bearer ${input.rawKey}` }
1436
+ }
1437
+ );
1438
+ if (response.status === 401) {
1439
+ return {
1440
+ outcome: "unauthorized",
1441
+ serverMessage: parseUnauthorizedServerMessage(await response.text())
1442
+ };
1443
+ }
1444
+ if (!response.ok) {
1445
+ return {
1446
+ outcome: "request-failed",
1447
+ message: `the auth check returned status ${response.status}`
1448
+ };
1449
+ }
1450
+ const responseBody = AuthCheckResponseSchema.parse(
1451
+ JSON.parse(await response.text())
1452
+ );
1453
+ return { outcome: "authorized", workspaceId: responseBody.workspaceId };
1454
+ } catch (caughtError) {
1455
+ return {
1456
+ outcome: "request-failed",
1457
+ message: caughtError instanceof Error ? caughtError.message : String(caughtError)
1458
+ };
1459
+ }
1460
+ }
1461
+
1462
+ // src/commands/login.ts
1463
+ function parseLoginFlags(args) {
1464
+ try {
1465
+ const { values } = parseArgs5({
1466
+ args,
1467
+ options: {
1468
+ key: { type: "string" },
1469
+ "base-url": { type: "string" }
1470
+ },
1471
+ strict: true,
1472
+ allowPositionals: false
1473
+ });
1474
+ return { key: values.key, baseUrl: values["base-url"] };
1475
+ } catch {
1476
+ return null;
1477
+ }
1478
+ }
1479
+ async function resolveLoginKey(flags, runtime) {
1480
+ if (flags.key !== void 0) {
1481
+ return flags.key.trim();
1482
+ }
1483
+ if (runtime.readLine !== void 0) {
1484
+ const lineFromStdin = await runtime.readLine();
1485
+ return lineFromStdin.trim();
1486
+ }
1487
+ return null;
1488
+ }
1489
+ function resolveLoginBaseUrl(flags, runtime, configFileResult) {
1490
+ const environment = parseCliEnvironment(runtime.env);
1491
+ return flags.baseUrl ?? environment.ASSEMBLE_BASE_URL ?? configFileResult.config.baseUrl ?? defaultCliBaseUrl;
1492
+ }
1493
+ async function validateAndSaveApiKey(rawKey, flags, runtime, output) {
1494
+ const configFileResult = readCliConfigFile(runtime.homeDir);
1495
+ if (configFileResult.warning !== null) {
1496
+ output.writeInfoLine(configFileResult.warning);
1497
+ }
1498
+ const baseUrl = resolveLoginBaseUrl(flags, runtime, configFileResult);
1499
+ const checkResult = await checkApiKeyWithPlatform({
1500
+ baseUrl,
1501
+ rawKey,
1502
+ fetchImplementation: runtime.fetchImplementation
1503
+ });
1504
+ if (checkResult.outcome === "unauthorized") {
1505
+ const rejectionDetail = checkResult.serverMessage === null ? "" : ` (${checkResult.serverMessage})`;
1506
+ output.writeFailureLine(
1507
+ `This API key was rejected${rejectionDetail}. It may have been revoked. Create a new key at ${cliDashboardKeysUrl}`
1508
+ );
1509
+ return 1;
1510
+ }
1511
+ if (checkResult.outcome === "request-failed") {
1512
+ output.writeFailureLine(
1513
+ `Could not reach ${baseUrl}: ${checkResult.message}. Check your network connection or pass a different --base-url.`
1514
+ );
1515
+ return 1;
1516
+ }
1517
+ const configFilePath = writeCliConfigFile(runtime.homeDir, {
1518
+ ...configFileResult.config,
1519
+ apiKey: rawKey,
1520
+ workspaceId: checkResult.workspaceId,
1521
+ ...flags.baseUrl === void 0 ? {} : { baseUrl: flags.baseUrl }
1522
+ });
1523
+ output.writeSuccessLine(
1524
+ `Logged in to workspace ${checkResult.workspaceId} with key ${maskRawApiKey(rawKey)}`
1525
+ );
1526
+ output.writeInfoLine(`Credentials saved to ${configFilePath}`);
1527
+ return 0;
1528
+ }
1529
+ async function runLoginCommand(args, runtime) {
1530
+ const output = createCliOutput(runtime.writeLine, resolveUseColor(runtime.env));
1531
+ const flags = parseLoginFlags(args);
1532
+ if (flags === null) {
1533
+ output.writeFailureLine(
1534
+ "Unrecognized login arguments. Usage: asm login --key <api-key> [--base-url <url>]"
1535
+ );
1536
+ return 1;
1537
+ }
1538
+ const rawKey = await resolveLoginKey(flags, runtime);
1539
+ if (rawKey === null || rawKey === "") {
1540
+ output.writeFailureLine(
1541
+ "No API key provided. Pass one with --key <api-key> or pipe it on stdin."
1542
+ );
1543
+ return 1;
1544
+ }
1545
+ if (!validateRawApiKeyFormat(rawKey)) {
1546
+ output.writeFailureLine(
1547
+ `That does not look like an Assemble API key. Expected format: ${expectedRawApiKeyFormat}`
1548
+ );
1549
+ return 1;
1550
+ }
1551
+ return await validateAndSaveApiKey(rawKey, flags, runtime, output);
1552
+ }
1553
+
1554
+ // src/commands/logout.ts
1555
+ function runLogoutCommand(runtime) {
1556
+ const output = createCliOutput(runtime.writeLine, resolveUseColor(runtime.env));
1557
+ const configFileResult = readCliConfigFile(runtime.homeDir);
1558
+ if (configFileResult.warning !== null) {
1559
+ output.writeInfoLine(configFileResult.warning);
1560
+ }
1561
+ if (configFileResult.config.apiKey === void 0 && configFileResult.config.workspaceId === void 0) {
1562
+ output.writeInfoLine("You are already logged out.");
1563
+ return 0;
1564
+ }
1565
+ const remainingConfig = { ...configFileResult.config };
1566
+ delete remainingConfig.apiKey;
1567
+ delete remainingConfig.workspaceId;
1568
+ const configFilePath = writeCliConfigFile(runtime.homeDir, remainingConfig);
1569
+ output.writeSuccessLine(
1570
+ `Logged out. Credentials removed from ${configFilePath}`
1571
+ );
1572
+ return 0;
1573
+ }
1574
+
1575
+ // src/commands/replay.ts
1576
+ import { parseArgs as parseArgs6 } from "util";
1577
+ import {
1578
+ compareRunToReplay,
1579
+ defaultRunStateDirectory,
1580
+ defaultTraceDirectory as defaultTraceDirectory2,
1581
+ loadReplaySource,
1582
+ replayRun
1583
+ } from "@assemble-dev/sdk";
1584
+ import { z as z5 } from "zod";
1585
+
1586
+ // src/commands/replay-comparison-format.ts
1587
+ var maxPrintedReplayDiffRows = 10;
1588
+ var maxDiffCellLength = 48;
1589
+ function buildReplayComparisonLines(input) {
1590
+ return [
1591
+ { tone: "info", text: "" },
1592
+ { tone: "info", text: `Replay of run ${input.sourceRunId} completed.` },
1593
+ { tone: "info", text: "" },
1594
+ { tone: "info", text: `Replay Run ID: ${input.replayRunId}` },
1595
+ ...buildFromStepLines(input.fromStepIndex),
1596
+ buildStatusLine(input.comparison),
1597
+ ...buildOutputLines(input.comparison),
1598
+ { tone: "info", text: buildLatencyLine(input.comparison) },
1599
+ buildRoutingLine(input.comparison),
1600
+ { tone: "info", text: "" }
1601
+ ];
1602
+ }
1603
+ function buildFromStepLines(fromStepIndex) {
1604
+ if (fromStepIndex === 0) {
1605
+ return [];
1606
+ }
1607
+ return [{ tone: "info", text: `Replayed from step ${fromStepIndex}` }];
1608
+ }
1609
+ function buildStatusLine(comparison) {
1610
+ return {
1611
+ tone: comparison.statusMatches ? "success" : "failure",
1612
+ text: `Status: ${comparison.originalStatus} (original) vs ${comparison.replayStatus} (replay)`
1613
+ };
1614
+ }
1615
+ function buildOutputLines(comparison) {
1616
+ if (comparison.outputMatches) {
1617
+ return [{ tone: "success", text: "Output matches" }];
1618
+ }
1619
+ const differenceCount = comparison.outputDiff.length;
1620
+ const printedDiffs = comparison.outputDiff.slice(0, maxPrintedReplayDiffRows);
1621
+ const lines = [
1622
+ {
1623
+ tone: "failure",
1624
+ text: `Output differs (${differenceCount} difference${differenceCount === 1 ? "" : "s"})`
1625
+ },
1626
+ ...buildAlignedDiffRows(printedDiffs)
1627
+ ];
1628
+ if (differenceCount > maxPrintedReplayDiffRows) {
1629
+ lines.push({
1630
+ tone: "info",
1631
+ text: ` \u2026 and ${differenceCount - maxPrintedReplayDiffRows} more differences`
1632
+ });
1633
+ }
1634
+ return lines;
1635
+ }
1636
+ function buildAlignedDiffRows(diffs) {
1637
+ const rows = diffs.map((diff) => ({
1638
+ path: diff.path,
1639
+ original: formatDiffCellValue(diff.original),
1640
+ replay: formatDiffCellValue(diff.replay)
1641
+ }));
1642
+ const pathWidth = Math.max(...rows.map((row) => row.path.length), 0);
1643
+ const originalWidth = Math.max(...rows.map((row) => row.original.length), 0);
1644
+ return rows.map((row) => ({
1645
+ tone: "info",
1646
+ text: ` ${row.path.padEnd(pathWidth)} | ${row.original.padEnd(originalWidth)} | ${row.replay}`
1647
+ }));
1648
+ }
1649
+ function formatDiffCellValue(value) {
1650
+ const serialized = JSON.stringify(value);
1651
+ if (serialized.length <= maxDiffCellLength) {
1652
+ return serialized;
1653
+ }
1654
+ return `${serialized.slice(0, maxDiffCellLength - 1)}\u2026`;
1655
+ }
1656
+ function formatReplayLatencyMs(latencyMs) {
1657
+ if (latencyMs === null) {
1658
+ return "n/a";
1659
+ }
1660
+ if (Math.abs(latencyMs) >= 1e3) {
1661
+ return `${(latencyMs / 1e3).toFixed(1)}s`;
1662
+ }
1663
+ return `${Math.round(latencyMs)}ms`;
1664
+ }
1665
+ function buildLatencyLine(comparison) {
1666
+ const { originalMs, replayMs, deltaMs } = comparison.latency;
1667
+ const baseText = `Latency: original ${formatReplayLatencyMs(originalMs)}, replay ${formatReplayLatencyMs(replayMs)}`;
1668
+ if (deltaMs === null) {
1669
+ return baseText;
1670
+ }
1671
+ const signedDelta = deltaMs >= 0 ? `+${formatReplayLatencyMs(deltaMs)}` : `-${formatReplayLatencyMs(Math.abs(deltaMs))}`;
1672
+ return `${baseText} (delta ${signedDelta})`;
1673
+ }
1674
+ function buildRoutingLine(comparison) {
1675
+ return {
1676
+ tone: comparison.routingMatches ? "success" : "failure",
1677
+ text: comparison.routingMatches ? "Routing matches" : "Routing differs"
1678
+ };
1679
+ }
1680
+
1681
+ // src/commands/replay-last-run.ts
1682
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1683
+ import { join as join6 } from "path";
1684
+ var lastRunFileRelativePath = join6(".assemble", "last-run");
1685
+ function resolveLastRunFilePath(workingDirectory) {
1686
+ return join6(workingDirectory, lastRunFileRelativePath);
1687
+ }
1688
+ function readLastRunIdFromFile(workingDirectory) {
1689
+ const lastRunFilePath = resolveLastRunFilePath(workingDirectory);
1690
+ if (!existsSync6(lastRunFilePath)) {
1691
+ return null;
1692
+ }
1693
+ const fileContents = readFileSync3(lastRunFilePath, "utf8").trim();
1694
+ return fileContents === "" ? null : fileContents;
1695
+ }
1696
+
1697
+ // src/commands/replay-workflow-loader.ts
1698
+ import { existsSync as existsSync7 } from "fs";
1699
+ import { resolve as resolve5 } from "path";
1700
+ import { createJiti as createJiti3 } from "jiti";
1701
+ function isReplayableWorkflowDefinition(candidate) {
1702
+ return typeof candidate === "object" && candidate !== null && typeof candidate.name === "string" && typeof candidate.run === "function" && candidate.inputSchema !== void 0 && Array.isArray(candidate.agents);
1703
+ }
1704
+ function listReplayModuleExportNames(loadedModule) {
1705
+ const exportNames = Object.keys(loadedModule);
1706
+ return exportNames.length === 0 ? "(none)" : exportNames.join(", ");
1707
+ }
1708
+ function listReplayWorkflowExportNames(loadedModule) {
1709
+ return Object.entries(loadedModule).filter(([, candidate]) => isReplayableWorkflowDefinition(candidate)).map(([exportName]) => exportName);
1710
+ }
1711
+ async function importReplayWorkflowModule(absoluteFilePath) {
1712
+ const jiti = createJiti3(import.meta.url, { interopDefault: false });
1713
+ try {
1714
+ return await jiti.import(absoluteFilePath);
1715
+ } catch (caughtError) {
1716
+ const reason = caughtError instanceof Error ? caughtError.message : String(caughtError);
1717
+ return `Could not load workflow file ${absoluteFilePath}: ${reason}`;
1718
+ }
1719
+ }
1720
+ function resolveNamedReplayWorkflowExport(loadedModule, exportName, absoluteFilePath) {
1721
+ const candidate = loadedModule[exportName];
1722
+ if (isReplayableWorkflowDefinition(candidate)) {
1723
+ return { outcome: "loaded", workflowDefinition: candidate };
1724
+ }
1725
+ return {
1726
+ outcome: "failed",
1727
+ message: `Export "${exportName}" in ${absoluteFilePath} is not a workflow. Available exports: ${listReplayModuleExportNames(loadedModule)}`
1728
+ };
1729
+ }
1730
+ function resolveDiscoveredReplayWorkflowExport(loadedModule, absoluteFilePath) {
1731
+ const defaultExport = loadedModule.default;
1732
+ if (isReplayableWorkflowDefinition(defaultExport)) {
1733
+ return { outcome: "loaded", workflowDefinition: defaultExport };
1734
+ }
1735
+ const workflowExportNames = listReplayWorkflowExportNames(loadedModule);
1736
+ const singleWorkflowExportName = workflowExportNames[0];
1737
+ if (workflowExportNames.length === 1 && singleWorkflowExportName !== void 0) {
1738
+ return resolveNamedReplayWorkflowExport(
1739
+ loadedModule,
1740
+ singleWorkflowExportName,
1741
+ absoluteFilePath
1742
+ );
1743
+ }
1744
+ if (workflowExportNames.length === 0) {
1745
+ return {
1746
+ outcome: "failed",
1747
+ message: `No workflow export found in ${absoluteFilePath}. Available exports: ${listReplayModuleExportNames(loadedModule)}. Export a workflow as the default export or pass --workflow <exportName>.`
1748
+ };
1749
+ }
1750
+ return {
1751
+ outcome: "failed",
1752
+ message: `Multiple workflow exports found in ${absoluteFilePath}: ${workflowExportNames.join(", ")}. Pass --workflow <exportName> to choose one.`
1753
+ };
1754
+ }
1755
+ async function loadReplayWorkflowFromFile(input) {
1756
+ const absoluteFilePath = resolve5(input.filePath);
1757
+ if (!existsSync7(absoluteFilePath)) {
1758
+ return {
1759
+ outcome: "failed",
1760
+ message: `Workflow file not found: ${absoluteFilePath}`
1761
+ };
1762
+ }
1763
+ const importResult = await importReplayWorkflowModule(absoluteFilePath);
1764
+ if (typeof importResult === "string") {
1765
+ return { outcome: "failed", message: importResult };
1766
+ }
1767
+ if (input.exportName !== void 0) {
1768
+ return resolveNamedReplayWorkflowExport(
1769
+ importResult,
1770
+ input.exportName,
1771
+ absoluteFilePath
1772
+ );
1773
+ }
1774
+ return resolveDiscoveredReplayWorkflowExport(importResult, absoluteFilePath);
1775
+ }
1776
+
1777
+ // src/commands/replay.ts
1778
+ var replayCommandUsage = "Usage: asm replay [runId] --workflow-file <file> [--workflow <exportName>] [--from-step <n>] [--no-mock-tools] [--trace-dir <dir>] [--state-dir <dir>]";
1779
+ var FromStepSchema = z5.coerce.number().int().min(0);
1780
+ function parseReplayFlags(args) {
1781
+ try {
1782
+ const { values, positionals } = parseArgs6({
1783
+ args,
1784
+ options: {
1785
+ "workflow-file": { type: "string" },
1786
+ workflow: { type: "string" },
1787
+ "from-step": { type: "string" },
1788
+ "mock-tools": { type: "boolean" },
1789
+ "no-mock-tools": { type: "boolean" },
1790
+ "trace-dir": { type: "string" },
1791
+ "state-dir": { type: "string" }
1792
+ },
1793
+ strict: true,
1794
+ allowPositionals: true
1795
+ });
1796
+ if (positionals.length > 1) {
1797
+ return null;
1798
+ }
1799
+ return {
1800
+ runId: positionals[0],
1801
+ workflowFilePath: values["workflow-file"],
1802
+ workflowExportName: values.workflow,
1803
+ fromStepRaw: values["from-step"],
1804
+ mockTools: values["no-mock-tools"] === true ? false : true,
1805
+ traceDirectory: values["trace-dir"] ?? defaultTraceDirectory2,
1806
+ runStateDirectory: values["state-dir"] ?? defaultRunStateDirectory
1807
+ };
1808
+ } catch {
1809
+ return null;
1810
+ }
1811
+ }
1812
+ function resolveReplaySourceRunId(positionalRunId) {
1813
+ if (positionalRunId !== void 0) {
1814
+ return { runId: positionalRunId };
1815
+ }
1816
+ const lastRunId = readLastRunIdFromFile(process.cwd());
1817
+ if (lastRunId !== null) {
1818
+ return { runId: lastRunId };
1819
+ }
1820
+ return {
1821
+ message: `No run id given and no ${lastRunFileRelativePath} file found. Pass a run id (asm replay <runId>) or run a workflow first so ${lastRunFileRelativePath} exists.`
1822
+ };
1823
+ }
1824
+ function resolveReplayFromStepIndex(fromStepRaw) {
1825
+ if (fromStepRaw === void 0) {
1826
+ return 0;
1827
+ }
1828
+ const parsedFromStep = FromStepSchema.safeParse(fromStepRaw);
1829
+ return parsedFromStep.success ? parsedFromStep.data : null;
1830
+ }
1831
+ function writeReplayComparisonLines(runtime, output, lines) {
1832
+ for (const line of lines) {
1833
+ if (line.tone === "success") {
1834
+ output.writeSuccessLine(line.text);
1835
+ } else if (line.tone === "failure") {
1836
+ output.writeFailureLine(line.text);
1837
+ } else {
1838
+ runtime.writeLine(line.text);
1839
+ }
1840
+ }
1841
+ }
1842
+ async function executeReplayComparison(input) {
1843
+ const loadResult = await loadReplayWorkflowFromFile({
1844
+ filePath: input.workflowFilePath,
1845
+ exportName: input.flags.workflowExportName
1846
+ });
1847
+ if (loadResult.outcome === "failed") {
1848
+ input.output.writeFailureLine(loadResult.message);
1849
+ return 1;
1850
+ }
1851
+ try {
1852
+ const replayResult = await replayRun({
1853
+ definition: loadResult.workflowDefinition,
1854
+ sourceRunId: input.sourceRunId,
1855
+ options: {
1856
+ fromStepIndex: input.fromStepIndex,
1857
+ mockTools: input.flags.mockTools,
1858
+ runStateDirectory: input.flags.runStateDirectory,
1859
+ traceDirectory: input.flags.traceDirectory
1860
+ }
1861
+ });
1862
+ const originalSource = loadReplaySource({
1863
+ runId: input.sourceRunId,
1864
+ runStateDirectory: input.flags.runStateDirectory,
1865
+ traceDirectory: input.flags.traceDirectory
1866
+ });
1867
+ const comparison = compareRunToReplay({
1868
+ original: originalSource.state,
1869
+ replay: replayResult.state
1870
+ });
1871
+ writeReplayComparisonLines(
1872
+ input.runtime,
1873
+ input.output,
1874
+ buildReplayComparisonLines({
1875
+ sourceRunId: input.sourceRunId,
1876
+ replayRunId: replayResult.runId,
1877
+ fromStepIndex: input.fromStepIndex,
1878
+ comparison
1879
+ })
1880
+ );
1881
+ return 0;
1882
+ } catch (caughtError) {
1883
+ const reason = caughtError instanceof Error ? caughtError.message : String(caughtError);
1884
+ input.output.writeFailureLine(`Replay failed: ${reason}`);
1885
+ return 1;
1886
+ }
1887
+ }
1888
+ async function runReplayCommand(args, runtime) {
1889
+ const output = createCliOutput(runtime.writeLine, resolveUseColor(runtime.env));
1890
+ const flags = parseReplayFlags(args);
1891
+ if (flags === null) {
1892
+ output.writeFailureLine(`Unrecognized replay arguments. ${replayCommandUsage}`);
1893
+ return 1;
1894
+ }
1895
+ if (flags.workflowFilePath === void 0) {
1896
+ output.writeFailureLine(
1897
+ `asm replay needs the workflow definition to re-run it. Pass --workflow-file <file> pointing at the workflow file used for the original run. ${replayCommandUsage}`
1898
+ );
1899
+ return 1;
1900
+ }
1901
+ const fromStepIndex = resolveReplayFromStepIndex(flags.fromStepRaw);
1902
+ if (fromStepIndex === null) {
1903
+ output.writeFailureLine("--from-step must be a non-negative integer.");
1904
+ return 1;
1905
+ }
1906
+ const runIdResolution = resolveReplaySourceRunId(flags.runId);
1907
+ if ("message" in runIdResolution) {
1908
+ output.writeFailureLine(runIdResolution.message);
1909
+ return 1;
1910
+ }
1911
+ return await executeReplayComparison({
1912
+ runtime,
1913
+ output,
1914
+ flags,
1915
+ workflowFilePath: flags.workflowFilePath,
1916
+ sourceRunId: runIdResolution.runId,
1917
+ fromStepIndex
1918
+ });
1919
+ }
1920
+
1921
+ // src/commands/whoami.ts
1922
+ function resolveWhoamiKey(environmentApiKey, configApiKey, homeDir) {
1923
+ if (environmentApiKey !== void 0) {
1924
+ return {
1925
+ rawKey: environmentApiKey,
1926
+ source: "the ASSEMBLE_API_KEY environment variable"
1927
+ };
1928
+ }
1929
+ if (configApiKey !== void 0) {
1930
+ return {
1931
+ rawKey: configApiKey,
1932
+ source: resolveCliConfigFilePath(homeDir)
1933
+ };
1934
+ }
1935
+ return null;
1936
+ }
1937
+ async function reportWhoamiIdentity(resolvedKey, baseUrl, runtime, output) {
1938
+ const checkResult = await checkApiKeyWithPlatform({
1939
+ baseUrl,
1940
+ rawKey: resolvedKey.rawKey,
1941
+ fetchImplementation: runtime.fetchImplementation
1942
+ });
1943
+ if (checkResult.outcome === "unauthorized") {
1944
+ output.writeFailureLine(
1945
+ `Your API key (from ${resolvedKey.source}) is invalid or revoked. Create a new key at ${cliDashboardKeysUrl}`
1946
+ );
1947
+ return 1;
1948
+ }
1949
+ if (checkResult.outcome === "request-failed") {
1950
+ output.writeFailureLine(
1951
+ `Could not reach ${baseUrl}: ${checkResult.message}. Check your network connection and try again.`
1952
+ );
1953
+ return 1;
1954
+ }
1955
+ output.writeSuccessLine(`Workspace: ${checkResult.workspaceId}`);
1956
+ output.writeInfoLine(
1957
+ `API key: ${maskRawApiKey(resolvedKey.rawKey)} (from ${resolvedKey.source})`
1958
+ );
1959
+ return 0;
1960
+ }
1961
+ async function runWhoamiCommand(runtime) {
1962
+ const output = createCliOutput(runtime.writeLine, resolveUseColor(runtime.env));
1963
+ const environment = parseCliEnvironment(runtime.env);
1964
+ const configFileResult = readCliConfigFile(runtime.homeDir);
1965
+ if (configFileResult.warning !== null) {
1966
+ output.writeInfoLine(configFileResult.warning);
1967
+ }
1968
+ const resolvedKey = resolveWhoamiKey(
1969
+ environment.ASSEMBLE_API_KEY,
1970
+ configFileResult.config.apiKey,
1971
+ runtime.homeDir
1972
+ );
1973
+ if (resolvedKey === null) {
1974
+ output.writeFailureLine(
1975
+ "You are not logged in. Run `asm login --key <api-key>` to authenticate."
1976
+ );
1977
+ return 1;
1978
+ }
1979
+ const baseUrl = environment.ASSEMBLE_BASE_URL ?? configFileResult.config.baseUrl ?? defaultCliBaseUrl;
1980
+ return await reportWhoamiIdentity(resolvedKey, baseUrl, runtime, output);
1981
+ }
1982
+
1983
+ // src/cli.ts
1984
+ var usageLines = [
1985
+ "asm \u2014 the Assemble CLI",
1986
+ "",
1987
+ "Usage:",
1988
+ " asm <command> [options]",
1989
+ "",
1990
+ "Commands:",
1991
+ " login Authenticate with an Assemble API key",
1992
+ " (--key <api-key>, --base-url <url>, or pipe the key on stdin)",
1993
+ " logout Remove saved credentials from ~/.assemble/config.json",
1994
+ " whoami Show the workspace and masked key for the active credentials",
1995
+ " run Execute a workflow file with live trace output",
1996
+ " (asm run <file> [--input <json>] [--input-file <path>]",
1997
+ " [--workflow <exportName>] [--trace-dir <dir>] [--no-upload])",
1998
+ " dev Watch a workflow file, re-run it on change, and stream traces",
1999
+ " (asm dev <file> [--input <json>] [--input-file <path>]",
2000
+ " [--workflow <exportName>] [--trace-dir <dir>] [--watch-dir <dir>]",
2001
+ " [--no-upload])",
2002
+ " init Scaffold a starter Assemble project into [dir]",
2003
+ " (asm init [dir], --force to write into a non-empty directory)",
2004
+ " eval Run an eval suite file and print the metric table",
2005
+ " (asm eval <suite-file> [--suite <exportName>] [--results-dir <dir>]",
2006
+ " [--no-upload])",
2007
+ " replay Re-run a recorded run and compare it to the original",
2008
+ " (asm replay [runId] --workflow-file <file> [--workflow <exportName>]",
2009
+ " [--from-step <n>] [--no-mock-tools] [--trace-dir <dir>] [--state-dir <dir>]",
2010
+ " --workflow-file is required; runId defaults to .assemble/last-run)",
2011
+ "",
2012
+ "Options:",
2013
+ " --help Show this usage information"
2014
+ ];
2015
+ function printCliUsage(runtime) {
2016
+ for (const usageLine of usageLines) {
2017
+ runtime.writeLine(usageLine);
2018
+ }
2019
+ }
2020
+ async function runCli(argv, runtime) {
2021
+ const commandName = argv[0];
2022
+ const commandArgs = argv.slice(1);
2023
+ if (commandName === void 0 || commandName === "--help" || commandName === "-h" || commandName === "help") {
2024
+ printCliUsage(runtime);
2025
+ return 0;
2026
+ }
2027
+ if (commandName === "login") {
2028
+ return await runLoginCommand(commandArgs, runtime);
2029
+ }
2030
+ if (commandName === "logout") {
2031
+ return runLogoutCommand(runtime);
2032
+ }
2033
+ if (commandName === "whoami") {
2034
+ return await runWhoamiCommand(runtime);
2035
+ }
2036
+ if (commandName === "run") {
2037
+ return await runRunCommand(commandArgs, runtime);
2038
+ }
2039
+ if (commandName === "dev") {
2040
+ return await runDevCommand(commandArgs, runtime);
2041
+ }
2042
+ if (commandName === "init") {
2043
+ return runInitCommand(commandArgs, runtime);
2044
+ }
2045
+ if (commandName === "eval") {
2046
+ return await runEvalCommand(commandArgs, runtime);
2047
+ }
2048
+ if (commandName === "replay") {
2049
+ return await runReplayCommand(commandArgs, runtime);
2050
+ }
2051
+ runtime.writeLine(`Unknown command: ${commandName}`);
2052
+ runtime.writeLine("");
2053
+ printCliUsage(runtime);
2054
+ return 1;
2055
+ }
2056
+
2057
+ // src/cli-runtime.ts
2058
+ import { homedir } from "os";
2059
+ import { createInterface } from "readline";
2060
+ async function readSingleLineFromStdin() {
2061
+ const readlineInterface = createInterface({ input: process.stdin });
2062
+ try {
2063
+ for await (const line of readlineInterface) {
2064
+ return line.trim();
2065
+ }
2066
+ return "";
2067
+ } finally {
2068
+ readlineInterface.close();
2069
+ }
2070
+ }
2071
+ function createDefaultCliRuntime() {
2072
+ return {
2073
+ env: process.env,
2074
+ homeDir: homedir(),
2075
+ fetchImplementation: fetch,
2076
+ writeLine: (line) => {
2077
+ process.stdout.write(`${line}
2078
+ `);
2079
+ },
2080
+ readLine: process.stdin.isTTY === true ? void 0 : readSingleLineFromStdin,
2081
+ now: () => /* @__PURE__ */ new Date(),
2082
+ sleep: async (milliseconds) => {
2083
+ await new Promise((resolveSleep) => {
2084
+ setTimeout(resolveSleep, milliseconds);
2085
+ });
2086
+ }
2087
+ };
2088
+ }
2089
+
2090
+ // src/index.ts
2091
+ function writeProcessErrorLine(line) {
2092
+ process.stderr.write(`${line}
2093
+ `);
2094
+ }
2095
+ async function runAsmCliProcess() {
2096
+ try {
2097
+ return await runCli(process.argv.slice(2), createDefaultCliRuntime());
2098
+ } catch (caughtError) {
2099
+ if (caughtError instanceof AppError) {
2100
+ writeProcessErrorLine(`\u2717 ${caughtError.message}`);
2101
+ return 1;
2102
+ }
2103
+ throw caughtError;
2104
+ }
2105
+ }
2106
+ process.exitCode = await runAsmCliProcess();