@studio-foundation/cli 0.3.0-beta.1 → 0.3.0-beta.6
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 +9 -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/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/config.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve, join } from 'node:path';
|
|
3
|
-
import yaml from 'js-yaml';
|
|
4
|
-
import { findStudioDir } from './studio-dir.js';
|
|
5
|
-
|
|
6
|
-
export interface StudioConfig {
|
|
7
|
-
providers?: {
|
|
8
|
-
openai?: { apiKey: string };
|
|
9
|
-
anthropic?: { apiKey: string };
|
|
10
|
-
ollama?: { baseUrl?: string };
|
|
11
|
-
};
|
|
12
|
-
paths?: {
|
|
13
|
-
configs?: string;
|
|
14
|
-
projects_dir?: string;
|
|
15
|
-
pipelines?: string;
|
|
16
|
-
};
|
|
17
|
-
defaults?: {
|
|
18
|
-
provider?: string;
|
|
19
|
-
model?: string;
|
|
20
|
-
};
|
|
21
|
-
api?: {
|
|
22
|
-
key?: string;
|
|
23
|
-
port?: number;
|
|
24
|
-
};
|
|
25
|
-
integrations?: Record<string, Record<string, unknown>>;
|
|
26
|
-
db?: {
|
|
27
|
-
type?: 'sqlite' | 'postgres' | 'inmemory';
|
|
28
|
-
url?: string; // required when type is 'postgres'
|
|
29
|
-
};
|
|
30
|
-
/** Resolved path to .studio/ dir — set at load time, not from YAML */
|
|
31
|
-
resolvedStudioDir?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const LEGACY_CONFIG_NAMES = ['.studiorc.yaml', '.studiorc.yml'];
|
|
35
|
-
|
|
36
|
-
export async function loadConfig(configPath?: string, cwd?: string): Promise<StudioConfig> {
|
|
37
|
-
const effectiveCwd = cwd ?? process.cwd();
|
|
38
|
-
|
|
39
|
-
if (configPath) {
|
|
40
|
-
return loadFromFile(resolve(configPath));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// 1. Try .studio/config.yaml (new standard)
|
|
44
|
-
const studioDir = await findStudioDir(effectiveCwd);
|
|
45
|
-
if (studioDir) {
|
|
46
|
-
const studioConfig = join(studioDir, 'config.yaml');
|
|
47
|
-
try {
|
|
48
|
-
const config = await loadFromFile(studioConfig);
|
|
49
|
-
config.resolvedStudioDir = studioDir;
|
|
50
|
-
return config;
|
|
51
|
-
} catch {
|
|
52
|
-
// .studio/ exists but no config.yaml — still set studioDir for path resolution
|
|
53
|
-
return { resolvedStudioDir: studioDir };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 2. Fallback: .studiorc.yaml / .studiorc.yml at cwd
|
|
58
|
-
for (const name of LEGACY_CONFIG_NAMES) {
|
|
59
|
-
const filePath = resolve(effectiveCwd, name);
|
|
60
|
-
try {
|
|
61
|
-
return await loadFromFile(filePath);
|
|
62
|
-
} catch {
|
|
63
|
-
// try next
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function loadFromFile(filePath: string): Promise<StudioConfig> {
|
|
71
|
-
let raw: string;
|
|
72
|
-
try {
|
|
73
|
-
raw = await readFile(filePath, 'utf-8');
|
|
74
|
-
} catch {
|
|
75
|
-
throw new Error(`Config file not found: ${filePath}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const resolved = resolveEnvVars(raw);
|
|
79
|
-
|
|
80
|
-
let parsed: unknown;
|
|
81
|
-
try {
|
|
82
|
-
parsed = yaml.load(resolved);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
`Failed to parse config ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
90
|
-
return {};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return parsed as StudioConfig;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function resolveEnvVars(content: string): string {
|
|
97
|
-
return content.replace(/\$\{([^}]+)\}/g, (_match, varName: string) => {
|
|
98
|
-
const value = process.env[varName.trim()];
|
|
99
|
-
return value === undefined ? '' : value;
|
|
100
|
-
});
|
|
101
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import 'dotenv/config';
|
|
4
|
-
import { Command } from 'commander';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import { runCommand } from './commands/run.js';
|
|
7
|
-
import { statusCommand } from './commands/status.js';
|
|
8
|
-
import { listCommand } from './commands/list.js';
|
|
9
|
-
import { logsCommand } from './commands/logs.js';
|
|
10
|
-
import { replayCommand, restartCommand } from './commands/replay.js';
|
|
11
|
-
import { validateCommand } from './commands/validate.js';
|
|
12
|
-
import { initCommand } from './commands/init.js';
|
|
13
|
-
import { configCommand } from './commands/config.js';
|
|
14
|
-
import { toolsCommand } from './commands/tools.js';
|
|
15
|
-
import { integrationsCommand } from './commands/integrations.js';
|
|
16
|
-
import { templatesCommand } from './commands/templates.js';
|
|
17
|
-
import { templateCommand } from './commands/template/index.js';
|
|
18
|
-
import { projectCommand } from './commands/project.js';
|
|
19
|
-
import { apiStartCommand, apiStopCommand, apiStatusCommand } from './commands/api.js';
|
|
20
|
-
import { installExtensionCommand } from './commands/install.js';
|
|
21
|
-
import { createRegistryCommand } from './commands/registry/index.js';
|
|
22
|
-
import { usersCommand } from './commands/users.js';
|
|
23
|
-
import { ollamaCommand } from './commands/ollama.js';
|
|
24
|
-
import { loadConfig } from './config.js';
|
|
25
|
-
|
|
26
|
-
const program = new Command();
|
|
27
|
-
|
|
28
|
-
program
|
|
29
|
-
.name('studio')
|
|
30
|
-
.description('Studio v7 — Agentic pipeline orchestrator')
|
|
31
|
-
.version('0.3.0');
|
|
32
|
-
|
|
33
|
-
program
|
|
34
|
-
.command('run <project/pipeline>')
|
|
35
|
-
.description('Run a pipeline (e.g. studio run cuisine/recipe-generator)')
|
|
36
|
-
.option('-i, --input <text>', 'Input description for the pipeline')
|
|
37
|
-
.option('-f, --input-file <path>', 'Path to YAML input file')
|
|
38
|
-
.option('-r, --repo <path>', 'Path to the target repository')
|
|
39
|
-
.option('--repo-url <url>', 'Git URL to clone as target repository')
|
|
40
|
-
.option('--config <path>', 'Path to .studiorc.yaml config file')
|
|
41
|
-
.option('--provider <name>', 'Override LLM provider for all stages (e.g. mock)')
|
|
42
|
-
.option('--json', 'Output results as JSON')
|
|
43
|
-
.option('--verbose', 'Show detailed execution logs')
|
|
44
|
-
.option('--live', 'Show live per-tool-call spinners during execution')
|
|
45
|
-
.option('--anonymize', 'Anonymize PII in inputs and outputs before sending to LLM')
|
|
46
|
-
.action(runCommand);
|
|
47
|
-
|
|
48
|
-
program
|
|
49
|
-
.command('status [run-id]')
|
|
50
|
-
.description('Show status of a pipeline run')
|
|
51
|
-
.option('--json', 'Output as JSON')
|
|
52
|
-
.action(statusCommand);
|
|
53
|
-
|
|
54
|
-
program
|
|
55
|
-
.command('logs <run-id>')
|
|
56
|
-
.description('Show log for a pipeline run (from .studio/runs/*.jsonl)')
|
|
57
|
-
.option('--raw', 'Output raw JSONL lines')
|
|
58
|
-
.option('--json', 'Output as pretty-printed JSON array')
|
|
59
|
-
.action(logsCommand);
|
|
60
|
-
|
|
61
|
-
program
|
|
62
|
-
.command('replay <run-id>')
|
|
63
|
-
.description('Replay a past pipeline run from JSONL logs, or re-execute from a specific stage')
|
|
64
|
-
.option('--verbose', 'Show complete outputs and tool call results')
|
|
65
|
-
.option('--restart', 'Re-execute pipeline from a specific stage (requires --stage)')
|
|
66
|
-
.option('--stage <index|name>', 'Stage index (0-based) or name to restart from (use with --restart)')
|
|
67
|
-
.option('--provider <name>', 'Override LLM provider (e.g. mock) — applies to resumed stages only')
|
|
68
|
-
.action((runId: string, options: { verbose?: boolean; restart?: boolean; stage?: string; provider?: string }) => {
|
|
69
|
-
if (options.restart) {
|
|
70
|
-
if (!options.stage) {
|
|
71
|
-
console.error(chalk.red('Error: --restart requires --stage <index|name>'));
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
return restartCommand(runId, { stage: options.stage, verbose: options.verbose, provider: options.provider });
|
|
75
|
-
}
|
|
76
|
-
return replayCommand(runId, options);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
program
|
|
80
|
-
.command('list <resource>')
|
|
81
|
-
.description('List projects, pipelines, agents, or runs')
|
|
82
|
-
.option('--status <status>', 'Filter runs by status')
|
|
83
|
-
.option('--limit <n>', 'Limit number of results', '10')
|
|
84
|
-
.option('--project <name>', 'Filter runs by project')
|
|
85
|
-
.option('--json', 'Output as JSON')
|
|
86
|
-
.action(listCommand);
|
|
87
|
-
|
|
88
|
-
program
|
|
89
|
-
.command('validate <contract> <output>')
|
|
90
|
-
.description('Validate an output against a contract')
|
|
91
|
-
.action(validateCommand);
|
|
92
|
-
|
|
93
|
-
program
|
|
94
|
-
.command('init [name]')
|
|
95
|
-
.description('Initialize a new Studio project in the current directory')
|
|
96
|
-
.option('--template <name>', 'Project template to use (e.g. software)')
|
|
97
|
-
.option('--project <name>', 'Project name (defaults to directory name or "default")')
|
|
98
|
-
.option('--provider <name>', 'LLM provider (anthropic, openai) — enables direct mode')
|
|
99
|
-
.option('--api-key <key>', 'API key for the provider')
|
|
100
|
-
.option('--force', 'Backup existing .studio/ and reinitialize')
|
|
101
|
-
.option('--yes', 'Skip confirmation prompts (for CI/CD)')
|
|
102
|
-
.option('--no-tools', 'Skip tool installation (direct mode only)')
|
|
103
|
-
.action(initCommand);
|
|
104
|
-
|
|
105
|
-
program
|
|
106
|
-
.command('config <action> [args...]')
|
|
107
|
-
.description('Manage Studio configuration (list, get, set, add-provider)')
|
|
108
|
-
.option('--api-key <key>', 'API key (used with: config set provider <name> --api-key <key>; config add-provider <name> --api-key <key>)')
|
|
109
|
-
.option('--set-default', 'Set as default provider (used with: config add-provider)')
|
|
110
|
-
.action(configCommand);
|
|
111
|
-
|
|
112
|
-
program
|
|
113
|
-
.command('tools <action> [args...]')
|
|
114
|
-
.description('Manage Studio tools (list, add, remove, info)')
|
|
115
|
-
.action(toolsCommand);
|
|
116
|
-
|
|
117
|
-
program
|
|
118
|
-
.command('integrations <action> [args...]')
|
|
119
|
-
.description('Manage Studio integrations (install, list, remove, test, set)')
|
|
120
|
-
.action((action: string, args: string[]) => {
|
|
121
|
-
void integrationsCommand(action, args, {});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
program
|
|
125
|
-
.command('templates <action> [args...]')
|
|
126
|
-
.description('Manage Studio templates (list)')
|
|
127
|
-
.action(templatesCommand);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
program
|
|
131
|
-
.command('template <action> [args...]')
|
|
132
|
-
.description('Template operations (validate)')
|
|
133
|
-
.action(templateCommand);
|
|
134
|
-
|
|
135
|
-
program
|
|
136
|
-
.command('project <action> [args...]')
|
|
137
|
-
.description('Manage Studio projects (add)')
|
|
138
|
-
.option('--template <name>', 'Template to use (blank, software, …)')
|
|
139
|
-
.option('--description <desc>', 'Project description')
|
|
140
|
-
.action(projectCommand);
|
|
141
|
-
|
|
142
|
-
program
|
|
143
|
-
.command('api <action>')
|
|
144
|
-
.description('Manage the Studio API server (start, stop, status)')
|
|
145
|
-
.option('--port <port>', 'Port to listen on (default: 3700)')
|
|
146
|
-
.option('--config <path>', 'Path to config file')
|
|
147
|
-
.action((action: string, options: { port?: string; config?: string }) => {
|
|
148
|
-
if (action === 'start') {
|
|
149
|
-
void apiStartCommand(options);
|
|
150
|
-
} else if (action === 'stop') {
|
|
151
|
-
void apiStopCommand();
|
|
152
|
-
} else if (action === 'status') {
|
|
153
|
-
void apiStatusCommand(options);
|
|
154
|
-
} else {
|
|
155
|
-
console.error(`Unknown api action: ${action}. Use: studio api start|stop|status`);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
program
|
|
161
|
-
.command('install <extension>')
|
|
162
|
-
.description('Install a Studio extension (api)')
|
|
163
|
-
.action((extension: string) => {
|
|
164
|
-
void installExtensionCommand(extension);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
program.addCommand(createRegistryCommand());
|
|
168
|
-
|
|
169
|
-
const usersCmd = program.command('users').description('Manage users');
|
|
170
|
-
|
|
171
|
-
usersCmd
|
|
172
|
-
.command('list')
|
|
173
|
-
.description('List all users')
|
|
174
|
-
.action(() => { void usersCommand('list', [], {}); });
|
|
175
|
-
|
|
176
|
-
usersCmd
|
|
177
|
-
.command('add <email>')
|
|
178
|
-
.description('Create a new user')
|
|
179
|
-
.option('--plan <plan>', 'User plan (free|pro|unlimited)', 'free')
|
|
180
|
-
.action((email: string, opts: { plan?: string }) => { void usersCommand('add', [email], opts); });
|
|
181
|
-
|
|
182
|
-
usersCmd
|
|
183
|
-
.command('remove <email>')
|
|
184
|
-
.description('Remove a user')
|
|
185
|
-
.action((email: string) => { void usersCommand('remove', [email], {}); });
|
|
186
|
-
|
|
187
|
-
usersCmd
|
|
188
|
-
.command('info <email>')
|
|
189
|
-
.description("Show user details and today's usage")
|
|
190
|
-
.action((email: string) => { void usersCommand('info', [email], {}); });
|
|
191
|
-
|
|
192
|
-
program
|
|
193
|
-
.command('ollama <action> [model]')
|
|
194
|
-
.description('Manage local Ollama instance (start, stop, status, pull <model>)')
|
|
195
|
-
.action(async (action: string, model: string | undefined) => {
|
|
196
|
-
const config = await loadConfig();
|
|
197
|
-
const baseUrl = config.providers?.ollama?.baseUrl ?? 'http://localhost:11434';
|
|
198
|
-
void ollamaCommand(action, model, baseUrl);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
program.parse();
|
package/src/models-cache.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
2
|
-
|
|
3
|
-
interface RawModel {
|
|
4
|
-
id: string;
|
|
5
|
-
created?: number;
|
|
6
|
-
created_at?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface CacheEntry {
|
|
10
|
-
models: string[];
|
|
11
|
-
fetchedAt: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const cache = new Map<string, CacheEntry>();
|
|
15
|
-
|
|
16
|
-
function cacheKey(provider: string, apiKey: string): string {
|
|
17
|
-
return `${provider}:${apiKey.slice(0, 8)}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function setCachedModels(provider: string, apiKey: string, models: string[]): void {
|
|
21
|
-
cache.set(cacheKey(provider, apiKey), { models, fetchedAt: Date.now() });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getCachedModels(provider: string, apiKey: string): string[] | null {
|
|
25
|
-
const key = cacheKey(provider, apiKey);
|
|
26
|
-
const entry = cache.get(key);
|
|
27
|
-
if (!entry) return null;
|
|
28
|
-
if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
|
|
29
|
-
cache.delete(key);
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
return entry.models;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function filterModels(provider: string, models: RawModel[]): string[] {
|
|
36
|
-
if (provider === 'anthropic') {
|
|
37
|
-
return models
|
|
38
|
-
.filter(
|
|
39
|
-
(m) =>
|
|
40
|
-
m.id.startsWith('claude-') &&
|
|
41
|
-
!m.id.startsWith('claude-instant') &&
|
|
42
|
-
!m.id.startsWith('claude-2')
|
|
43
|
-
)
|
|
44
|
-
.map((m) => m.id);
|
|
45
|
-
}
|
|
46
|
-
if (provider === 'openai') {
|
|
47
|
-
return models
|
|
48
|
-
.filter(
|
|
49
|
-
(m) =>
|
|
50
|
-
m.id.startsWith('gpt-4') ||
|
|
51
|
-
m.id.startsWith('o1') ||
|
|
52
|
-
m.id.startsWith('o3')
|
|
53
|
-
)
|
|
54
|
-
.map((m) => m.id);
|
|
55
|
-
}
|
|
56
|
-
return models.map((m) => m.id);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function sortModels(modelIds: string[], rawModels: RawModel[]): string[] {
|
|
60
|
-
const tsMap = new Map<string, number>();
|
|
61
|
-
for (const m of rawModels) {
|
|
62
|
-
if (m.created !== undefined) {
|
|
63
|
-
tsMap.set(m.id, m.created);
|
|
64
|
-
} else if (m.created_at) {
|
|
65
|
-
tsMap.set(m.id, new Date(m.created_at).getTime());
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return [...modelIds].sort((a, b) => {
|
|
69
|
-
const ta = tsMap.get(a) ?? 0;
|
|
70
|
-
const tb = tsMap.get(b) ?? 0;
|
|
71
|
-
if (ta !== tb) return tb - ta;
|
|
72
|
-
return b.localeCompare(a);
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function parseAndCacheModels(provider: string, apiKey: string, json: unknown): void {
|
|
77
|
-
try {
|
|
78
|
-
const data = ((json as Record<string, unknown>).data ?? []) as RawModel[];
|
|
79
|
-
const filtered = filterModels(provider, data);
|
|
80
|
-
const sorted = sortModels(filtered, data);
|
|
81
|
-
setCachedModels(provider, apiKey, sorted);
|
|
82
|
-
} catch {
|
|
83
|
-
// ignore malformed responses
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const SUPPORTED_PROVIDERS = new Set(['anthropic', 'openai']);
|
|
88
|
-
|
|
89
|
-
async function fetchModelsFromProvider(provider: string, apiKey: string): Promise<string[]> {
|
|
90
|
-
if (!SUPPORTED_PROVIDERS.has(provider)) return [];
|
|
91
|
-
|
|
92
|
-
const abort = new AbortController();
|
|
93
|
-
const timer = setTimeout(() => abort.abort(), 5000);
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
let url: string;
|
|
97
|
-
let headers: Record<string, string>;
|
|
98
|
-
|
|
99
|
-
if (provider === 'anthropic') {
|
|
100
|
-
url = 'https://api.anthropic.com/v1/models';
|
|
101
|
-
headers = { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' };
|
|
102
|
-
} else {
|
|
103
|
-
url = 'https://api.openai.com/v1/models';
|
|
104
|
-
headers = { Authorization: `Bearer ${apiKey}` };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const res = await fetch(url, { signal: abort.signal, headers });
|
|
108
|
-
if (!res.ok) return [];
|
|
109
|
-
|
|
110
|
-
const json = await res.json();
|
|
111
|
-
const data = ((json as Record<string, unknown>).data ?? []) as RawModel[];
|
|
112
|
-
const filtered = filterModels(provider, data);
|
|
113
|
-
const sorted = sortModels(filtered, data);
|
|
114
|
-
setCachedModels(provider, apiKey, sorted);
|
|
115
|
-
return sorted;
|
|
116
|
-
} catch {
|
|
117
|
-
return [];
|
|
118
|
-
} finally {
|
|
119
|
-
clearTimeout(timer);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export async function getAvailableModels(provider: string, apiKey: string): Promise<string[]> {
|
|
124
|
-
const cached = getCachedModels(provider, apiKey);
|
|
125
|
-
if (cached !== null) return cached;
|
|
126
|
-
return fetchModelsFromProvider(provider, apiKey);
|
|
127
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import type { ToolCallCompleteEvent } from '@studio-foundation/contracts';
|
|
5
|
-
|
|
6
|
-
export interface FileChange {
|
|
7
|
-
path: string;
|
|
8
|
-
status: 'M' | 'A';
|
|
9
|
-
added: number;
|
|
10
|
-
removed: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class FileChangeCollector {
|
|
14
|
-
private paths = new Set<string>();
|
|
15
|
-
|
|
16
|
-
onToolCallComplete(event: ToolCallCompleteEvent): void {
|
|
17
|
-
if (event.tool !== 'repo_manager-write_file') return;
|
|
18
|
-
if (event.error) return;
|
|
19
|
-
|
|
20
|
-
const result = event.result as { path?: string } | undefined;
|
|
21
|
-
if (result?.path) {
|
|
22
|
-
this.paths.add(result.path);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
getWrittenPaths(): string[] {
|
|
27
|
-
return [...this.paths];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
computeSummary(repoPath: string): FileChange[] | null {
|
|
31
|
-
const written = this.getWrittenPaths();
|
|
32
|
-
if (written.length === 0) return null;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const diffOutput = execSync(
|
|
36
|
-
`git diff --numstat -- ${written.map((p) => JSON.stringify(p)).join(' ')}`,
|
|
37
|
-
{ cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
38
|
-
).trim();
|
|
39
|
-
|
|
40
|
-
const diffMap = new Map<string, { added: number; removed: number }>();
|
|
41
|
-
if (diffOutput) {
|
|
42
|
-
for (const line of diffOutput.split('\n')) {
|
|
43
|
-
const [addedStr, removedStr, ...pathParts] = line.split('\t');
|
|
44
|
-
const filePath = pathParts.join('\t');
|
|
45
|
-
diffMap.set(filePath, {
|
|
46
|
-
added: parseInt(addedStr, 10) || 0,
|
|
47
|
-
removed: parseInt(removedStr, 10) || 0,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const changes: FileChange[] = [];
|
|
53
|
-
for (const filePath of written) {
|
|
54
|
-
const diff = diffMap.get(filePath);
|
|
55
|
-
if (diff) {
|
|
56
|
-
changes.push({ path: filePath, status: 'M', added: diff.added, removed: diff.removed });
|
|
57
|
-
} else {
|
|
58
|
-
let lineCount = 0;
|
|
59
|
-
try {
|
|
60
|
-
const wcOutput = execSync(
|
|
61
|
-
`wc -l < ${JSON.stringify(join(repoPath, filePath))}`,
|
|
62
|
-
{ cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
63
|
-
).trim();
|
|
64
|
-
lineCount = parseInt(wcOutput, 10) || 0;
|
|
65
|
-
} catch {
|
|
66
|
-
// File might have been deleted or moved — skip line count
|
|
67
|
-
}
|
|
68
|
-
changes.push({ path: filePath, status: 'A', added: lineCount, removed: 0 });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return changes;
|
|
73
|
-
} catch {
|
|
74
|
-
// git not available or not a git repo — graceful degradation
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function formatFileChanges(changes: FileChange[]): string {
|
|
81
|
-
if (changes.length === 0) return '';
|
|
82
|
-
|
|
83
|
-
const lines: string[] = ['', 'Changes:'];
|
|
84
|
-
for (const change of changes) {
|
|
85
|
-
const tag = change.status === 'A'
|
|
86
|
-
? chalk.green('A')
|
|
87
|
-
: chalk.yellow('M');
|
|
88
|
-
|
|
89
|
-
const detail = change.status === 'A'
|
|
90
|
-
? chalk.gray(`(new file, ${change.added} lines)`)
|
|
91
|
-
: chalk.gray(`(+${change.added} -${change.removed})`);
|
|
92
|
-
|
|
93
|
-
lines.push(` ${tag} ${change.path} ${detail}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return lines.join('\n');
|
|
97
|
-
}
|
package/src/output/formatter.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import type { PipelineRun } from '@studio-foundation/contracts';
|
|
3
|
-
|
|
4
|
-
export function formatResult(run: PipelineRun): void {
|
|
5
|
-
console.log('');
|
|
6
|
-
console.log(`Pipeline: ${chalk.bold(run.pipeline_name)}`);
|
|
7
|
-
|
|
8
|
-
if (run.status === 'success') {
|
|
9
|
-
console.log(`Status: ${chalk.green('✓ success')}`);
|
|
10
|
-
} else if (run.status === 'rejected') {
|
|
11
|
-
console.log(`Status: ${chalk.red('✗ rejected by QA')}`);
|
|
12
|
-
} else {
|
|
13
|
-
console.log(`Status: ${chalk.red('✗ failed')}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (run.started_at && run.completed_at) {
|
|
17
|
-
const duration = formatDuration(
|
|
18
|
-
new Date(run.completed_at).getTime() - new Date(run.started_at).getTime()
|
|
19
|
-
);
|
|
20
|
-
console.log(`Duration: ${duration}`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (run.stages.length > 0) {
|
|
24
|
-
console.log('');
|
|
25
|
-
console.log('Stages:');
|
|
26
|
-
const total = run.stages.length;
|
|
27
|
-
|
|
28
|
-
for (let i = 0; i < run.stages.length; i++) {
|
|
29
|
-
const stage = run.stages[i];
|
|
30
|
-
const index = `[${i + 1}/${total}]`;
|
|
31
|
-
const name = stage.stage_name;
|
|
32
|
-
const attempts = stage.tasks[0]?.agent_runs.length ?? 0;
|
|
33
|
-
|
|
34
|
-
const dots = '.'.repeat(Math.max(2, 30 - name.length));
|
|
35
|
-
|
|
36
|
-
if (stage.status === 'success') {
|
|
37
|
-
console.log(
|
|
38
|
-
` ${index} ${name} ${chalk.gray(dots)} ${chalk.green('✓')} (${attempts} attempt${attempts !== 1 ? 's' : ''})`
|
|
39
|
-
);
|
|
40
|
-
} else if (stage.status === 'rejected') {
|
|
41
|
-
console.log(
|
|
42
|
-
` ${index} ${name} ${chalk.gray(dots)} ${chalk.red('✗ REJECTED')}`
|
|
43
|
-
);
|
|
44
|
-
} else if (stage.status === 'failed') {
|
|
45
|
-
console.log(
|
|
46
|
-
` ${index} ${name} ${chalk.gray(dots)} ${chalk.red('✗ FAILED')} (${attempts} attempts exhausted)`
|
|
47
|
-
);
|
|
48
|
-
// Show errors from the last agent run
|
|
49
|
-
const lastAgentRun = stage.tasks[0]?.agent_runs.at(-1);
|
|
50
|
-
if (lastAgentRun?.error) {
|
|
51
|
-
console.log(`${chalk.gray(' Errors:')}`);
|
|
52
|
-
console.log(`${chalk.gray(' -')} ${lastAgentRun.error}`);
|
|
53
|
-
}
|
|
54
|
-
} else if (stage.status === 'skipped') {
|
|
55
|
-
const reasonText = stage.skipped_reason ? ` (skipped: ${stage.skipped_reason})` : '';
|
|
56
|
-
console.log(
|
|
57
|
-
` ${index} ${name} ${chalk.gray(dots)} ${chalk.dim('⊘ skipped')}${chalk.gray(reasonText)}`
|
|
58
|
-
);
|
|
59
|
-
} else {
|
|
60
|
-
console.log(
|
|
61
|
-
` ${index} ${name} ${chalk.gray(dots)} ${chalk.yellow(stage.status)}`
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
console.log('');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function formatJson(data: unknown): void {
|
|
71
|
-
console.log(JSON.stringify(data, null, 2));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function formatError(error: Error): void {
|
|
75
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function formatDuration(ms: number): string {
|
|
79
|
-
if (ms < 1000) return `${ms}ms`;
|
|
80
|
-
const seconds = Math.floor(ms / 1000);
|
|
81
|
-
if (seconds < 60) return `${seconds}s`;
|
|
82
|
-
const minutes = Math.floor(seconds / 60);
|
|
83
|
-
const remainingSeconds = seconds % 60;
|
|
84
|
-
return `${minutes}m${remainingSeconds.toString().padStart(2, '0')}s`;
|
|
85
|
-
}
|