@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/commands/install.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
|
|
4
|
-
const KNOWN_EXTENSIONS: Record<string, string> = {
|
|
5
|
-
api: '@studio-foundation/api',
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export async function installExtensionCommand(extension: string): Promise<void> {
|
|
9
|
-
const pkg = KNOWN_EXTENSIONS[extension];
|
|
10
|
-
if (!pkg) {
|
|
11
|
-
console.error(`Unknown extension: ${extension}. Available: ${Object.keys(KNOWN_EXTENSIONS).join(', ')}`);
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
console.log(`Installing ${pkg}...`);
|
|
16
|
-
try {
|
|
17
|
-
execSync(`npm install -g ${pkg}`, { stdio: 'inherit' });
|
|
18
|
-
console.log(chalk.green(`✓ ${pkg} installed. Run: studio api start`));
|
|
19
|
-
} catch {
|
|
20
|
-
console.error(chalk.red(`Failed to install ${pkg}`));
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, access, unlink } from 'node:fs/promises';
|
|
2
|
-
import { resolve, join, dirname } from 'node:path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import ora from 'ora';
|
|
5
|
-
import * as yaml from 'js-yaml';
|
|
6
|
-
import { getBundledIntegrationTemplate, listAvailableIntegrationTemplates, loadProjectIntegrations } from '@studio-foundation/runner';
|
|
7
|
-
import type { IntegrationPluginDef } from '@studio-foundation/contracts';
|
|
8
|
-
import { loadConfig, resolveEnvVars } from '../config.js';
|
|
9
|
-
import { setConfigValue } from './config.js';
|
|
10
|
-
|
|
11
|
-
export function getIntegrationsDir(studioDir: string): string {
|
|
12
|
-
return resolve(studioDir, 'integrations');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function getIntegrationStatus(
|
|
16
|
-
plugin: IntegrationPluginDef,
|
|
17
|
-
config: Record<string, unknown>
|
|
18
|
-
): 'configured' | 'not-configured' {
|
|
19
|
-
const required = plugin.config?.required ?? [];
|
|
20
|
-
const allSet = required.every(key => key in config && config[key] !== '');
|
|
21
|
-
return allSet ? 'configured' : 'not-configured';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function loadRawIntegrationsConfig(studioDir: string): Promise<Record<string, Record<string, unknown>>> {
|
|
25
|
-
const configFile = join(studioDir, 'config.yaml');
|
|
26
|
-
try {
|
|
27
|
-
const raw = await readFile(configFile, 'utf-8');
|
|
28
|
-
const parsed = yaml.load(resolveEnvVars(raw)) as Record<string, unknown>;
|
|
29
|
-
return (parsed?.['integrations'] ?? {}) as Record<string, Record<string, unknown>>;
|
|
30
|
-
} catch {
|
|
31
|
-
return {};
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function formatExtras(plugin: IntegrationPluginDef, config: Record<string, unknown>): string {
|
|
36
|
-
if (plugin.name === 'linear') {
|
|
37
|
-
const autoTrigger = config['autoTrigger'] ?? plugin.config?.optional?.['autoTrigger'] ?? false;
|
|
38
|
-
return `auto-trigger: ${autoTrigger ? 'on' : 'off'}`;
|
|
39
|
-
}
|
|
40
|
-
if (plugin.name === 'slack') {
|
|
41
|
-
const channel = config['channel'] ?? plugin.config?.optional?.['channel'] ?? '';
|
|
42
|
-
return channel ? `channel: ${String(channel)}` : '';
|
|
43
|
-
}
|
|
44
|
-
return '';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function resolveStudioDir(): Promise<string> {
|
|
48
|
-
const config = await loadConfig();
|
|
49
|
-
const studioDir = config.resolvedStudioDir;
|
|
50
|
-
if (!studioDir) {
|
|
51
|
-
console.error("Error: No .studio/ directory found. Run 'studio init' first.");
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
return studioDir;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function installIntegration(source: string, integrationsDir: string): Promise<string> {
|
|
58
|
-
await mkdir(integrationsDir, { recursive: true });
|
|
59
|
-
|
|
60
|
-
let name: string;
|
|
61
|
-
let content: string;
|
|
62
|
-
|
|
63
|
-
if (source.startsWith('@studio-foundation/integration-')) {
|
|
64
|
-
name = source.replace('@studio-foundation/integration-', '');
|
|
65
|
-
const bundled = await getBundledIntegrationTemplate(name);
|
|
66
|
-
if (!bundled) {
|
|
67
|
-
const available = await listAvailableIntegrationTemplates();
|
|
68
|
-
throw new Error(
|
|
69
|
-
`Unknown integration '${name}'. Available: ${available.map(t => t.name).join(', ')}`
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
content = bundled;
|
|
73
|
-
} else {
|
|
74
|
-
try {
|
|
75
|
-
content = await readFile(source, 'utf-8');
|
|
76
|
-
} catch {
|
|
77
|
-
throw new Error(`File not found: ${source}`);
|
|
78
|
-
}
|
|
79
|
-
const def = yaml.load(content) as IntegrationPluginDef;
|
|
80
|
-
name = def.name;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const destPath = join(integrationsDir, `${name}.integration.yaml`);
|
|
84
|
-
const alreadyExists = await access(destPath).then(() => true).catch(() => false);
|
|
85
|
-
if (alreadyExists) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`'${name}' already installed. Run: studio integrations remove ${name}`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
await writeFile(destPath, content, 'utf-8');
|
|
92
|
-
return name;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export async function removeIntegration(name: string, integrationsDir: string): Promise<void> {
|
|
96
|
-
const filePath = join(integrationsDir, `${name}.integration.yaml`);
|
|
97
|
-
try {
|
|
98
|
-
await unlink(filePath);
|
|
99
|
-
} catch {
|
|
100
|
-
throw new Error(`Integration '${name}' not found`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function loadRawFullConfig(configFile: string): Promise<Record<string, unknown>> {
|
|
105
|
-
try {
|
|
106
|
-
const raw = await readFile(configFile, 'utf-8');
|
|
107
|
-
const parsed = yaml.load(resolveEnvVars(raw));
|
|
108
|
-
return (parsed && typeof parsed === 'object' ? parsed : {}) as Record<string, unknown>;
|
|
109
|
-
} catch {
|
|
110
|
-
return {};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function saveIntegrationConfig(configFile: string, config: Record<string, unknown>): Promise<void> {
|
|
115
|
-
await mkdir(dirname(configFile), { recursive: true });
|
|
116
|
-
await writeFile(configFile, yaml.dump(config), 'utf-8');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface IntegrationTestResult {
|
|
120
|
-
success: boolean;
|
|
121
|
-
statusCode?: number;
|
|
122
|
-
body?: string;
|
|
123
|
-
error?: string;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export async function runIntegrationTest(
|
|
127
|
-
plugin: IntegrationPluginDef,
|
|
128
|
-
config: Record<string, unknown>,
|
|
129
|
-
fetcher: typeof fetch = fetch
|
|
130
|
-
): Promise<IntegrationTestResult> {
|
|
131
|
-
const testDef = plugin.test;
|
|
132
|
-
if (!testDef) {
|
|
133
|
-
throw new Error(`Integration '${plugin.name}' has no test: configuration`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const resolveVar = (str: string) =>
|
|
137
|
-
str.replace(/\$\{([^}]+)\}/g, (_, key: string) => String(config[key.trim()] ?? ''));
|
|
138
|
-
|
|
139
|
-
const headers: Record<string, string> = {
|
|
140
|
-
'Content-Type': 'application/json',
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
if (testDef.auth) {
|
|
144
|
-
const resolvedAuth = resolveVar(testDef.auth);
|
|
145
|
-
const colonIdx = resolvedAuth.indexOf(':');
|
|
146
|
-
if (colonIdx !== -1) {
|
|
147
|
-
const scheme = resolvedAuth.slice(0, colonIdx);
|
|
148
|
-
const token = resolvedAuth.slice(colonIdx + 1);
|
|
149
|
-
headers['Authorization'] = `${scheme.charAt(0).toUpperCase()}${scheme.slice(1)} ${token}`;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const response = await fetcher(testDef.endpoint, {
|
|
155
|
-
method: testDef.method ?? 'GET',
|
|
156
|
-
headers,
|
|
157
|
-
...(testDef.body ? { body: testDef.body } : {}),
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const body = await response.text().catch(() => '');
|
|
161
|
-
const expectedStatus = testDef.expect?.status ?? 200;
|
|
162
|
-
|
|
163
|
-
if (response.status !== expectedStatus) {
|
|
164
|
-
return { success: false, statusCode: response.status, body };
|
|
165
|
-
}
|
|
166
|
-
return { success: true, statusCode: response.status, body };
|
|
167
|
-
} catch (err) {
|
|
168
|
-
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export async function integrationsCommand(
|
|
173
|
-
action: string,
|
|
174
|
-
_args: string[],
|
|
175
|
-
_options: Record<string, string | boolean | undefined>
|
|
176
|
-
): Promise<void> {
|
|
177
|
-
try {
|
|
178
|
-
switch (action) {
|
|
179
|
-
case 'install': {
|
|
180
|
-
const source = _args[0];
|
|
181
|
-
if (!source) {
|
|
182
|
-
console.error('Usage: studio integrations install <source>');
|
|
183
|
-
console.error(' <source> can be @studio/integration-<name> or a local file path');
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
const studioDir = await resolveStudioDir();
|
|
187
|
-
const intDir = getIntegrationsDir(studioDir);
|
|
188
|
-
const spinner = ora(`Installing ${source}...`).start();
|
|
189
|
-
try {
|
|
190
|
-
const name = await installIntegration(source, intDir);
|
|
191
|
-
spinner.succeed(chalk.green(`✓ Integration '${name}' installed`));
|
|
192
|
-
console.log(`\n Configure with: ${chalk.cyan(`studio integrations set ${name}.<key> <value>`)}`);
|
|
193
|
-
console.log(` Test with: ${chalk.cyan(`studio integrations test ${name}`)}\n`);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
spinner.fail(err instanceof Error ? err.message : String(err));
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
case 'list': {
|
|
201
|
-
const studioDir = await resolveStudioDir();
|
|
202
|
-
const intDir = getIntegrationsDir(studioDir);
|
|
203
|
-
const plugins = await loadProjectIntegrations(intDir);
|
|
204
|
-
|
|
205
|
-
if (plugins.length === 0) {
|
|
206
|
-
console.log(chalk.yellow('\nNo integrations installed.'));
|
|
207
|
-
console.log(` Run: ${chalk.cyan('studio integrations install @studio/integration-<name>')}\n`);
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const config = await loadRawIntegrationsConfig(studioDir);
|
|
212
|
-
console.log('');
|
|
213
|
-
for (const plugin of plugins) {
|
|
214
|
-
const pluginConfig = (config[plugin.name] ?? {}) as Record<string, unknown>;
|
|
215
|
-
const status = getIntegrationStatus(plugin, pluginConfig);
|
|
216
|
-
const dot = status === 'configured' ? chalk.green('●') : chalk.gray('○');
|
|
217
|
-
const statusLabel = status === 'configured'
|
|
218
|
-
? chalk.green('configured')
|
|
219
|
-
: chalk.gray('not configured');
|
|
220
|
-
const extras = formatExtras(plugin, pluginConfig);
|
|
221
|
-
const version = `v${plugin.version}`;
|
|
222
|
-
console.log(`${plugin.name.padEnd(12)} ${dot} ${statusLabel.padEnd(20)} ${extras.padEnd(20)} ${chalk.gray(version)}`);
|
|
223
|
-
}
|
|
224
|
-
console.log('');
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
case 'remove': {
|
|
228
|
-
const name = _args[0];
|
|
229
|
-
if (!name) {
|
|
230
|
-
console.error('Usage: studio integrations remove <name>');
|
|
231
|
-
process.exit(1);
|
|
232
|
-
}
|
|
233
|
-
const studioDir = await resolveStudioDir();
|
|
234
|
-
await removeIntegration(name, getIntegrationsDir(studioDir));
|
|
235
|
-
console.log(chalk.green(`✓ Integration '${name}' removed`));
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
case 'test': {
|
|
239
|
-
const name = _args[0];
|
|
240
|
-
if (!name) {
|
|
241
|
-
console.error('Usage: studio integrations test <name>');
|
|
242
|
-
process.exit(1);
|
|
243
|
-
}
|
|
244
|
-
const studioDir = await resolveStudioDir();
|
|
245
|
-
const intDir = getIntegrationsDir(studioDir);
|
|
246
|
-
const plugins = await loadProjectIntegrations(intDir);
|
|
247
|
-
const plugin = plugins.find(p => p.name === name);
|
|
248
|
-
|
|
249
|
-
if (!plugin) {
|
|
250
|
-
console.error(
|
|
251
|
-
`Error: '${name}' not installed. Run: studio integrations install @studio/integration-${name}`
|
|
252
|
-
);
|
|
253
|
-
process.exit(1);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (!plugin.test) {
|
|
257
|
-
console.error(`Error: Integration '${name}' has no test: configuration in its .integration.yaml`);
|
|
258
|
-
process.exit(1);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const intConfig = await loadRawIntegrationsConfig(studioDir);
|
|
262
|
-
const pluginConfig = intConfig[name] ?? {};
|
|
263
|
-
const required = plugin.config?.required ?? [];
|
|
264
|
-
const missing = required.filter(key => !pluginConfig[key] && !process.env[key]);
|
|
265
|
-
if (missing.length > 0) {
|
|
266
|
-
for (const key of missing) {
|
|
267
|
-
console.error(`Error: ${key} not set. Run: studio integrations set ${name}.${key} <value>`);
|
|
268
|
-
}
|
|
269
|
-
process.exit(1);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const spinner = ora(`Testing ${name} connection...`).start();
|
|
273
|
-
const result = await runIntegrationTest(plugin, pluginConfig as Record<string, unknown>);
|
|
274
|
-
|
|
275
|
-
if (result.success) {
|
|
276
|
-
spinner.succeed(chalk.green(`✓ ${name} connected`));
|
|
277
|
-
} else {
|
|
278
|
-
const detail = result.error ?? `HTTP ${result.statusCode}`;
|
|
279
|
-
spinner.fail(chalk.red(`✗ ${name} error — ${detail}`));
|
|
280
|
-
process.exit(1);
|
|
281
|
-
}
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
case 'set': {
|
|
285
|
-
const dotPath = _args[0];
|
|
286
|
-
const value = _args[1];
|
|
287
|
-
if (!dotPath || value === undefined) {
|
|
288
|
-
console.error('Usage: studio integrations set <name>.<key> <value>');
|
|
289
|
-
process.exit(1);
|
|
290
|
-
}
|
|
291
|
-
const dotIndex = dotPath.indexOf('.');
|
|
292
|
-
if (dotIndex === -1) {
|
|
293
|
-
console.error('Error: path must be <integration-name>.<key> (e.g. linear.autoTrigger)');
|
|
294
|
-
process.exit(1);
|
|
295
|
-
}
|
|
296
|
-
const integrationName = dotPath.slice(0, dotIndex);
|
|
297
|
-
const key = dotPath.slice(dotIndex + 1);
|
|
298
|
-
|
|
299
|
-
const studioDir = await resolveStudioDir();
|
|
300
|
-
const intDir = getIntegrationsDir(studioDir);
|
|
301
|
-
|
|
302
|
-
const pluginPath = join(intDir, `${integrationName}.integration.yaml`);
|
|
303
|
-
const isInstalled = await access(pluginPath).then(() => true).catch(() => false);
|
|
304
|
-
if (!isInstalled) {
|
|
305
|
-
console.error(
|
|
306
|
-
`Error: Integration '${integrationName}' not installed. ` +
|
|
307
|
-
`Run: studio integrations install @studio/integration-${integrationName}`
|
|
308
|
-
);
|
|
309
|
-
process.exit(1);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const configFile = join(studioDir, 'config.yaml');
|
|
313
|
-
const rawConfig = await loadRawFullConfig(configFile);
|
|
314
|
-
setConfigValue(rawConfig, `integrations.${integrationName}.${key}`, value);
|
|
315
|
-
await saveIntegrationConfig(configFile, rawConfig);
|
|
316
|
-
|
|
317
|
-
console.log(chalk.green(`✓ Set integrations.${integrationName}.${key} = ${value}`));
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
default:
|
|
321
|
-
console.error(`Unknown integrations action: ${action}. Available: install, list, remove, test, set`);
|
|
322
|
-
process.exit(1);
|
|
323
|
-
}
|
|
324
|
-
} catch (error) {
|
|
325
|
-
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
326
|
-
console.log('\nAborted.');
|
|
327
|
-
process.exit(0);
|
|
328
|
-
}
|
|
329
|
-
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
330
|
-
process.exit(1);
|
|
331
|
-
}
|
|
332
|
-
}
|
package/src/commands/list.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve, join } from 'node:path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { loadConfig } from '../config.js';
|
|
5
|
-
|
|
6
|
-
const RUNS_DIR = '.studio/runs';
|
|
7
|
-
|
|
8
|
-
interface ListOptions {
|
|
9
|
-
status?: string;
|
|
10
|
-
limit?: string;
|
|
11
|
-
json?: boolean;
|
|
12
|
-
project?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function listCommand(
|
|
16
|
-
resource: string,
|
|
17
|
-
options: ListOptions
|
|
18
|
-
): Promise<void> {
|
|
19
|
-
try {
|
|
20
|
-
const config = await loadConfig();
|
|
21
|
-
const configsDir = config.paths?.configs
|
|
22
|
-
? resolve(config.paths.configs)
|
|
23
|
-
: config.resolvedStudioDir
|
|
24
|
-
? resolve(config.resolvedStudioDir)
|
|
25
|
-
: resolve('./configs');
|
|
26
|
-
|
|
27
|
-
switch (resource) {
|
|
28
|
-
case 'projects':
|
|
29
|
-
await listProjects(configsDir, options.json);
|
|
30
|
-
break;
|
|
31
|
-
case 'pipelines':
|
|
32
|
-
await listPipelines(configsDir, options.json);
|
|
33
|
-
break;
|
|
34
|
-
case 'agents':
|
|
35
|
-
await listAgents(configsDir, options.json);
|
|
36
|
-
break;
|
|
37
|
-
case 'runs':
|
|
38
|
-
await listRuns(options);
|
|
39
|
-
break;
|
|
40
|
-
default:
|
|
41
|
-
console.error(
|
|
42
|
-
`Unknown resource: ${resource}. Available: projects, pipelines, agents, runs`
|
|
43
|
-
);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.error('Error:', error instanceof Error ? error.message : error);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function listProjects(configsDir: string, json?: boolean): Promise<void> {
|
|
53
|
-
// With the flat .studio/ structure, there is no projects/ layer.
|
|
54
|
-
if (json) {
|
|
55
|
-
console.log(JSON.stringify([configsDir], null, 2));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
console.log('\nThis workspace uses a flat .studio/ structure (no projects/ layer).');
|
|
59
|
-
console.log('Run `studio list pipelines` to see available pipelines.');
|
|
60
|
-
console.log('');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function listPipelines(configsDir: string, json?: boolean): Promise<void> {
|
|
64
|
-
const pipelinesDir = join(configsDir, 'pipelines');
|
|
65
|
-
const results = await getFileNames(pipelinesDir, '.pipeline.yaml');
|
|
66
|
-
|
|
67
|
-
if (json) {
|
|
68
|
-
console.log(JSON.stringify(results, null, 2));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (results.length === 0) {
|
|
73
|
-
console.log(chalk.yellow('No pipelines found'));
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
console.log('\nPipelines:');
|
|
78
|
-
for (const name of results) {
|
|
79
|
-
console.log(` - ${name}`);
|
|
80
|
-
}
|
|
81
|
-
console.log('');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function listAgents(configsDir: string, json?: boolean): Promise<void> {
|
|
85
|
-
const agentsDir = join(configsDir, 'agents');
|
|
86
|
-
const results = await getFileNames(agentsDir, '.agent.yaml');
|
|
87
|
-
|
|
88
|
-
if (json) {
|
|
89
|
-
console.log(JSON.stringify(results, null, 2));
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (results.length === 0) {
|
|
94
|
-
console.log(chalk.yellow('No agents found'));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
console.log('\nAgents:');
|
|
99
|
-
for (const name of results) {
|
|
100
|
-
console.log(` - ${name}`);
|
|
101
|
-
}
|
|
102
|
-
console.log('');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
interface RunListEntry {
|
|
106
|
-
date: string;
|
|
107
|
-
pipeline: string;
|
|
108
|
-
run_id: string;
|
|
109
|
-
status: string;
|
|
110
|
-
filename: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function listRuns(options: ListOptions): Promise<void> {
|
|
114
|
-
const runsPath = resolve(process.cwd(), RUNS_DIR);
|
|
115
|
-
let entries: import('node:fs').Dirent[];
|
|
116
|
-
try {
|
|
117
|
-
entries = await readdir(runsPath, { withFileTypes: true });
|
|
118
|
-
} catch {
|
|
119
|
-
if (options.json) {
|
|
120
|
-
console.log('[]');
|
|
121
|
-
} else {
|
|
122
|
-
console.log(chalk.yellow(`No runs directory at ${runsPath}. Run a pipeline first.`));
|
|
123
|
-
}
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const jsonlFiles = entries
|
|
128
|
-
.filter((e) => e.isFile() && e.name.endsWith('.jsonl'))
|
|
129
|
-
.map((e) => e.name);
|
|
130
|
-
|
|
131
|
-
const runs: RunListEntry[] = [];
|
|
132
|
-
for (const filename of jsonlFiles) {
|
|
133
|
-
const match = filename.match(/^(\d{4}-\d{2}-\d{2}T\d{1,2}h\d{1,2}m)-(.+)-([a-f0-9]{8})\.jsonl$/i);
|
|
134
|
-
if (!match) continue;
|
|
135
|
-
const datePart = match[1];
|
|
136
|
-
const pipeline = match[2];
|
|
137
|
-
const runId = match[3];
|
|
138
|
-
|
|
139
|
-
let status = 'running';
|
|
140
|
-
try {
|
|
141
|
-
const content = await readFile(resolve(runsPath, filename), 'utf-8');
|
|
142
|
-
const lines = content.split('\n').filter((l) => l.trim().length > 0);
|
|
143
|
-
const lastLine = lines[lines.length - 1];
|
|
144
|
-
if (lastLine) {
|
|
145
|
-
const record = JSON.parse(lastLine) as Record<string, unknown>;
|
|
146
|
-
if (record.event === 'pipeline_complete' && typeof record.status === 'string') {
|
|
147
|
-
status = record.status;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
} catch {
|
|
151
|
-
// keep default status
|
|
152
|
-
}
|
|
153
|
-
runs.push({
|
|
154
|
-
date: datePart ?? filename,
|
|
155
|
-
pipeline,
|
|
156
|
-
run_id: runId ?? '',
|
|
157
|
-
status,
|
|
158
|
-
filename,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
runs.sort((a, b) => (b.date > a.date ? 1 : -1));
|
|
163
|
-
const limit = options.limit ? parseInt(options.limit, 10) : 10;
|
|
164
|
-
const limited = runs.slice(0, isNaN(limit) ? 10 : limit);
|
|
165
|
-
|
|
166
|
-
if (options.json) {
|
|
167
|
-
console.log(JSON.stringify(limited, null, 2));
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (limited.length === 0) {
|
|
172
|
-
console.log(chalk.yellow('No runs found'));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const statusColor = (s: string) =>
|
|
177
|
-
s === 'success' ? chalk.green(s) : s === 'rejected' || s === 'failed' ? chalk.red(s) : chalk.gray(s);
|
|
178
|
-
|
|
179
|
-
console.log('\nRuns:');
|
|
180
|
-
for (const r of limited) {
|
|
181
|
-
console.log(` ${r.date} ${r.pipeline} ${r.run_id} ${statusColor(r.status)}`);
|
|
182
|
-
}
|
|
183
|
-
console.log('');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function getFileNames(dir: string, suffix: string): Promise<string[]> {
|
|
187
|
-
let files: string[];
|
|
188
|
-
try {
|
|
189
|
-
files = await readdir(dir);
|
|
190
|
-
} catch {
|
|
191
|
-
return [];
|
|
192
|
-
}
|
|
193
|
-
return files
|
|
194
|
-
.filter((f) => f.endsWith(suffix))
|
|
195
|
-
.map((f) => f.replace(suffix, ''))
|
|
196
|
-
.sort();
|
|
197
|
-
}
|
package/src/commands/logs.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
|
|
5
|
-
const RUNS_DIR = '.studio/runs';
|
|
6
|
-
|
|
7
|
-
interface LogsOptions {
|
|
8
|
-
raw?: boolean;
|
|
9
|
-
json?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function runIdShort(runId: string): string {
|
|
13
|
-
return runId.replace(/-/g, '').slice(0, 8);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function formatEventLine(record: Record<string, unknown>): string {
|
|
17
|
-
const event = record.event as string;
|
|
18
|
-
const ts = record.ts as string;
|
|
19
|
-
const time = ts ? new Date(ts).toISOString().slice(11, 19) : '';
|
|
20
|
-
|
|
21
|
-
switch (event) {
|
|
22
|
-
case 'pipeline_start':
|
|
23
|
-
return chalk.blue(`${time} Pipeline started: ${record.project}/${record.pipeline} (run ${record.run_id})`);
|
|
24
|
-
case 'pipeline_complete':
|
|
25
|
-
return chalk.blue(
|
|
26
|
-
`${time} Pipeline complete: ${record.status} (${record.duration_ms}ms, ${record.total_tokens ?? 0} tokens)`
|
|
27
|
-
);
|
|
28
|
-
case 'stage_start':
|
|
29
|
-
return chalk.gray(
|
|
30
|
-
` ${time} [${(record.stage_index as number) + 1}/${record.total_stages}] ${record.stage} ...`
|
|
31
|
-
);
|
|
32
|
-
case 'stage_complete': {
|
|
33
|
-
const status = record.status as string;
|
|
34
|
-
const icon = status === 'success' ? chalk.green('✓') : chalk.red('✗');
|
|
35
|
-
const attempts = record.attempts !== undefined ? ` (${record.attempts} attempt(s))` : '';
|
|
36
|
-
const duration = record.duration_ms !== undefined ? ` ${record.duration_ms}ms` : '';
|
|
37
|
-
return chalk.gray(` ${time} ${icon} ${record.stage} ${status}${attempts}${duration}`);
|
|
38
|
-
}
|
|
39
|
-
case 'stage_retry':
|
|
40
|
-
return chalk.yellow(
|
|
41
|
-
` ${time} ↻ Retry #${record.attempt}: ${record.failure_reason ?? 'unknown'}`
|
|
42
|
-
);
|
|
43
|
-
case 'group_start':
|
|
44
|
-
return chalk.gray(` ${time} Group ${record.group} (max ${record.max_iterations} iterations)`);
|
|
45
|
-
case 'group_iteration':
|
|
46
|
-
return chalk.yellow(` ${time} ↻ Iteration ${record.iteration}/${record.max_iterations}`);
|
|
47
|
-
case 'group_feedback':
|
|
48
|
-
return chalk.yellow(` ${time} Rejected: ${record.rejection_reason}`);
|
|
49
|
-
case 'group_complete':
|
|
50
|
-
return chalk.gray(` ${time} Group complete: ${record.status} (${record.iterations} iterations)`);
|
|
51
|
-
default:
|
|
52
|
-
return chalk.gray(`${time} ${event} ${JSON.stringify(record)}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function logsCommand(runId: string, options: LogsOptions): Promise<void> {
|
|
57
|
-
try {
|
|
58
|
-
const runsPath = resolve(process.cwd(), RUNS_DIR);
|
|
59
|
-
let entries: import('node:fs').Dirent[];
|
|
60
|
-
try {
|
|
61
|
-
entries = await readdir(runsPath, { withFileTypes: true });
|
|
62
|
-
} catch {
|
|
63
|
-
console.error(chalk.yellow(`No runs directory found at ${runsPath}`));
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const shortId = runIdShort(runId);
|
|
68
|
-
const jsonlFiles = entries
|
|
69
|
-
.filter((e) => e.isFile() && e.name.endsWith('.jsonl'))
|
|
70
|
-
.map((e) => e.name);
|
|
71
|
-
|
|
72
|
-
const matching = jsonlFiles.filter((name) => name.endsWith(`-${shortId}.jsonl`));
|
|
73
|
-
if (matching.length === 0) {
|
|
74
|
-
console.error(chalk.yellow(`No run log found for run id: ${runId}`));
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const mostRecent = matching.sort().reverse()[0];
|
|
79
|
-
const filePath = resolve(runsPath, mostRecent);
|
|
80
|
-
const content = await readFile(filePath, 'utf-8');
|
|
81
|
-
const lines = content
|
|
82
|
-
.split('\n')
|
|
83
|
-
.filter((line) => line.trim().length > 0)
|
|
84
|
-
.map((line) => {
|
|
85
|
-
try {
|
|
86
|
-
return JSON.parse(line) as Record<string, unknown>;
|
|
87
|
-
} catch {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
.filter((r): r is Record<string, unknown> => r !== null);
|
|
92
|
-
|
|
93
|
-
const forRun = lines.filter(
|
|
94
|
-
(r) => (r.run_id as string) === shortId || (r.run_id as string)?.startsWith(shortId)
|
|
95
|
-
);
|
|
96
|
-
const records = forRun.length > 0 ? forRun : lines;
|
|
97
|
-
|
|
98
|
-
if (options.raw) {
|
|
99
|
-
for (const r of records) {
|
|
100
|
-
console.log(JSON.stringify(r));
|
|
101
|
-
}
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (options.json) {
|
|
106
|
-
console.log(JSON.stringify(records, null, 2));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
console.log(chalk.bold(`\nRun ${shortId} — ${mostRecent}\n`));
|
|
111
|
-
for (const record of records) {
|
|
112
|
-
console.log(formatEventLine(record));
|
|
113
|
-
}
|
|
114
|
-
console.log('');
|
|
115
|
-
} catch (error) {
|
|
116
|
-
console.error('Error:', error instanceof Error ? error.message : error);
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
}
|