@studio-foundation/cli 0.3.0-beta.1 → 0.3.0-beta.5

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 (91) hide show
  1. package/package.json +8 -5
  2. package/ARCHITECTURE.md +0 -40
  3. package/src/commands/api.ts +0 -126
  4. package/src/commands/config.ts +0 -433
  5. package/src/commands/init.ts +0 -879
  6. package/src/commands/install.ts +0 -23
  7. package/src/commands/integrations.ts +0 -332
  8. package/src/commands/list.ts +0 -197
  9. package/src/commands/logs.ts +0 -119
  10. package/src/commands/ollama.ts +0 -168
  11. package/src/commands/project.ts +0 -167
  12. package/src/commands/registry/audit.ts +0 -87
  13. package/src/commands/registry/index.ts +0 -63
  14. package/src/commands/registry/install.ts +0 -222
  15. package/src/commands/registry/publish.ts +0 -112
  16. package/src/commands/registry/remove.ts +0 -89
  17. package/src/commands/registry/search.ts +0 -93
  18. package/src/commands/registry/sync.ts +0 -24
  19. package/src/commands/registry/update.ts +0 -63
  20. package/src/commands/replay.ts +0 -559
  21. package/src/commands/run.ts +0 -454
  22. package/src/commands/status.ts +0 -163
  23. package/src/commands/template/index.ts +0 -59
  24. package/src/commands/template/validate.ts +0 -175
  25. package/src/commands/templates.ts +0 -99
  26. package/src/commands/tools.ts +0 -227
  27. package/src/commands/users.ts +0 -127
  28. package/src/commands/validate.ts +0 -46
  29. package/src/config.ts +0 -101
  30. package/src/index.ts +0 -201
  31. package/src/models-cache.ts +0 -127
  32. package/src/output/file-changes.ts +0 -97
  33. package/src/output/formatter.ts +0 -85
  34. package/src/output/formatters.ts +0 -303
  35. package/src/output/logger.ts +0 -18
  36. package/src/output/parallel-progress.ts +0 -103
  37. package/src/output/progress.ts +0 -414
  38. package/src/provider-validator.ts +0 -109
  39. package/src/registry/cache.ts +0 -46
  40. package/src/registry/client.ts +0 -84
  41. package/src/registry/lockfile.ts +0 -68
  42. package/src/registry/resolver.ts +0 -113
  43. package/src/registry/types.ts +0 -66
  44. package/src/run-logger.ts +0 -66
  45. package/src/run-store-factory.ts +0 -30
  46. package/src/studio-dir.ts +0 -28
  47. package/src/utils/input-wizard.ts +0 -73
  48. package/src/utils/placeholders.ts +0 -10
  49. package/templates/.studiorc.yaml +0 -16
  50. package/templates/projects/blank/metadata.json +0 -7
  51. package/templates/studio-config.yaml +0 -14
  52. package/tests/__stubs__/studio-runner.ts +0 -104
  53. package/tests/commands/api.test.ts +0 -110
  54. package/tests/commands/config.test.ts +0 -221
  55. package/tests/commands/init.test.ts +0 -919
  56. package/tests/commands/install.test.ts +0 -52
  57. package/tests/commands/integrations.test.ts +0 -158
  58. package/tests/commands/ollama.test.ts +0 -139
  59. package/tests/commands/project.test.ts +0 -179
  60. package/tests/commands/registry/audit.test.ts +0 -56
  61. package/tests/commands/registry/install.test.ts +0 -200
  62. package/tests/commands/registry/publish.test.ts +0 -56
  63. package/tests/commands/registry/remove.test.ts +0 -103
  64. package/tests/commands/registry/search.test.ts +0 -44
  65. package/tests/commands/registry/sync.test.ts +0 -37
  66. package/tests/commands/registry/update.test.ts +0 -62
  67. package/tests/commands/replay.test.ts +0 -283
  68. package/tests/commands/template/validate.test.ts +0 -150
  69. package/tests/commands/templates.test.ts +0 -42
  70. package/tests/commands/tools.test.ts +0 -106
  71. package/tests/config.test.ts +0 -142
  72. package/tests/formatter.test.ts +0 -158
  73. package/tests/integration/sigint.test.ts +0 -188
  74. package/tests/models-cache.test.ts +0 -250
  75. package/tests/output/file-changes.test.ts +0 -178
  76. package/tests/output/formatters.test.ts +0 -448
  77. package/tests/output/progress-spinner.test.ts +0 -232
  78. package/tests/output/progress-timer.test.ts +0 -230
  79. package/tests/provider-validator.test.ts +0 -182
  80. package/tests/registry/cache.test.ts +0 -66
  81. package/tests/registry/client.test.ts +0 -70
  82. package/tests/registry/lockfile.test.ts +0 -87
  83. package/tests/registry/resolver.test.ts +0 -122
  84. package/tests/run-logger-events.test.ts +0 -326
  85. package/tests/run-logger.test.ts +0 -64
  86. package/tests/run-store-factory.test.ts +0 -51
  87. package/tests/studio-dir.test.ts +0 -31
  88. package/tests/utils/input-wizard.test.ts +0 -153
  89. package/tests/utils/placeholders.test.ts +0 -36
  90. package/tsconfig.json +0 -24
  91. package/vitest.config.ts +0 -20
