@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.
- package/package.json +8 -5
- package/ARCHITECTURE.md +0 -40
- package/src/commands/api.ts +0 -126
- package/src/commands/config.ts +0 -433
- package/src/commands/init.ts +0 -879
- package/src/commands/install.ts +0 -23
- package/src/commands/integrations.ts +0 -332
- package/src/commands/list.ts +0 -197
- package/src/commands/logs.ts +0 -119
- package/src/commands/ollama.ts +0 -168
- package/src/commands/project.ts +0 -167
- package/src/commands/registry/audit.ts +0 -87
- package/src/commands/registry/index.ts +0 -63
- package/src/commands/registry/install.ts +0 -222
- package/src/commands/registry/publish.ts +0 -112
- package/src/commands/registry/remove.ts +0 -89
- package/src/commands/registry/search.ts +0 -93
- package/src/commands/registry/sync.ts +0 -24
- package/src/commands/registry/update.ts +0 -63
- package/src/commands/replay.ts +0 -559
- package/src/commands/run.ts +0 -454
- package/src/commands/status.ts +0 -163
- package/src/commands/template/index.ts +0 -59
- package/src/commands/template/validate.ts +0 -175
- package/src/commands/templates.ts +0 -99
- package/src/commands/tools.ts +0 -227
- package/src/commands/users.ts +0 -127
- package/src/commands/validate.ts +0 -46
- package/src/config.ts +0 -101
- package/src/index.ts +0 -201
- package/src/models-cache.ts +0 -127
- package/src/output/file-changes.ts +0 -97
- package/src/output/formatter.ts +0 -85
- package/src/output/formatters.ts +0 -303
- package/src/output/logger.ts +0 -18
- package/src/output/parallel-progress.ts +0 -103
- package/src/output/progress.ts +0 -414
- package/src/provider-validator.ts +0 -109
- package/src/registry/cache.ts +0 -46
- package/src/registry/client.ts +0 -84
- package/src/registry/lockfile.ts +0 -68
- package/src/registry/resolver.ts +0 -113
- package/src/registry/types.ts +0 -66
- package/src/run-logger.ts +0 -66
- package/src/run-store-factory.ts +0 -30
- package/src/studio-dir.ts +0 -28
- package/src/utils/input-wizard.ts +0 -73
- package/src/utils/placeholders.ts +0 -10
- package/templates/.studiorc.yaml +0 -16
- package/templates/projects/blank/metadata.json +0 -7
- package/templates/studio-config.yaml +0 -14
- package/tests/__stubs__/studio-runner.ts +0 -104
- package/tests/commands/api.test.ts +0 -110
- package/tests/commands/config.test.ts +0 -221
- package/tests/commands/init.test.ts +0 -919
- package/tests/commands/install.test.ts +0 -52
- package/tests/commands/integrations.test.ts +0 -158
- package/tests/commands/ollama.test.ts +0 -139
- package/tests/commands/project.test.ts +0 -179
- package/tests/commands/registry/audit.test.ts +0 -56
- package/tests/commands/registry/install.test.ts +0 -200
- package/tests/commands/registry/publish.test.ts +0 -56
- package/tests/commands/registry/remove.test.ts +0 -103
- package/tests/commands/registry/search.test.ts +0 -44
- package/tests/commands/registry/sync.test.ts +0 -37
- package/tests/commands/registry/update.test.ts +0 -62
- package/tests/commands/replay.test.ts +0 -283
- package/tests/commands/template/validate.test.ts +0 -150
- package/tests/commands/templates.test.ts +0 -42
- package/tests/commands/tools.test.ts +0 -106
- package/tests/config.test.ts +0 -142
- package/tests/formatter.test.ts +0 -158
- package/tests/integration/sigint.test.ts +0 -188
- package/tests/models-cache.test.ts +0 -250
- package/tests/output/file-changes.test.ts +0 -178
- package/tests/output/formatters.test.ts +0 -448
- package/tests/output/progress-spinner.test.ts +0 -232
- package/tests/output/progress-timer.test.ts +0 -230
- package/tests/provider-validator.test.ts +0 -182
- package/tests/registry/cache.test.ts +0 -66
- package/tests/registry/client.test.ts +0 -70
- package/tests/registry/lockfile.test.ts +0 -87
- package/tests/registry/resolver.test.ts +0 -122
- package/tests/run-logger-events.test.ts +0 -326
- package/tests/run-logger.test.ts +0 -64
- package/tests/run-store-factory.test.ts +0 -51
- package/tests/studio-dir.test.ts +0 -31
- package/tests/utils/input-wizard.test.ts +0 -153
- package/tests/utils/placeholders.test.ts +0 -36
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -20
package/src/output/progress.ts
DELETED
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import ora, { type Ora } from 'ora';
|
|
3
|
-
import type { EngineEvents } from '@studio-foundation/engine';
|
|
4
|
-
import { formatDuration } from './formatter.js';
|
|
5
|
-
import { summarizeToolCalls, getToolIcon, summarizeToolParams, summarizeToolResult, formatStageOutput, formatToolResult, formatTokens, formatStageLine, countWriteFiles } from './formatters.js';
|
|
6
|
-
import { ParallelRenderer } from './parallel-progress.js';
|
|
7
|
-
|
|
8
|
-
export class ProgressDisplay {
|
|
9
|
-
private spinner: Ora | null = null;
|
|
10
|
-
private spinnerText = '';
|
|
11
|
-
private toolSpinner: Ora | null = null;
|
|
12
|
-
private thinkingSpinner: Ora | null = null;
|
|
13
|
-
private currentToolText = '';
|
|
14
|
-
private isStreamingTokens = false;
|
|
15
|
-
private stageStartTime = 0;
|
|
16
|
-
private timerInterval: ReturnType<typeof setInterval> | null = null;
|
|
17
|
-
|
|
18
|
-
// Parallel group rendering
|
|
19
|
-
private isInParallelGroup = false;
|
|
20
|
-
private parallelRenderer: ParallelRenderer | null = null;
|
|
21
|
-
|
|
22
|
-
// State tracking for stage progress
|
|
23
|
-
runId = '';
|
|
24
|
-
private currentAttempt = 1;
|
|
25
|
-
private currentStageIndex = 0;
|
|
26
|
-
private currentTotalStages = 0;
|
|
27
|
-
private currentStageName = '';
|
|
28
|
-
|
|
29
|
-
readonly live: boolean;
|
|
30
|
-
readonly verbose: boolean;
|
|
31
|
-
|
|
32
|
-
constructor(
|
|
33
|
-
private jsonMode: boolean,
|
|
34
|
-
mode: 'quiet' | 'verbose' | 'live' | { live: boolean; verbose: boolean }
|
|
35
|
-
) {
|
|
36
|
-
if (typeof mode === 'string') {
|
|
37
|
-
this.live = mode === 'live';
|
|
38
|
-
this.verbose = mode === 'verbose';
|
|
39
|
-
} else {
|
|
40
|
-
this.live = mode.live;
|
|
41
|
-
this.verbose = mode.verbose;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private resetStageTimer(): void {
|
|
46
|
-
this.stageStartTime = Date.now();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private startTimer(updateFn: (elapsed: string) => void): void {
|
|
50
|
-
this.timerInterval = setInterval(() => {
|
|
51
|
-
const s = Math.floor((Date.now() - this.stageStartTime) / 1000);
|
|
52
|
-
updateFn(`${s}s`);
|
|
53
|
-
}, 1000);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private clearTimer(): void {
|
|
57
|
-
if (this.timerInterval) {
|
|
58
|
-
clearInterval(this.timerInterval);
|
|
59
|
-
this.timerInterval = null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private elapsedSeconds(): number {
|
|
64
|
-
return Math.floor((Date.now() - this.stageStartTime) / 1000);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interrupt(): void {
|
|
68
|
-
this.clearTimer();
|
|
69
|
-
this.parallelRenderer?.interrupt();
|
|
70
|
-
this.parallelRenderer = null;
|
|
71
|
-
this.isInParallelGroup = false;
|
|
72
|
-
if (this.isStreamingTokens) {
|
|
73
|
-
process.stdout.write('\n');
|
|
74
|
-
this.isStreamingTokens = false;
|
|
75
|
-
}
|
|
76
|
-
this.toolSpinner?.stop();
|
|
77
|
-
this.toolSpinner = null;
|
|
78
|
-
this.thinkingSpinner?.stop();
|
|
79
|
-
this.thinkingSpinner = null;
|
|
80
|
-
this.spinner?.stop();
|
|
81
|
-
this.spinner = null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
getEvents(): EngineEvents {
|
|
85
|
-
return {
|
|
86
|
-
onPipelineStart: (event) => {
|
|
87
|
-
if (this.jsonMode) return;
|
|
88
|
-
this.runId = event.run_id;
|
|
89
|
-
console.log(chalk.blue(`\nRunning pipeline: ${event.pipeline_name}\n`));
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
onStageStart: (event) => {
|
|
93
|
-
if (this.jsonMode) return;
|
|
94
|
-
this.currentStageIndex = event.stage_index;
|
|
95
|
-
this.currentTotalStages = event.total_stages;
|
|
96
|
-
this.currentStageName = event.stage_name;
|
|
97
|
-
this.currentAttempt = 1;
|
|
98
|
-
const prefix = `[${event.stage_index + 1}/${event.total_stages}]`;
|
|
99
|
-
|
|
100
|
-
if (this.isInParallelGroup) {
|
|
101
|
-
// Parallel mode: delegate to multi-line renderer (no ora spinner)
|
|
102
|
-
this.parallelRenderer!.addStage(prefix, event.stage_name);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (this.live) {
|
|
107
|
-
console.log(chalk.cyan(`${formatStageLine(prefix, event.stage_name, '')}...`));
|
|
108
|
-
this.thinkingSpinner = ora({ text: chalk.dim('Thinking... (0s)'), indent: 2, color: 'gray' }).start();
|
|
109
|
-
this.resetStageTimer();
|
|
110
|
-
this.startTimer((elapsed) => {
|
|
111
|
-
if (this.thinkingSpinner) {
|
|
112
|
-
this.thinkingSpinner.text = chalk.dim(`Thinking... (${elapsed})`);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
} else {
|
|
116
|
-
this.spinnerText = formatStageLine(prefix, event.stage_name, '');
|
|
117
|
-
this.spinner = ora({ text: this.spinnerText, color: 'cyan' }).start();
|
|
118
|
-
this.resetStageTimer();
|
|
119
|
-
this.startTimer((elapsed) => {
|
|
120
|
-
if (this.spinner) {
|
|
121
|
-
this.spinner.text = formatStageLine(prefix, event.stage_name, `(${elapsed})`);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
onStageComplete: (event) => {
|
|
128
|
-
if (this.jsonMode) return;
|
|
129
|
-
|
|
130
|
-
const duration = formatDuration(event.duration_ms);
|
|
131
|
-
const prefix = `[${event.stage_index + 1}/${event.total_stages}]`;
|
|
132
|
-
|
|
133
|
-
// Build compact info parts: duration, tokens, files
|
|
134
|
-
const infoParts: string[] = [duration];
|
|
135
|
-
if (event.token_usage && event.token_usage.total_tokens > 0) {
|
|
136
|
-
infoParts.push(`${formatTokens(event.token_usage.total_tokens)} tokens`);
|
|
137
|
-
}
|
|
138
|
-
const filesWritten = event.tool_calls ? countWriteFiles(event.tool_calls) : 0;
|
|
139
|
-
if (filesWritten > 0) {
|
|
140
|
-
infoParts.push(`${filesWritten} file${filesWritten !== 1 ? 's' : ''}`);
|
|
141
|
-
}
|
|
142
|
-
const infoStr = infoParts.join(', ');
|
|
143
|
-
|
|
144
|
-
if (this.isInParallelGroup) {
|
|
145
|
-
// Parallel mode: freeze the line with final status
|
|
146
|
-
let finalText: string;
|
|
147
|
-
if (event.status === 'success') {
|
|
148
|
-
finalText = formatStageLine(prefix, event.stage_name, chalk.green('✓') + chalk.gray(` (${infoStr})`));
|
|
149
|
-
} else if (event.status === 'rejected') {
|
|
150
|
-
finalText = formatStageLine(prefix, event.stage_name, chalk.red('✗ rejected') + chalk.gray(` (${duration})`));
|
|
151
|
-
} else {
|
|
152
|
-
finalText = formatStageLine(prefix, event.stage_name, chalk.red('✗ failed') + chalk.gray(` (${infoStr})`));
|
|
153
|
-
}
|
|
154
|
-
this.parallelRenderer?.completeStage(event.stage_name, finalText);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (this.live) {
|
|
159
|
-
if (this.isStreamingTokens) {
|
|
160
|
-
process.stdout.write('\n');
|
|
161
|
-
this.isStreamingTokens = false;
|
|
162
|
-
}
|
|
163
|
-
this.clearTimer();
|
|
164
|
-
this.thinkingSpinner?.stop();
|
|
165
|
-
this.thinkingSpinner = null;
|
|
166
|
-
if (event.status === 'success') {
|
|
167
|
-
console.log(chalk.green(` ✓`) + chalk.gray(` (${infoStr})`));
|
|
168
|
-
} else if (event.status === 'rejected') {
|
|
169
|
-
console.log(chalk.red(` ✗ rejected`) + chalk.gray(` (${duration})`));
|
|
170
|
-
if (event.rejection_reason) console.log(chalk.red(` ${event.rejection_reason}`));
|
|
171
|
-
if (event.rejection_details?.length) {
|
|
172
|
-
for (const detail of event.rejection_details) {
|
|
173
|
-
console.log(chalk.yellow(` - ${detail}`));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
console.log(chalk.red(` ✗ failed`) + chalk.gray(` (${infoStr})`));
|
|
178
|
-
}
|
|
179
|
-
} else if (event.status === 'success') {
|
|
180
|
-
this.spinner?.succeed(
|
|
181
|
-
formatStageLine(prefix, event.stage_name, chalk.green('✓') + chalk.gray(` (${infoStr})`))
|
|
182
|
-
);
|
|
183
|
-
} else if (event.status === 'rejected') {
|
|
184
|
-
this.spinner?.fail(
|
|
185
|
-
formatStageLine(prefix, event.stage_name, chalk.red('✗ rejected') + chalk.gray(` (${duration})`))
|
|
186
|
-
);
|
|
187
|
-
if (event.rejection_reason) {
|
|
188
|
-
console.log(chalk.red(` ${event.rejection_reason}`));
|
|
189
|
-
}
|
|
190
|
-
if (event.rejection_details?.length) {
|
|
191
|
-
for (const detail of event.rejection_details) {
|
|
192
|
-
console.log(chalk.yellow(` - ${detail}`));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
this.spinner?.fail(
|
|
197
|
-
formatStageLine(prefix, event.stage_name, chalk.red('✗ failed') + chalk.gray(` (${infoStr})`))
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
this.clearTimer();
|
|
201
|
-
this.spinner = null;
|
|
202
|
-
|
|
203
|
-
// Tool call summary: quiet + verbose only (in live mode, each was shown individually)
|
|
204
|
-
if (!this.live && event.tool_calls && event.tool_calls.length > 0) {
|
|
205
|
-
const summary = summarizeToolCalls(event.tool_calls);
|
|
206
|
-
if (summary) console.log(chalk.gray(` ${summary}`));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Formatted output: verbose only
|
|
210
|
-
if (this.verbose && event.status !== 'rejected' && event.output && typeof event.output === 'object') {
|
|
211
|
-
const depth = Infinity;
|
|
212
|
-
const formatted = formatStageOutput(event.output as Record<string, unknown>, depth);
|
|
213
|
-
if (formatted) {
|
|
214
|
-
for (const line of formatted.split('\n')) {
|
|
215
|
-
console.log(chalk.gray(` ${line}`));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Token breakdown: verbose mode (both standalone and live+verbose)
|
|
221
|
-
if (this.verbose && event.token_usage) {
|
|
222
|
-
const u = event.token_usage;
|
|
223
|
-
console.log(chalk.gray(` Tokens: ${u.prompt_tokens} prompt + ${u.completion_tokens} completion = ${u.total_tokens} total`));
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
|
|
227
|
-
onTaskRetry: (event) => {
|
|
228
|
-
if (this.jsonMode) return;
|
|
229
|
-
|
|
230
|
-
if (this.isInParallelGroup) {
|
|
231
|
-
// In parallel mode the spinner keeps running; nothing to do here.
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
this.clearTimer();
|
|
236
|
-
// Stop any active spinners before printing retry info
|
|
237
|
-
if (this.isStreamingTokens) {
|
|
238
|
-
process.stdout.write('\n');
|
|
239
|
-
this.isStreamingTokens = false;
|
|
240
|
-
}
|
|
241
|
-
this.toolSpinner?.stop();
|
|
242
|
-
this.toolSpinner = null;
|
|
243
|
-
this.thinkingSpinner?.stop();
|
|
244
|
-
this.thinkingSpinner = null;
|
|
245
|
-
|
|
246
|
-
this.currentAttempt = event.attempt + 1;
|
|
247
|
-
const prefix = `[${this.currentStageIndex + 1}/${this.currentTotalStages}]`;
|
|
248
|
-
const reason = event.failures.length > 0 ? event.failures[0] : 'validation failed';
|
|
249
|
-
|
|
250
|
-
if (this.live) {
|
|
251
|
-
console.log(chalk.yellow(` ✗ retry (${reason})`));
|
|
252
|
-
} else {
|
|
253
|
-
this.spinner?.fail(
|
|
254
|
-
formatStageLine(prefix, this.currentStageName, chalk.yellow('✗ retry') + chalk.gray(` (${reason})`))
|
|
255
|
-
);
|
|
256
|
-
this.spinner = null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Verbose extras
|
|
260
|
-
if (this.verbose && event.failures.length > 1) {
|
|
261
|
-
for (const failure of event.failures.slice(1)) {
|
|
262
|
-
console.log(chalk.yellow(` - ${failure}`));
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (this.verbose && event.agent_output_raw) {
|
|
266
|
-
console.log(chalk.gray(` Raw response: ${event.agent_output_raw.slice(0, 300)}`));
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Restart spinner with next attempt counter
|
|
270
|
-
if (!this.live) {
|
|
271
|
-
const suffix = `(attempt ${this.currentAttempt}/${event.max_attempts})`;
|
|
272
|
-
this.spinnerText = formatStageLine(prefix, this.currentStageName, suffix);
|
|
273
|
-
this.spinner = ora({ text: this.spinnerText, color: 'cyan' }).start();
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
|
|
277
|
-
onGroupStart: (event) => {
|
|
278
|
-
if (event.parallel) {
|
|
279
|
-
this.isInParallelGroup = true;
|
|
280
|
-
this.parallelRenderer = new ParallelRenderer();
|
|
281
|
-
}
|
|
282
|
-
// Otherwise silent — sequential group is transparent at the pipeline level
|
|
283
|
-
},
|
|
284
|
-
|
|
285
|
-
onGroupIteration: (event) => {
|
|
286
|
-
if (this.jsonMode) return;
|
|
287
|
-
if (event.iteration > 1) {
|
|
288
|
-
console.log(chalk.yellow(`\n ↻ Feedback loop iteration ${event.iteration}/${event.max_iterations}`));
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
|
|
292
|
-
onGroupFeedback: (event) => {
|
|
293
|
-
if (this.jsonMode) return;
|
|
294
|
-
console.log(chalk.yellow(` Rejected: ${event.rejection_reason}`));
|
|
295
|
-
if (this.verbose && event.rejection_details.length > 0) {
|
|
296
|
-
for (const detail of event.rejection_details) {
|
|
297
|
-
console.log(chalk.yellow(` - ${detail}`));
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
console.log(chalk.yellow(` Re-running with feedback...`));
|
|
301
|
-
},
|
|
302
|
-
|
|
303
|
-
onGroupComplete: (event) => {
|
|
304
|
-
if (this.isInParallelGroup) {
|
|
305
|
-
this.parallelRenderer?.stop();
|
|
306
|
-
this.parallelRenderer = null;
|
|
307
|
-
this.isInParallelGroup = false;
|
|
308
|
-
}
|
|
309
|
-
if (this.jsonMode) return;
|
|
310
|
-
if (event.iterations > 1) {
|
|
311
|
-
if (event.status === 'success') {
|
|
312
|
-
console.log(chalk.green(` ✓ Approved after ${event.iterations} iterations`));
|
|
313
|
-
} else {
|
|
314
|
-
console.log(chalk.red(` ✗ Rejected after ${event.iterations} iterations (max reached)`));
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
|
|
319
|
-
onAgentThinking: (event) => {
|
|
320
|
-
if (this.jsonMode || !this.live || this.isInParallelGroup) return;
|
|
321
|
-
for (const line of event.thought.trim().split('\n')) {
|
|
322
|
-
const trimmed = line.trim();
|
|
323
|
-
if (trimmed) console.log(chalk.dim(` 🤔 ${trimmed}`));
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
|
|
327
|
-
onAgentProgress: (event) => {
|
|
328
|
-
if (this.jsonMode || !this.live || this.isInParallelGroup) return;
|
|
329
|
-
for (const line of event.message.trim().split('\n')) {
|
|
330
|
-
const trimmed = line.trim();
|
|
331
|
-
if (trimmed) console.log(chalk.dim(` 💭 ${trimmed}`));
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
|
|
335
|
-
onAgentToken: (event) => {
|
|
336
|
-
if (this.jsonMode || !this.live || this.isInParallelGroup) return;
|
|
337
|
-
if (this.thinkingSpinner) {
|
|
338
|
-
this.clearTimer();
|
|
339
|
-
this.thinkingSpinner.stop();
|
|
340
|
-
this.thinkingSpinner = null;
|
|
341
|
-
process.stdout.write(' '); // indent to match spinner position
|
|
342
|
-
}
|
|
343
|
-
this.isStreamingTokens = true;
|
|
344
|
-
process.stdout.write(chalk.dim(event.token));
|
|
345
|
-
},
|
|
346
|
-
|
|
347
|
-
onToolCallStart: (event) => {
|
|
348
|
-
if (this.jsonMode || !this.live || this.isInParallelGroup) return;
|
|
349
|
-
// End any in-progress token stream line before starting tool spinner
|
|
350
|
-
if (this.isStreamingTokens) {
|
|
351
|
-
process.stdout.write('\n');
|
|
352
|
-
this.isStreamingTokens = false;
|
|
353
|
-
}
|
|
354
|
-
this.clearTimer();
|
|
355
|
-
this.thinkingSpinner?.stop();
|
|
356
|
-
this.thinkingSpinner = null;
|
|
357
|
-
const icon = getToolIcon(event.tool);
|
|
358
|
-
const params = summarizeToolParams(event.tool, event.params);
|
|
359
|
-
this.currentToolText = `${icon} ${event.tool}${params}`;
|
|
360
|
-
this.toolSpinner = ora({
|
|
361
|
-
text: chalk.white(this.currentToolText),
|
|
362
|
-
indent: 2,
|
|
363
|
-
color: 'cyan',
|
|
364
|
-
}).start();
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
onToolCallComplete: (event) => {
|
|
368
|
-
if (this.jsonMode || !this.live || this.isInParallelGroup) return;
|
|
369
|
-
const summary = summarizeToolResult(event.result, event.error);
|
|
370
|
-
if (event.error) {
|
|
371
|
-
this.toolSpinner?.fail(chalk.red(`${this.currentToolText} — ${event.error}`));
|
|
372
|
-
} else {
|
|
373
|
-
this.toolSpinner?.succeed(chalk.white(this.currentToolText) + chalk.gray(` → ${summary}`));
|
|
374
|
-
}
|
|
375
|
-
this.toolSpinner = null;
|
|
376
|
-
|
|
377
|
-
// Verbose: print full tool result below the spinner line
|
|
378
|
-
if (this.verbose && !event.error) {
|
|
379
|
-
const full = formatToolResult(event.result);
|
|
380
|
-
for (const line of full.split('\n')) {
|
|
381
|
-
console.log(chalk.gray(line));
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Restart thinking spinner even on error — LLM still processes the result and may retry
|
|
386
|
-
const fromSec = this.elapsedSeconds();
|
|
387
|
-
this.thinkingSpinner = ora({ text: chalk.dim(`Thinking... (from ${fromSec}s)`), indent: 2, color: 'gray' }).start();
|
|
388
|
-
this.thinkingSpinner.text = chalk.dim(`Thinking... (from ${fromSec}s)`);
|
|
389
|
-
this.startTimer((elapsed) => {
|
|
390
|
-
if (this.thinkingSpinner) {
|
|
391
|
-
this.thinkingSpinner.text = chalk.dim(`Thinking... (from ${elapsed})`);
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
},
|
|
395
|
-
|
|
396
|
-
onPipelineComplete: (event) => {
|
|
397
|
-
if (this.jsonMode) return;
|
|
398
|
-
|
|
399
|
-
console.log('');
|
|
400
|
-
const duration = formatDuration(event.duration_ms);
|
|
401
|
-
const tokenStr = event.total_tokens > 0 ? `, ${formatTokens(event.total_tokens)} tokens` : '';
|
|
402
|
-
const toolStr = event.total_tool_calls > 0 ? `, ${event.total_tool_calls} tool calls` : '';
|
|
403
|
-
|
|
404
|
-
if (event.status === 'success') {
|
|
405
|
-
console.log(chalk.green('✓ Pipeline completed') + chalk.gray(` (${duration}${tokenStr}${toolStr})`));
|
|
406
|
-
} else if (event.status === 'rejected') {
|
|
407
|
-
console.log(chalk.red('✗ Pipeline rejected') + chalk.gray(` (${duration}${tokenStr})`));
|
|
408
|
-
} else {
|
|
409
|
-
console.log(chalk.red('✗ Pipeline failed') + chalk.gray(` (${duration}${tokenStr})`));
|
|
410
|
-
}
|
|
411
|
-
},
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { parseAndCacheModels } from './models-cache.js';
|
|
2
|
-
|
|
3
|
-
export type ValidationResult =
|
|
4
|
-
| { status: 'valid' }
|
|
5
|
-
| { status: 'invalid'; error: string }
|
|
6
|
-
| { status: 'warning'; message: string };
|
|
7
|
-
|
|
8
|
-
export interface ValidateOptions {
|
|
9
|
-
baseUrl?: string;
|
|
10
|
-
timeoutMs?: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const DEFAULT_TIMEOUT_MS = 5000;
|
|
14
|
-
|
|
15
|
-
export async function validateApiKeyLive(
|
|
16
|
-
provider: string,
|
|
17
|
-
apiKey: string,
|
|
18
|
-
options: ValidateOptions = {}
|
|
19
|
-
): Promise<ValidationResult> {
|
|
20
|
-
const abort = new AbortController();
|
|
21
|
-
const timer = setTimeout(
|
|
22
|
-
() => abort.abort(),
|
|
23
|
-
options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
if (provider === 'anthropic') {
|
|
28
|
-
return await validateAnthropicKey(apiKey, abort.signal);
|
|
29
|
-
} else if (provider === 'openai') {
|
|
30
|
-
return await validateOpenAIKey(apiKey, abort.signal);
|
|
31
|
-
} else if (provider === 'google') {
|
|
32
|
-
return await validateGoogleKey(apiKey, abort.signal);
|
|
33
|
-
} else if (provider === 'ollama') {
|
|
34
|
-
return await validateLocalOllama(
|
|
35
|
-
options.baseUrl ?? 'http://localhost:11434',
|
|
36
|
-
abort.signal
|
|
37
|
-
);
|
|
38
|
-
} else {
|
|
39
|
-
return { status: 'warning', message: `Cannot validate unknown provider '${provider}'` };
|
|
40
|
-
}
|
|
41
|
-
} catch (err) {
|
|
42
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
43
|
-
return { status: 'warning', message: 'Validation timed out — proceeding anyway' };
|
|
44
|
-
}
|
|
45
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
-
return { status: 'warning', message: `Could not validate key: ${message}` };
|
|
47
|
-
} finally {
|
|
48
|
-
clearTimeout(timer);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function validateAnthropicKey(
|
|
53
|
-
apiKey: string,
|
|
54
|
-
signal: AbortSignal
|
|
55
|
-
): Promise<ValidationResult> {
|
|
56
|
-
const res = await fetch('https://api.anthropic.com/v1/models', {
|
|
57
|
-
signal,
|
|
58
|
-
headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
|
|
59
|
-
});
|
|
60
|
-
if (res.status === 200) {
|
|
61
|
-
try { parseAndCacheModels('anthropic', apiKey, await res.json()); } catch { /* ignore */ }
|
|
62
|
-
return { status: 'valid' };
|
|
63
|
-
}
|
|
64
|
-
if (res.status === 401 || res.status === 403) {
|
|
65
|
-
return { status: 'invalid', error: `Invalid key (${res.status} Unauthorized)` };
|
|
66
|
-
}
|
|
67
|
-
return { status: 'warning', message: `Unexpected response ${res.status} — proceeding anyway` };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function validateOpenAIKey(
|
|
71
|
-
apiKey: string,
|
|
72
|
-
signal: AbortSignal
|
|
73
|
-
): Promise<ValidationResult> {
|
|
74
|
-
const res = await fetch('https://api.openai.com/v1/models', {
|
|
75
|
-
signal,
|
|
76
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
77
|
-
});
|
|
78
|
-
if (res.status === 200) {
|
|
79
|
-
try { parseAndCacheModels('openai', apiKey, await res.json()); } catch { /* ignore */ }
|
|
80
|
-
return { status: 'valid' };
|
|
81
|
-
}
|
|
82
|
-
if (res.status === 401 || res.status === 403) {
|
|
83
|
-
return { status: 'invalid', error: `Invalid key (${res.status} Unauthorized)` };
|
|
84
|
-
}
|
|
85
|
-
return { status: 'warning', message: `Unexpected response ${res.status} — proceeding anyway` };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function validateGoogleKey(
|
|
89
|
-
apiKey: string,
|
|
90
|
-
signal: AbortSignal
|
|
91
|
-
): Promise<ValidationResult> {
|
|
92
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`;
|
|
93
|
-
const res = await fetch(url, { signal });
|
|
94
|
-
if (res.status === 200) return { status: 'valid' };
|
|
95
|
-
if (res.status === 400 || res.status === 403) {
|
|
96
|
-
return { status: 'invalid', error: `Invalid key (${res.status})` };
|
|
97
|
-
}
|
|
98
|
-
return { status: 'warning', message: `Unexpected response ${res.status} — proceeding anyway` };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function validateLocalOllama(
|
|
102
|
-
baseUrl: string,
|
|
103
|
-
signal: AbortSignal
|
|
104
|
-
): Promise<ValidationResult> {
|
|
105
|
-
const url = baseUrl.replace(/\/$/, '') + '/api/tags';
|
|
106
|
-
const res = await fetch(url, { signal });
|
|
107
|
-
if (res.status === 200) return { status: 'valid' };
|
|
108
|
-
return { status: 'warning', message: `Ollama returned ${res.status} — proceeding anyway` };
|
|
109
|
-
}
|
package/src/registry/cache.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import type { RegistryIndex } from './types.js';
|
|
5
|
-
|
|
6
|
-
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
7
|
-
|
|
8
|
-
interface CachedIndex extends RegistryIndex {
|
|
9
|
-
_cached_at: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class RegistryCache {
|
|
13
|
-
private cacheDir: string;
|
|
14
|
-
private cachePath: string;
|
|
15
|
-
|
|
16
|
-
constructor(cacheDir?: string) {
|
|
17
|
-
this.cacheDir = cacheDir ?? resolve(homedir(), '.cache', 'studio', 'registry');
|
|
18
|
-
this.cachePath = resolve(this.cacheDir, 'index.json');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async read(): Promise<RegistryIndex | null> {
|
|
22
|
-
let raw: string;
|
|
23
|
-
try {
|
|
24
|
-
raw = await readFile(this.cachePath, 'utf8');
|
|
25
|
-
} catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const data = JSON.parse(raw) as CachedIndex;
|
|
30
|
-
const age = Date.now() - new Date(data._cached_at).getTime();
|
|
31
|
-
if (age > CACHE_TTL_MS) return null;
|
|
32
|
-
|
|
33
|
-
const { _cached_at: _, ...index } = data;
|
|
34
|
-
return index as RegistryIndex;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async write(index: RegistryIndex): Promise<void> {
|
|
38
|
-
await mkdir(this.cacheDir, { recursive: true });
|
|
39
|
-
const data: CachedIndex = { ...index, _cached_at: new Date().toISOString() };
|
|
40
|
-
await writeFile(this.cachePath, JSON.stringify(data, null, 2) + '\n');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async isFresh(): Promise<boolean> {
|
|
44
|
-
return (await this.read()) !== null;
|
|
45
|
-
}
|
|
46
|
-
}
|
package/src/registry/client.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
-
import { resolve, dirname } from 'node:path';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
|
-
import type { RegistryIndex, PackageMetadata, PackageType } from './types.js';
|
|
5
|
-
import { REGISTRY_RAW_BASE, REGISTRY_API_BASE } from './types.js';
|
|
6
|
-
|
|
7
|
-
interface GitHubContentItem {
|
|
8
|
-
name: string;
|
|
9
|
-
path: string;
|
|
10
|
-
type: 'file' | 'dir';
|
|
11
|
-
download_url: string | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class RegistryClient {
|
|
15
|
-
async fetchIndex(): Promise<RegistryIndex> {
|
|
16
|
-
const res = await fetch(`${REGISTRY_RAW_BASE}/index.json`);
|
|
17
|
-
if (!res.ok) throw new Error(`Failed to fetch registry index: HTTP ${res.status}`);
|
|
18
|
-
return res.json() as Promise<RegistryIndex>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async fetchMetadata(type: PackageType, name: string): Promise<PackageMetadata> {
|
|
22
|
-
const res = await fetch(`${REGISTRY_RAW_BASE}/${type}s/${name}/metadata.json`);
|
|
23
|
-
if (!res.ok) throw new Error(`Package '${name}' not found in registry`);
|
|
24
|
-
return res.json() as Promise<PackageMetadata>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Download a single-file package (tool, pipeline, integration, agent, skill).
|
|
29
|
-
* Returns { destPath, sha256 }.
|
|
30
|
-
*/
|
|
31
|
-
async downloadFile(
|
|
32
|
-
type: PackageType,
|
|
33
|
-
name: string,
|
|
34
|
-
filename: string,
|
|
35
|
-
destDir: string,
|
|
36
|
-
): Promise<{ destPath: string; sha256: string }> {
|
|
37
|
-
const url = `${REGISTRY_RAW_BASE}/${type}s/${name}/${filename}`;
|
|
38
|
-
const res = await fetch(url);
|
|
39
|
-
if (!res.ok) throw new Error(`Failed to download ${filename}: HTTP ${res.status}`);
|
|
40
|
-
const content = await res.text();
|
|
41
|
-
const destPath = resolve(destDir, filename);
|
|
42
|
-
await mkdir(destDir, { recursive: true });
|
|
43
|
-
await writeFile(destPath, content);
|
|
44
|
-
const sha256 = createHash('sha256').update(content).digest('hex');
|
|
45
|
-
return { destPath, sha256 };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Download a directory package (template, plugin) via GitHub API.
|
|
50
|
-
* Returns SHA256 of all file contents concatenated (sorted by path).
|
|
51
|
-
*/
|
|
52
|
-
async downloadDirectory(
|
|
53
|
-
type: PackageType,
|
|
54
|
-
name: string,
|
|
55
|
-
remotePath: string,
|
|
56
|
-
localDestDir: string,
|
|
57
|
-
): Promise<string> {
|
|
58
|
-
const res = await fetch(
|
|
59
|
-
`${REGISTRY_API_BASE}/contents/${type}s/${name}/${remotePath}`,
|
|
60
|
-
{ headers: { Accept: 'application/vnd.github+json' } },
|
|
61
|
-
);
|
|
62
|
-
if (!res.ok) throw new Error(`Failed to list directory: HTTP ${res.status}`);
|
|
63
|
-
const items = (await res.json()) as GitHubContentItem[];
|
|
64
|
-
|
|
65
|
-
const hash = createHash('sha256');
|
|
66
|
-
const sortedItems = [...items].sort((a, b) => a.path.localeCompare(b.path));
|
|
67
|
-
|
|
68
|
-
for (const item of sortedItems) {
|
|
69
|
-
const localPath = resolve(localDestDir, item.name);
|
|
70
|
-
if (item.type === 'dir') {
|
|
71
|
-
await this.downloadDirectory(type, name, `${remotePath}/${item.name}`, localPath);
|
|
72
|
-
} else if (item.download_url) {
|
|
73
|
-
const fileRes = await fetch(item.download_url);
|
|
74
|
-
if (!fileRes.ok) throw new Error(`Failed to download ${item.path}`);
|
|
75
|
-
const content = await fileRes.text();
|
|
76
|
-
await mkdir(dirname(localPath), { recursive: true });
|
|
77
|
-
await writeFile(localPath, content);
|
|
78
|
-
hash.update(item.path + content);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return hash.digest('hex');
|
|
83
|
-
}
|
|
84
|
-
}
|