@studio-foundation/cli 0.3.0-beta.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.
- package/ARCHITECTURE.md +40 -0
- package/LICENSE +663 -0
- package/README.md +165 -0
- package/dist/commands/api.d.ts +16 -0
- package/dist/commands/api.d.ts.map +1 -0
- package/dist/commands/api.js +118 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/config.d.ts +32 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +390 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/init.d.ts +93 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +728 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +22 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/integrations.d.ts +14 -0
- package/dist/commands/integrations.d.ts.map +1 -0
- package/dist/commands/integrations.js +282 -0
- package/dist/commands/integrations.js.map +1 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +161 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/logs.d.ts +7 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +98 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/ollama.d.ts +6 -0
- package/dist/commands/ollama.d.ts.map +1 -0
- package/dist/commands/ollama.js +161 -0
- package/dist/commands/ollama.js.map +1 -0
- package/dist/commands/project.d.ts +38 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +137 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/registry/audit.d.ts +13 -0
- package/dist/commands/registry/audit.d.ts.map +1 -0
- package/dist/commands/registry/audit.js +65 -0
- package/dist/commands/registry/audit.js.map +1 -0
- package/dist/commands/registry/index.d.ts +3 -0
- package/dist/commands/registry/index.d.ts.map +1 -0
- package/dist/commands/registry/index.js +53 -0
- package/dist/commands/registry/index.js.map +1 -0
- package/dist/commands/registry/install.d.ts +17 -0
- package/dist/commands/registry/install.d.ts.map +1 -0
- package/dist/commands/registry/install.js +153 -0
- package/dist/commands/registry/install.js.map +1 -0
- package/dist/commands/registry/publish.d.ts +8 -0
- package/dist/commands/registry/publish.d.ts.map +1 -0
- package/dist/commands/registry/publish.js +94 -0
- package/dist/commands/registry/publish.js.map +1 -0
- package/dist/commands/registry/remove.d.ts +8 -0
- package/dist/commands/registry/remove.d.ts.map +1 -0
- package/dist/commands/registry/remove.js +82 -0
- package/dist/commands/registry/remove.js.map +1 -0
- package/dist/commands/registry/search.d.ts +9 -0
- package/dist/commands/registry/search.d.ts.map +1 -0
- package/dist/commands/registry/search.js +68 -0
- package/dist/commands/registry/search.js.map +1 -0
- package/dist/commands/registry/sync.d.ts +8 -0
- package/dist/commands/registry/sync.d.ts.map +1 -0
- package/dist/commands/registry/sync.js +19 -0
- package/dist/commands/registry/sync.js.map +1 -0
- package/dist/commands/registry/update.d.ts +15 -0
- package/dist/commands/registry/update.d.ts.map +1 -0
- package/dist/commands/registry/update.js +48 -0
- package/dist/commands/registry/update.js.map +1 -0
- package/dist/commands/replay.d.ts +36 -0
- package/dist/commands/replay.d.ts.map +1 -0
- package/dist/commands/replay.js +453 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/run.d.ts +18 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +406 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +144 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/template/index.d.ts +2 -0
- package/dist/commands/template/index.d.ts.map +1 -0
- package/dist/commands/template/index.js +57 -0
- package/dist/commands/template/index.js.map +1 -0
- package/dist/commands/template/validate.d.ts +8 -0
- package/dist/commands/template/validate.d.ts.map +1 -0
- package/dist/commands/template/validate.js +135 -0
- package/dist/commands/template/validate.js.map +1 -0
- package/dist/commands/templates.d.ts +14 -0
- package/dist/commands/templates.d.ts.map +1 -0
- package/dist/commands/templates.js +86 -0
- package/dist/commands/templates.js.map +1 -0
- package/dist/commands/tools.d.ts +12 -0
- package/dist/commands/tools.d.ts.map +1 -0
- package/dist/commands/tools.js +200 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commands/users.d.ts +4 -0
- package/dist/commands/users.d.ts.map +1 -0
- package/dist/commands/users.js +123 -0
- package/dist/commands/users.js.map +1 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +39 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +178 -0
- package/dist/index.js.map +1 -0
- package/dist/models-cache.d.ts +13 -0
- package/dist/models-cache.d.ts.map +1 -0
- package/dist/models-cache.js +106 -0
- package/dist/models-cache.js.map +1 -0
- package/dist/output/file-changes.d.ts +15 -0
- package/dist/output/file-changes.d.ts.map +1 -0
- package/dist/output/file-changes.js +77 -0
- package/dist/output/file-changes.js.map +1 -0
- package/dist/output/formatter.d.ts +6 -0
- package/dist/output/formatter.d.ts.map +1 -0
- package/dist/output/formatter.js +70 -0
- package/dist/output/formatter.js.map +1 -0
- package/dist/output/formatters.d.ts +42 -0
- package/dist/output/formatters.d.ts.map +1 -0
- package/dist/output/formatters.js +280 -0
- package/dist/output/formatters.js.map +1 -0
- package/dist/output/logger.d.ts +3 -0
- package/dist/output/logger.d.ts.map +1 -0
- package/dist/output/logger.js +15 -0
- package/dist/output/logger.js.map +1 -0
- package/dist/output/parallel-progress.d.ts +33 -0
- package/dist/output/parallel-progress.d.ts.map +1 -0
- package/dist/output/parallel-progress.js +85 -0
- package/dist/output/parallel-progress.js.map +1 -0
- package/dist/output/progress.d.ts +32 -0
- package/dist/output/progress.d.ts.map +1 -0
- package/dist/output/progress.js +393 -0
- package/dist/output/progress.js.map +1 -0
- package/dist/provider-validator.d.ts +15 -0
- package/dist/provider-validator.d.ts.map +1 -0
- package/dist/provider-validator.js +85 -0
- package/dist/provider-validator.js.map +1 -0
- package/dist/registry/cache.d.ts +10 -0
- package/dist/registry/cache.d.ts.map +1 -0
- package/dist/registry/cache.js +36 -0
- package/dist/registry/cache.js.map +1 -0
- package/dist/registry/client.d.ts +19 -0
- package/dist/registry/client.d.ts.map +1 -0
- package/dist/registry/client.js +63 -0
- package/dist/registry/client.js.map +1 -0
- package/dist/registry/lockfile.d.ts +17 -0
- package/dist/registry/lockfile.d.ts.map +1 -0
- package/dist/registry/lockfile.js +61 -0
- package/dist/registry/lockfile.js.map +1 -0
- package/dist/registry/resolver.d.ts +13 -0
- package/dist/registry/resolver.d.ts.map +1 -0
- package/dist/registry/resolver.js +79 -0
- package/dist/registry/resolver.js.map +1 -0
- package/dist/registry/types.d.ts +59 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +14 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/run-logger.d.ts +8 -0
- package/dist/run-logger.d.ts.map +1 -0
- package/dist/run-logger.js +53 -0
- package/dist/run-logger.js.map +1 -0
- package/dist/run-store-factory.d.ts +9 -0
- package/dist/run-store-factory.d.ts.map +1 -0
- package/dist/run-store-factory.js +26 -0
- package/dist/run-store-factory.js.map +1 -0
- package/dist/studio-dir.d.ts +7 -0
- package/dist/studio-dir.d.ts.map +1 -0
- package/dist/studio-dir.js +27 -0
- package/dist/studio-dir.js.map +1 -0
- package/dist/utils/input-wizard.d.ts +4 -0
- package/dist/utils/input-wizard.d.ts.map +1 -0
- package/dist/utils/input-wizard.js +56 -0
- package/dist/utils/input-wizard.js.map +1 -0
- package/dist/utils/placeholders.d.ts +6 -0
- package/dist/utils/placeholders.d.ts.map +1 -0
- package/dist/utils/placeholders.js +12 -0
- package/dist/utils/placeholders.js.map +1 -0
- package/package.json +43 -0
- package/src/commands/api.ts +126 -0
- package/src/commands/config.ts +433 -0
- package/src/commands/init.ts +879 -0
- package/src/commands/install.ts +23 -0
- package/src/commands/integrations.ts +332 -0
- package/src/commands/list.ts +197 -0
- package/src/commands/logs.ts +119 -0
- package/src/commands/ollama.ts +168 -0
- package/src/commands/project.ts +167 -0
- package/src/commands/registry/audit.ts +87 -0
- package/src/commands/registry/index.ts +63 -0
- package/src/commands/registry/install.ts +222 -0
- package/src/commands/registry/publish.ts +112 -0
- package/src/commands/registry/remove.ts +89 -0
- package/src/commands/registry/search.ts +93 -0
- package/src/commands/registry/sync.ts +24 -0
- package/src/commands/registry/update.ts +63 -0
- package/src/commands/replay.ts +559 -0
- package/src/commands/run.ts +454 -0
- package/src/commands/status.ts +163 -0
- package/src/commands/template/index.ts +59 -0
- package/src/commands/template/validate.ts +175 -0
- package/src/commands/templates.ts +99 -0
- package/src/commands/tools.ts +227 -0
- package/src/commands/users.ts +127 -0
- package/src/commands/validate.ts +46 -0
- package/src/config.ts +101 -0
- package/src/index.ts +201 -0
- package/src/models-cache.ts +127 -0
- package/src/output/file-changes.ts +97 -0
- package/src/output/formatter.ts +85 -0
- package/src/output/formatters.ts +303 -0
- package/src/output/logger.ts +18 -0
- package/src/output/parallel-progress.ts +103 -0
- package/src/output/progress.ts +414 -0
- package/src/provider-validator.ts +109 -0
- package/src/registry/cache.ts +46 -0
- package/src/registry/client.ts +84 -0
- package/src/registry/lockfile.ts +68 -0
- package/src/registry/resolver.ts +113 -0
- package/src/registry/types.ts +66 -0
- package/src/run-logger.ts +66 -0
- package/src/run-store-factory.ts +30 -0
- package/src/studio-dir.ts +28 -0
- package/src/utils/input-wizard.ts +73 -0
- package/src/utils/placeholders.ts +10 -0
- package/templates/.studiorc.yaml +16 -0
- package/templates/projects/blank/metadata.json +7 -0
- package/templates/studio-config.yaml +14 -0
- package/tests/__stubs__/studio-runner.ts +104 -0
- package/tests/commands/api.test.ts +110 -0
- package/tests/commands/config.test.ts +221 -0
- package/tests/commands/init.test.ts +919 -0
- package/tests/commands/install.test.ts +52 -0
- package/tests/commands/integrations.test.ts +158 -0
- package/tests/commands/ollama.test.ts +139 -0
- package/tests/commands/project.test.ts +179 -0
- package/tests/commands/registry/audit.test.ts +56 -0
- package/tests/commands/registry/install.test.ts +200 -0
- package/tests/commands/registry/publish.test.ts +56 -0
- package/tests/commands/registry/remove.test.ts +103 -0
- package/tests/commands/registry/search.test.ts +44 -0
- package/tests/commands/registry/sync.test.ts +37 -0
- package/tests/commands/registry/update.test.ts +62 -0
- package/tests/commands/replay.test.ts +283 -0
- package/tests/commands/template/validate.test.ts +150 -0
- package/tests/commands/templates.test.ts +42 -0
- package/tests/commands/tools.test.ts +106 -0
- package/tests/config.test.ts +142 -0
- package/tests/formatter.test.ts +158 -0
- package/tests/integration/sigint.test.ts +188 -0
- package/tests/models-cache.test.ts +250 -0
- package/tests/output/file-changes.test.ts +178 -0
- package/tests/output/formatters.test.ts +448 -0
- package/tests/output/progress-spinner.test.ts +232 -0
- package/tests/output/progress-timer.test.ts +230 -0
- package/tests/provider-validator.test.ts +182 -0
- package/tests/registry/cache.test.ts +66 -0
- package/tests/registry/client.test.ts +70 -0
- package/tests/registry/lockfile.test.ts +87 -0
- package/tests/registry/resolver.test.ts +122 -0
- package/tests/run-logger-events.test.ts +326 -0
- package/tests/run-logger.test.ts +64 -0
- package/tests/run-store-factory.test.ts +51 -0
- package/tests/studio-dir.test.ts +31 -0
- package/tests/utils/input-wizard.test.ts +153 -0
- package/tests/utils/placeholders.test.ts +36 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,559 @@
|
|
|
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
|
+
}
|