@@ -1,559 +0,0 @@
1
- import { readFileSync, readdirSync } from 'node:fs';
2
- import { resolve, join } from 'node:path';
3
- import chalk from 'chalk';
4
- import { ProgressDisplay } from '../output/progress.js';
5
- import type { PipelineDefinition, ToolCall } from '@studio-foundation/contracts';
6
- import { isStageGroup } from '@studio-foundation/contracts';
7
-
8
- // ── JSONL file discovery ─────────────────────────────────────────────────────
9
-
10
- function normalizeRunId(runId: string): string {
11
- return runId.replace(/-/g, '');
12
- }
13
-
14
- /**
15
- * Extracts the 8-char run-id suffix from a JSONL filename.
16
- * Filename format: `<date>-<pipeline>-<shortRunId>.jsonl`
17
- * The run-id is always the last segment before `.jsonl`.
18
- */
19
- function extractRunIdFromFilename(filename: string): string {
20
- const base = filename.replace(/\.jsonl$/, '');
21
- const lastDash = base.lastIndexOf('-');
22
- return lastDash >= 0 ? base.slice(lastDash + 1) : base;
23
- }
24
-
25
- export function findJsonlFile(runsDir: string, runId: string): string {
26
- let entries: string[];
27
- try {
28
- entries = readdirSync(runsDir).filter((f) => f.endsWith('.jsonl'));
29
- } catch {
30
- throw new Error(`No runs directory found at ${runsDir}`);
31
- }
32
-
33
- const needle = normalizeRunId(runId);
34
-
35
- const matching = entries.filter((name) => {
36
- const fileRunId = extractRunIdFromFilename(name);
37
- return fileRunId.startsWith(needle);
38
- });
39
-
40
- if (matching.length === 0) {
41
- throw new Error(
42
- `No run log found for run id "${runId}". Use \`studio logs\` to see available runs.`
43
- );
44
- }
45
-
46
- if (matching.length > 1) {
47
- const ids = matching.map((f) => ` - ${extractRunIdFromFilename(f)} (${f})`).join('\n');
48
- throw new Error(`Multiple runs match "${runId}":\n${ids}\nProvide more characters to disambiguate.`);
49
- }
50
-
51
- return resolve(runsDir, matching[0]);
52
- }
53
-
54
- // ── JSONL → EngineEvents mapping ─────────────────────────────────────────────
55
-
56
- export interface MappedEvent {
57
- handler: string;
58
- payload: Record<string, unknown>;
59
- }
60
-
61
- export function mapJsonlLineToEvent(
62
- line: Record<string, unknown>
63
- ): MappedEvent | null {
64
- const event = line.event as string;
65
-
66
- switch (event) {
67
- case 'pipeline_start':
68
- return {
69
- handler: 'onPipelineStart',
70
- payload: {
71
- pipeline_name: line.pipeline as string,
72
- run_id: line.run_id as string,
73
- },
74
- };
75
-
76
- case 'pipeline_complete':
77
- return {
78
- handler: 'onPipelineComplete',
79
- payload: {
80
- pipeline_name: line.pipeline_name as string,
81
- run_id: line.run_id as string,
82
- status: line.status as string,
83
- duration_ms: line.duration_ms as number,
84
- total_tokens: line.total_tokens as number,
85
- total_tool_calls: line.total_tool_calls as number,
86
- },
87
- };
88
-
89
- case 'stage_start':
90
- return {
91
- handler: 'onStageStart',
92
- payload: {
93
- stage_name: line.stage as string,
94
- stage_index: line.stage_index as number,
95
- total_stages: line.total_stages as number,
96
- },
97
- };
98
-
99
- case 'stage_complete': {
100
- const tokens = line.tokens as
101
- | { prompt: number; completion: number; total: number }
102
- | undefined;
103
- return {
104
- handler: 'onStageComplete',
105
- payload: {
106
- stage_name: line.stage as string,
107
- stage_index: line.stage_index as number,
108
- total_stages: line.total_stages as number,
109
- status: line.status as string,
110
- attempts: line.attempts as number,
111
- duration_ms: line.duration_ms as number,
112
- ...(tokens
113
- ? {
114
- token_usage: {
115
- prompt_tokens: tokens.prompt,
116
- completion_tokens: tokens.completion,
117
- total_tokens: tokens.total,
118
- },
119
- }
120
- : {}),
121
- ...(line.tool_calls ? { tool_calls: line.tool_calls } : {}),
122
- ...(line.output !== undefined ? { output: line.output } : {}),
123
- ...(line.rejection_reason ? { rejection_reason: line.rejection_reason } : {}),
124
- ...(line.rejection_details ? { rejection_details: line.rejection_details } : {}),
125
- },
126
- };
127
- }
128
-
129
- case 'stage_retry':
130
- return {
131
- handler: 'onTaskRetry',
132
- payload: {
133
- stage: line.stage as string,
134
- attempt: line.attempt as number,
135
- max_attempts: line.max_attempts as number,
136
- failures: line.failures as string[],
137
- ...(line.agent_output_raw ? { agent_output_raw: line.agent_output_raw } : {}),
138
- ...(line.tool_calls_count !== undefined
139
- ? { tool_calls_count: line.tool_calls_count }
140
- : {}),
141
- },
142
- };
143
-
144
- case 'group_start':
145
- return {
146
- handler: 'onGroupStart',
147
- payload: {
148
- group_name: line.group as string,
149
- max_iterations: line.max_iterations as number,
150
- },
151
- };
152
-
153
- case 'group_iteration':
154
- return {
155
- handler: 'onGroupIteration',
156
- payload: {
157
- group_name: line.group as string,
158
- iteration: line.iteration as number,
159
- max_iterations: line.max_iterations as number,
160
- },
161
- };
162
-
163
- case 'group_feedback':
164
- return {
165
- handler: 'onGroupFeedback',
166
- payload: {
167
- group_name: line.group as string,
168
- iteration: line.iteration as number,
169
- rejection_reason: line.rejection_reason as string,
170
- rejection_details: line.rejection_details as string[],
171
- },
172
- };
173
-
174
- case 'group_complete':
175
- return {
176
- handler: 'onGroupComplete',
177
- payload: {
178
- group_name: line.group as string,
179
- iterations: line.iterations as number,
180
- status: line.status as string,
181
- },
182
- };
183
-
184
- case 'tool_call_start':
185
- return {
186
- handler: 'onToolCallStart',
187
- payload: {
188
- tool: line.tool as string,
189
- params: (line.params as Record<string, unknown>) ?? {},
190
- timestamp: 0,
191
- },
192
- };
193
-
194
- case 'tool_call_complete':
195
- return {
196
- handler: 'onToolCallComplete',
197
- payload: {
198
- tool: line.tool as string,
199
- result: line.result,
200
- ...(line.error ? { error: line.error } : {}),
201
- duration_ms: (line.duration_ms as number) ?? 0,
202
- timestamp: 0,
203
- },
204
- };
205
-
206
- default:
207
- return null;
208
- }
209
- }
210
-
211
- // ── Resume helpers ───────────────────────────────────────────────────────────
212
-
213
- export interface ResumeContext {
214
- pipelineInput: string | Record<string, unknown>;
215
- stageOutputs: Map<string, unknown>;
216
- stageToolResults: Map<string, ToolCall[]>;
217
- pipelineName?: string;
218
- }
219
-
220
- /**
221
- * Parse a JSONL log string and extract data needed to resume from a stage.
222
- * Returns input from pipeline_start, and outputs+tool_calls from stage_complete events.
223
- */
224
- export function parseJsonlForResume(jsonlContent: string): ResumeContext {
225
- const stageOutputs = new Map<string, unknown>();
226
- const stageToolResults = new Map<string, ToolCall[]>();
227
- let pipelineInput: string | Record<string, unknown> = {};
228
- let pipelineName: string | undefined;
229
-
230
- const lines = jsonlContent.split('\n').filter((l) => l.trim().length > 0);
231
-
232
- for (const raw of lines) {
233
- let record: Record<string, unknown>;
234
- try {
235
- record = JSON.parse(raw) as Record<string, unknown>;
236
- } catch {
237
- continue; // skip corrupt lines
238
- }
239
-
240
- const event = record.event as string;
241
-
242
- if (event === 'pipeline_start') {
243
- if (record.input !== undefined) {
244
- pipelineInput = record.input as string | Record<string, unknown>;
245
- }
246
- if (record.pipeline !== undefined) {
247
- pipelineName = record.pipeline as string;
248
- }
249
- }
250
-
251
- if (event === 'stage_complete') {
252
- const stageName = record.stage as string;
253
- if (!stageName) continue;
254
-
255
- if (record.output !== undefined) {
256
- stageOutputs.set(stageName, record.output);
257
- }
258
-
259
- if (Array.isArray(record.tool_calls) && record.tool_calls.length > 0) {
260
- stageToolResults.set(stageName, record.tool_calls as ToolCall[]);
261
- }
262
- }
263
- }
264
-
265
- return { pipelineInput, stageOutputs, stageToolResults, pipelineName };
266
- }
267
-
268
- /**
269
- * Resolve a --stage argument (integer index or stage name) to a stage name.
270
- * Groups are transparent — index counts leaf stages only.
271
- * Throws with a clear message if not found or out of bounds.
272
- */
273
- export function resolveStageFromPipeline(
274
- stageArg: string,
275
- pipeline: PipelineDefinition
276
- ): string {
277
- // Collect leaf stage names in order (groups are transparent)
278
- const leafNames: string[] = [];
279
- for (const entry of pipeline.stages) {
280
- if (isStageGroup(entry)) {
281
- for (const s of entry.stages) leafNames.push(s.name);
282
- } else {
283
- leafNames.push(entry.name);
284
- }
285
- }
286
-
287
- // Try numeric index first (e.g. "0", "3")
288
- if (/^\d+$/.test(stageArg)) {
289
- const asNumber = parseInt(stageArg, 10);
290
- if (asNumber < 0 || asNumber >= leafNames.length) {
291
- throw new Error(
292
- `Stage index ${asNumber} is out of bounds. Pipeline has ${leafNames.length} stages (0–${leafNames.length - 1}).`
293
- );
294
- }
295
- return leafNames[asNumber]!;
296
- }
297
-
298
- // Try name match
299
- if (leafNames.includes(stageArg)) {
300
- return stageArg;
301
- }
302
-
303
- throw new Error(
304
- `Stage "${stageArg}" not found in pipeline. Available stages: ${leafNames.join(', ')}`
305
- );
306
- }
307
-
308
- // ── Restart command ──────────────────────────────────────────────────────────
309
-
310
- interface RestartOptions {
311
- stage: string;
312
- verbose?: boolean;
313
- provider?: string;
314
- }
315
-
316
- export async function restartCommand(
317
- runId: string,
318
- options: RestartOptions
319
- ): Promise<void> {
320
- try {
321
- const runsDir = resolve(process.cwd(), '.studio/runs');
322
- const filePath = findJsonlFile(runsDir, runId);
323
- const content = readFileSync(filePath, 'utf-8');
324
-
325
- const { pipelineInput, stageOutputs, stageToolResults, pipelineName } = parseJsonlForResume(content);
326
-
327
- if (!pipelineName) {
328
- throw new Error(`Could not determine pipeline name from run log for run ${runId}`);
329
- }
330
-
331
- // Load config + dependencies (mirror run.ts pattern exactly)
332
- const { loadConfig } = await import('../config.js');
333
- const { PipelineEngine, loadPipelineByName, DirectEngineSpawner, resolveRepoPath } = await import('@studio-foundation/engine');
334
- const { createDefaultRegistry, ToolRegistry, loadProjectTools, loadPlugins, MCPClient } = await import('@studio-foundation/runner');
335
- const { createRunStore } = await import('../run-store-factory.js');
336
- const { createRunLogger } = await import('../run-logger.js');
337
- const { mergeEvents } = await import('./run.js');
338
- const yaml = await import('js-yaml');
339
-
340
- const config = await loadConfig();
341
-
342
- let runStore = null;
343
- try {
344
- runStore = await createRunStore(config);
345
- } catch (err) {
346
- console.warn(chalk.yellow(`⚠ Run store unavailable: ${err instanceof Error ? err.message : String(err)}. Continuing with JSONL logging only.`));
347
- }
348
-
349
- const configsDir = config.paths?.configs
350
- ? resolve(config.paths.configs)
351
- : config.resolvedStudioDir
352
- ? resolve(config.resolvedStudioDir)
353
- : resolve('./configs');
354
- const pipelinesDir = join(configsDir, 'pipelines');
355
-
356
- // Load pipeline to resolve --stage
357
- const pipelineDef = await loadPipelineByName(pipelineName, pipelinesDir);
358
- const resolvedStage = resolveStageFromPipeline(options.stage, pipelineDef);
359
-
360
- // Resolve repo path
361
- let repoPath: string;
362
- try {
363
- repoPath = await resolveRepoPath({
364
- repoPathOverride: undefined,
365
- repoUrl: pipelineDef.repo?.url,
366
- rawProjectsDir: config.paths?.projects_dir || process.env.STUDIO_PROJECTS_DIR,
367
- pipelineName,
368
- branch: pipelineDef.repo?.branch,
369
- });
370
- } catch (err) {
371
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
372
- process.exit(1);
373
- }
374
-
375
- const providerRegistry = createDefaultRegistry(
376
- options.provider === 'mock' ? {} : {
377
- openai: config.providers?.openai ? { apiKey: config.providers.openai.apiKey } : undefined,
378
- anthropic: config.providers?.anthropic ? { apiKey: config.providers.anthropic.apiKey } : undefined,
379
- openaiResponses: config.providers?.openai ? { apiKey: config.providers.openai.apiKey } : undefined,
380
- ollama: config.providers?.ollama ? { baseUrl: config.providers.ollama.baseUrl } : undefined,
381
- }
382
- );
383
-
384
- if (options.provider === 'mock') {
385
- const mockYamlPath = join(configsDir, 'mock.yaml');
386
- let mockRaw: string;
387
- try {
388
- mockRaw = readFileSync(mockYamlPath, 'utf-8');
389
- } catch {
390
- console.error(`Error: --provider mock requires ${mockYamlPath}`);
391
- process.exit(1);
392
- }
393
- const mockConfig = yaml.default.load(mockRaw) as {
394
- stages: Record<string, { output: Record<string, unknown>; tool_calls: Array<{ name: string; arguments: Record<string, unknown> }> }>;
395
- };
396
- const stagesMap = new Map(Object.entries(mockConfig.stages));
397
- const { MockProvider } = await import('@studio-foundation/runner');
398
- const mockProvider = new MockProvider(stagesMap);
399
- providerRegistry.register(mockProvider);
400
- } else if (options.provider && !providerRegistry.has(options.provider)) {
401
- console.error(`Error: Unknown provider "${options.provider}". Available: ${providerRegistry.list().join(', ')}`);
402
- process.exit(1);
403
- }
404
-
405
- const toolsDir = resolve(configsDir, 'tools');
406
- const loadedPlugins = await loadProjectTools(toolsDir, repoPath);
407
- const toolRegistry = new ToolRegistry();
408
- for (const plugin of loadedPlugins) {
409
- toolRegistry.registerPlugin(plugin.name, plugin.tools, plugin.promptSnippet);
410
- }
411
-
412
- const pluginsDir = resolve(configsDir, 'plugins');
413
- const pluginManifests = await loadPlugins(pluginsDir);
414
- const mcpClients: InstanceType<typeof MCPClient>[] = [];
415
- for (const manifest of pluginManifests) {
416
- for (const [serverName, serverDef] of Object.entries(manifest.mcpServers)) {
417
- const client = new MCPClient(manifest.name, serverName, serverDef);
418
- try {
419
- await client.start();
420
- const mcpTools = await client.getTools();
421
- toolRegistry.registerPlugin(`${manifest.name}-${serverName}`, mcpTools);
422
- mcpClients.push(client);
423
- } catch (err) {
424
- console.warn(chalk.yellow(`⚠ Plugin '${manifest.name}': failed to start MCP server '${serverName}': ${(err as Error).message}`));
425
- }
426
- }
427
- }
428
-
429
- const pluginSkills: Record<string, string[]> = {};
430
- for (const manifest of pluginManifests) {
431
- if (manifest.skills.length > 0) {
432
- pluginSkills[manifest.name] = manifest.skills.map(
433
- (s: { name: string; content: string }) => `## Skill: ${s.name}\n\n${s.content}`
434
- );
435
- }
436
- }
437
-
438
- console.log(chalk.cyan(`Resuming ${pipelineName} from stage ${chalk.bold(resolvedStage)}`));
439
- console.log(chalk.gray(`Original run: ${runId}`));
440
- console.log('');
441
-
442
- const progress = new ProgressDisplay(false, { live: true, verbose: !!options.verbose });
443
- const runLogger = createRunLogger(process.cwd());
444
- const events = mergeEvents(progress.getEvents(), runLogger, pipelineName, pipelineInput);
445
-
446
- const engineConfig = {
447
- configsDir,
448
- repoPath,
449
- providerRegistry,
450
- toolRegistry,
451
- pluginSkills,
452
- db: runStore ?? undefined,
453
- defaultProvider: config.defaults?.provider,
454
- defaultModel: config.defaults?.model,
455
- ...(options.provider ? { providerOverride: options.provider } : {}),
456
- };
457
-
458
- const spawner = new DirectEngineSpawner(engineConfig);
459
- const engine = new PipelineEngine({ ...engineConfig, spawner, maxDepth: 3 }, events);
460
-
461
- const controller = new AbortController();
462
- let forceExitOnNextInterrupt = false;
463
-
464
- const onInterrupt = () => {
465
- if (forceExitOnNextInterrupt) {
466
- process.exit(130);
467
- }
468
- forceExitOnNextInterrupt = true;
469
- controller.abort();
470
- progress.interrupt();
471
- process.stderr.write('\n' + chalk.yellow('⚠ Cancelling run...') + '\n');
472
- };
473
- process.on('SIGINT', onInterrupt);
474
- process.on('SIGTERM', onInterrupt);
475
-
476
- let result;
477
- try {
478
- result = await engine.run({
479
- pipeline: pipelineName,
480
- input: pipelineInput,
481
- resumeFromStage: resolvedStage,
482
- priorStageOutputs: stageOutputs,
483
- priorStageToolResults: stageToolResults,
484
- originalRunId: runId,
485
- signal: controller.signal,
486
- });
487
- } finally {
488
- process.off('SIGINT', onInterrupt);
489
- process.off('SIGTERM', onInterrupt);
490
- await runLogger.close();
491
- if (runStore && result) {
492
- await (runStore as { saveLogPath?: (id: string, path: string) => Promise<void> }).saveLogPath?.(result.id, runLogger.getLogPath());
493
- }
494
- await (runStore as { close?: () => Promise<void> } | null)?.close?.();
495
- await Promise.allSettled(mcpClients.map((c) => c.close()));
496
- }
497
-
498
- console.log('');
499
- console.log(chalk.gray(`Run ID: ${progress.runId}`));
500
- if (result) {
501
- process.exit(result.status === 'success' ? 0 : 1);
502
- }
503
- } catch (error) {
504
- console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
505
- process.exit(1);
506
- }
507
- }
508
-
509
- // ── Replay command ───────────────────────────────────────────────────────────
510
-
511
- interface ReplayOptions {
512
- verbose?: boolean;
513
- }
514
-
515
- export async function replayCommand(
516
- runId: string,
517
- options: ReplayOptions
518
- ): Promise<void> {
519
- try {
520
- const runsDir = resolve(process.cwd(), '.studio/runs');
521
- const filePath = findJsonlFile(runsDir, runId);
522
-
523
- const content = readFileSync(filePath, 'utf-8');
524
- const lines = content
525
- .split('\n')
526
- .filter((l) => l.trim().length > 0);
527
-
528
- const progress = new ProgressDisplay(false, {
529
- live: true,
530
- verbose: !!options.verbose,
531
- });
532
- const events = progress.getEvents();
533
-
534
- for (const raw of lines) {
535
- let parsed: Record<string, unknown>;
536
- try {
537
- parsed = JSON.parse(raw) as Record<string, unknown>;
538
- } catch {
539
- // Skip corrupt lines
540
- continue;
541
- }
542
-
543
- const mapped = mapJsonlLineToEvent(parsed);
544
- if (!mapped) continue;
545
-
546
- const handler = events[mapped.handler as keyof typeof events];
547
- if (handler) {
548
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
549
- (handler as (e: any) => void)(mapped.payload);
550
- }
551
- }
552
- } catch (error) {
553
- console.error(
554
- chalk.red('Error:'),
555
- error instanceof Error ? error.message : error
556
- );
557
- process.exit(1);
558
- }
559
- }