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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/package.json +8 -5
  2. package/ARCHITECTURE.md +0 -40
  3. package/src/commands/api.ts +0 -126
  4. package/src/commands/config.ts +0 -433
  5. package/src/commands/init.ts +0 -879
  6. package/src/commands/install.ts +0 -23
  7. package/src/commands/integrations.ts +0 -332
  8. package/src/commands/list.ts +0 -197
  9. package/src/commands/logs.ts +0 -119
  10. package/src/commands/ollama.ts +0 -168
  11. package/src/commands/project.ts +0 -167
  12. package/src/commands/registry/audit.ts +0 -87
  13. package/src/commands/registry/index.ts +0 -63
  14. package/src/commands/registry/install.ts +0 -222
  15. package/src/commands/registry/publish.ts +0 -112
  16. package/src/commands/registry/remove.ts +0 -89
  17. package/src/commands/registry/search.ts +0 -93
  18. package/src/commands/registry/sync.ts +0 -24
  19. package/src/commands/registry/update.ts +0 -63
  20. package/src/commands/replay.ts +0 -559
  21. package/src/commands/run.ts +0 -454
  22. package/src/commands/status.ts +0 -163
  23. package/src/commands/template/index.ts +0 -59
  24. package/src/commands/template/validate.ts +0 -175
  25. package/src/commands/templates.ts +0 -99
  26. package/src/commands/tools.ts +0 -227
  27. package/src/commands/users.ts +0 -127
  28. package/src/commands/validate.ts +0 -46
  29. package/src/config.ts +0 -101
  30. package/src/index.ts +0 -201
  31. package/src/models-cache.ts +0 -127
  32. package/src/output/file-changes.ts +0 -97
  33. package/src/output/formatter.ts +0 -85
  34. package/src/output/formatters.ts +0 -303
  35. package/src/output/logger.ts +0 -18
  36. package/src/output/parallel-progress.ts +0 -103
  37. package/src/output/progress.ts +0 -414
  38. package/src/provider-validator.ts +0 -109
  39. package/src/registry/cache.ts +0 -46
  40. package/src/registry/client.ts +0 -84
  41. package/src/registry/lockfile.ts +0 -68
  42. package/src/registry/resolver.ts +0 -113
  43. package/src/registry/types.ts +0 -66
  44. package/src/run-logger.ts +0 -66
  45. package/src/run-store-factory.ts +0 -30
  46. package/src/studio-dir.ts +0 -28
  47. package/src/utils/input-wizard.ts +0 -73
  48. package/src/utils/placeholders.ts +0 -10
  49. package/templates/.studiorc.yaml +0 -16
  50. package/templates/projects/blank/metadata.json +0 -7
  51. package/templates/studio-config.yaml +0 -14
  52. package/tests/__stubs__/studio-runner.ts +0 -104
  53. package/tests/commands/api.test.ts +0 -110
  54. package/tests/commands/config.test.ts +0 -221
  55. package/tests/commands/init.test.ts +0 -919
  56. package/tests/commands/install.test.ts +0 -52
  57. package/tests/commands/integrations.test.ts +0 -158
  58. package/tests/commands/ollama.test.ts +0 -139
  59. package/tests/commands/project.test.ts +0 -179
  60. package/tests/commands/registry/audit.test.ts +0 -56
  61. package/tests/commands/registry/install.test.ts +0 -200
  62. package/tests/commands/registry/publish.test.ts +0 -56
  63. package/tests/commands/registry/remove.test.ts +0 -103
  64. package/tests/commands/registry/search.test.ts +0 -44
  65. package/tests/commands/registry/sync.test.ts +0 -37
  66. package/tests/commands/registry/update.test.ts +0 -62
  67. package/tests/commands/replay.test.ts +0 -283
  68. package/tests/commands/template/validate.test.ts +0 -150
  69. package/tests/commands/templates.test.ts +0 -42
  70. package/tests/commands/tools.test.ts +0 -106
  71. package/tests/config.test.ts +0 -142
  72. package/tests/formatter.test.ts +0 -158
  73. package/tests/integration/sigint.test.ts +0 -188
  74. package/tests/models-cache.test.ts +0 -250
  75. package/tests/output/file-changes.test.ts +0 -178
  76. package/tests/output/formatters.test.ts +0 -448
  77. package/tests/output/progress-spinner.test.ts +0 -232
  78. package/tests/output/progress-timer.test.ts +0 -230
  79. package/tests/provider-validator.test.ts +0 -182
  80. package/tests/registry/cache.test.ts +0 -66
  81. package/tests/registry/client.test.ts +0 -70
  82. package/tests/registry/lockfile.test.ts +0 -87
  83. package/tests/registry/resolver.test.ts +0 -122
  84. package/tests/run-logger-events.test.ts +0 -326
  85. package/tests/run-logger.test.ts +0 -64
  86. package/tests/run-store-factory.test.ts +0 -51
  87. package/tests/studio-dir.test.ts +0 -31
  88. package/tests/utils/input-wizard.test.ts +0 -153
  89. package/tests/utils/placeholders.test.ts +0 -36
  90. package/tsconfig.json +0 -24
  91. package/vitest.config.ts +0 -20
@@ -1,168 +0,0 @@
1
- import { spawnSync } from 'node:child_process';
2
- import chalk from 'chalk';
3
-
4
- const OLLAMA_DOCKER_IMAGE = 'ollama/ollama';
5
-
6
- function formatBytes(bytes: number): string {
7
- const gb = bytes / (1024 ** 3);
8
- return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(bytes / (1024 ** 2)).toFixed(0)} MB`;
9
- }
10
-
11
- async function isOllamaRunning(baseUrl: string): Promise<false | { models: Array<{ name: string; size: number }> }> {
12
- try {
13
- const res = await fetch(`${baseUrl}/api/tags`);
14
- if (!res.ok) return false;
15
- const data = await res.json() as { models?: Array<{ name: string; size: number }> };
16
- return { models: data.models ?? [] };
17
- } catch {
18
- return false;
19
- }
20
- }
21
-
22
- export async function ollamaStatusCommand(baseUrl: string): Promise<void> {
23
- const result = await isOllamaRunning(baseUrl);
24
- if (!result) {
25
- console.log(chalk.red(' ✗ Ollama not running'));
26
- console.log('');
27
- console.log('To start Ollama:');
28
- console.log(` ${chalk.cyan('ollama serve')} # native`);
29
- console.log(` ${chalk.cyan(`docker run -d -p 11434:11434 ${OLLAMA_DOCKER_IMAGE}`)} # Docker`);
30
- return;
31
- }
32
- console.log(chalk.green(` ✓ Ollama running at ${baseUrl}`));
33
- if (result.models.length === 0) {
34
- console.log(' No models pulled yet. Run: studio ollama pull llama3.3');
35
- } else {
36
- console.log('');
37
- console.log(' Pulled models:');
38
- for (const model of result.models) {
39
- console.log(` ${chalk.bold(model.name.padEnd(30))} ${formatBytes(model.size)}`);
40
- }
41
- }
42
- }
43
-
44
- export async function ollamaStartCommand(baseUrl: string): Promise<void> {
45
- const running = await isOllamaRunning(baseUrl);
46
- if (running) {
47
- console.log(chalk.green(` ✓ Ollama already running at ${baseUrl}`));
48
- return;
49
- }
50
-
51
- const hasNative = spawnSync('ollama', ['--version'], { stdio: 'ignore' }).status === 0;
52
- if (hasNative) {
53
- console.log('Ollama is installed but not running. Start it with:');
54
- console.log('');
55
- console.log(` ${chalk.cyan('ollama serve')}`);
56
- return;
57
- }
58
-
59
- const hasDocker = spawnSync('docker', ['--version'], { stdio: 'ignore' }).status === 0;
60
- if (hasDocker) {
61
- console.log('Docker is available. Start Ollama with:');
62
- console.log('');
63
- console.log(` ${chalk.cyan(`docker run -d -p 11434:11434 --name ollama ${OLLAMA_DOCKER_IMAGE}`)}`);
64
- console.log('');
65
- console.log('Then pull a model:');
66
- console.log(` ${chalk.cyan('studio ollama pull llama3.3')}`);
67
- return;
68
- }
69
-
70
- console.log(chalk.yellow(' Neither Ollama nor Docker found.'));
71
- console.log('');
72
- console.log('Options:');
73
- console.log(` Install Ollama natively: ${chalk.cyan('https://ollama.com')}`);
74
- console.log(` Install Docker: ${chalk.cyan('https://docker.com')}`);
75
- }
76
-
77
- export async function ollamaStopCommand(): Promise<void> {
78
- console.log('To stop Ollama:');
79
- console.log('');
80
- console.log(` Native: ${chalk.cyan('Ctrl+C')} in the terminal running ${chalk.cyan('ollama serve')}`);
81
- console.log(` Docker: ${chalk.cyan('docker stop ollama')}`);
82
- }
83
-
84
- export async function ollamaPullCommand(model: string, baseUrl: string): Promise<void> {
85
- const running = await isOllamaRunning(baseUrl);
86
- if (!running) {
87
- console.error(chalk.red(` ✗ Ollama not running at ${baseUrl}`));
88
- console.error(` Run: ${chalk.cyan('studio ollama start')}`);
89
- process.exit(1);
90
- }
91
-
92
- process.stdout.write(`Pulling ${chalk.bold(model)}...`);
93
-
94
- try {
95
- const res = await fetch(`${baseUrl}/api/pull`, {
96
- method: 'POST',
97
- headers: { 'Content-Type': 'application/json' },
98
- body: JSON.stringify({ model, stream: true }),
99
- });
100
-
101
- if (!res.ok || !res.body) {
102
- process.stdout.write('\n');
103
- console.error(`Pull failed: HTTP ${res.status}`);
104
- process.exit(1);
105
- }
106
-
107
- const reader = res.body.getReader();
108
- const decoder = new TextDecoder();
109
- let lastStatus = '';
110
- let buffer = '';
111
-
112
- while (true) {
113
- const { done, value } = await reader.read();
114
- if (done) break;
115
-
116
- buffer += decoder.decode(value, { stream: true });
117
- const lines = buffer.split('\n');
118
- buffer = lines.pop() ?? '';
119
-
120
- for (const line of lines) {
121
- const trimmed = line.trim();
122
- if (!trimmed) continue;
123
- try {
124
- const event = JSON.parse(trimmed) as { status?: string; error?: string };
125
- if (event.error) {
126
- process.stdout.write('\n');
127
- console.error(chalk.red(` ✗ ${event.error}`));
128
- process.exit(1);
129
- }
130
- if (event.status && event.status !== lastStatus) {
131
- process.stdout.write(`\r${event.status.padEnd(60)}`);
132
- lastStatus = event.status;
133
- }
134
- } catch {
135
- // ignore non-JSON lines
136
- }
137
- }
138
- }
139
-
140
- process.stdout.write('\n');
141
- console.log(chalk.green(` ✓ Pulled ${model}`));
142
- } catch (err) {
143
- process.stdout.write('\n');
144
- console.error(chalk.red(` ✗ Pull failed: ${err instanceof Error ? err.message : String(err)}`));
145
- console.error(` You can retry with: ${chalk.cyan(`studio ollama pull ${model}`)}`);
146
- process.exit(1);
147
- }
148
- }
149
-
150
- export async function ollamaCommand(action: string, modelArg: string | undefined, baseUrl: string): Promise<void> {
151
- if (action === 'status') {
152
- await ollamaStatusCommand(baseUrl);
153
- } else if (action === 'start') {
154
- await ollamaStartCommand(baseUrl);
155
- } else if (action === 'stop') {
156
- await ollamaStopCommand();
157
- } else if (action === 'pull') {
158
- if (!modelArg) {
159
- console.error('Usage: studio ollama pull <model>');
160
- console.error('Example: studio ollama pull llama3.3');
161
- process.exit(1);
162
- }
163
- await ollamaPullCommand(modelArg, baseUrl);
164
- } else {
165
- console.error(`Unknown ollama action: ${action}. Use: studio ollama start|stop|status|pull <model>`);
166
- process.exit(1);
167
- }
168
- }
@@ -1,167 +0,0 @@
1
- import { mkdir, access, cp } from 'node:fs/promises';
2
- import { resolve, join, relative, sep } from 'node:path';
3
- import chalk from 'chalk';
4
- import { input, select } from '@inquirer/prompts';
5
- import { listTemplates } from './templates.js';
6
-
7
- const TEMPLATES_DIR = resolve(import.meta.dirname, '../../templates');
8
-
9
- export const PROJECT_SUBDIRS = ['pipelines', 'agents', 'contracts', 'tools', 'inputs'];
10
-
11
- /**
12
- * Create a project directory under .studio/projects/.
13
- * Throws if the project already exists or the template is not found.
14
- *
15
- * @deprecated The flat .studio/ structure no longer uses projects/ subdirs.
16
- * This function is kept for backward compatibility but will be removed.
17
- */
18
- export async function createProjectDir(
19
- projectsDir: string,
20
- projectName: string,
21
- templateName?: string,
22
- options?: { withTools?: boolean }
23
- ): Promise<void> {
24
- const withTools = options?.withTools ?? true;
25
- const projectDir = join(projectsDir, projectName);
26
-
27
- // Check if already exists
28
- const alreadyExists = await access(projectDir).then(() => true).catch(() => false);
29
- if (alreadyExists) {
30
- throw new Error(`Project '${projectName}' already exists in ${projectsDir}`);
31
- }
32
-
33
- if (templateName) {
34
- const templateDir = resolve(TEMPLATES_DIR, 'projects', templateName);
35
- const templateExists = await access(templateDir).then(() => true).catch(() => false);
36
- if (!templateExists) {
37
- throw new Error(
38
- `Template '${templateName}' not found. Run 'studio templates list' to see available templates.`
39
- );
40
- }
41
-
42
- await mkdir(projectDir, { recursive: true });
43
- if (withTools) {
44
- await cp(templateDir, projectDir, {
45
- recursive: true,
46
- filter: (src) => !src.endsWith('metadata.json'),
47
- });
48
- } else {
49
- await cp(templateDir, projectDir, {
50
- recursive: true,
51
- filter: (src) => {
52
- const rel = relative(templateDir, src);
53
- return !rel.endsWith('metadata.json') && rel !== 'tools' && !rel.startsWith('tools' + sep);
54
- },
55
- });
56
- await mkdir(join(projectDir, 'tools'), { recursive: true });
57
- }
58
- } else {
59
- for (const sub of PROJECT_SUBDIRS) {
60
- await mkdir(join(projectDir, sub), { recursive: true });
61
- }
62
- }
63
- }
64
-
65
- /**
66
- * Validate a project name: lowercase alphanumeric + hyphens, no leading/trailing hyphens.
67
- * Returns true if valid, or an error string if not.
68
- */
69
- export function validateProjectName(name: string): true | string {
70
- if (/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name)) {
71
- return true;
72
- }
73
- return 'Project name must be lowercase alphanumeric with hyphens (e.g. my-project)';
74
- }
75
-
76
- /**
77
- * Non-interactive project creation.
78
- * Validates name, then delegates to createProjectDir.
79
- *
80
- * @deprecated Use studio init in a new directory instead.
81
- */
82
- export async function projectAddDirect(
83
- studioDir: string,
84
- projectName: string,
85
- templateName?: string,
86
- _description?: string
87
- ): Promise<void> {
88
- const validation = validateProjectName(projectName);
89
- if (validation !== true) {
90
- throw new Error(validation);
91
- }
92
- const projectsDir = join(studioDir, 'projects');
93
- await createProjectDir(projectsDir, projectName, templateName);
94
- }
95
-
96
- /**
97
- * Interactive wizard for adding a project to an existing workspace.
98
- *
99
- * @deprecated Use studio init in a new directory instead.
100
- */
101
- export async function projectAddWizard(studioDir: string): Promise<void> {
102
- // Step 1: Project name
103
- const rawName = await input({
104
- message: 'Project name:',
105
- validate: (value: string) => {
106
- const trimmed = value.trim();
107
- if (!trimmed) return 'Project name is required';
108
- const v = validateProjectName(trimmed);
109
- return v === true ? true : v;
110
- },
111
- });
112
- const projectName = rawName.trim();
113
-
114
- // Step 2: Description (optional, not persisted)
115
- await input({
116
- message: 'Description (optional, press Enter to skip):',
117
- });
118
-
119
- // Step 3: Template
120
- const templates = await listTemplates();
121
- const templateChoices = templates.map((t) => ({
122
- value: t.name,
123
- name: `${t.name} — ${t.description}`,
124
- }));
125
-
126
- const templateName = await select({
127
- message: 'Choose a template:',
128
- choices: templateChoices,
129
- });
130
-
131
- // Step 4: Create
132
- console.log('');
133
- const projectsDir = join(studioDir, 'projects');
134
- await projectAddDirect(studioDir, projectName, templateName);
135
-
136
- // Step 5: Success output
137
- for (const sub of PROJECT_SUBDIRS) {
138
- console.log(chalk.green(` ✓ ${join(projectsDir, projectName, sub).replace(process.cwd() + '/', '')}/`));
139
- }
140
- console.log('');
141
-
142
- // Step 6: Next steps
143
- const selectedTemplate = templates.find((t) => t.name === templateName);
144
- const firstPipeline = selectedTemplate?.pipelines?.[0] ?? 'your-pipeline';
145
- console.log(chalk.bold('Done! Run your first pipeline:'));
146
- console.log(` ${chalk.cyan(`studio run ${projectName}/${firstPipeline} --input "..."`)}`);
147
- console.log('');
148
- }
149
-
150
- /**
151
- * CLI dispatcher for `studio project <action> [args...]`.
152
- * This command is deprecated — the flat .studio/ structure no longer uses projects/ subdirs.
153
- */
154
- export async function projectCommand(
155
- _action: string,
156
- _args: string[],
157
- _options: { template?: string; description?: string }
158
- ): Promise<void> {
159
- console.error(
160
- chalk.red(' ✗ `studio project add` is no longer supported.')
161
- );
162
- console.log('');
163
- console.log('Each workspace now has one flat .studio/ structure.');
164
- console.log(`To start a new project, create a new directory and run ${chalk.cyan('studio init')}.`);
165
- console.log('');
166
- process.exit(1);
167
- }
@@ -1,87 +0,0 @@
1
- import chalk from 'chalk';
2
- import { readFile } from 'node:fs/promises';
3
- import { resolve } from 'node:path';
4
- import { existsSync } from 'node:fs';
5
- import { createHash } from 'node:crypto';
6
- import { RegistryLockfile } from '../../registry/lockfile.js';
7
- import { findStudioDir } from '../../studio-dir.js';
8
- import { INSTALL_DIRS } from '../../registry/types.js';
9
- import type { PackageType } from '../../registry/types.js';
10
-
11
- interface AuditResult {
12
- name: string;
13
- ok: boolean;
14
- status: 'ok' | 'tampered' | 'missing';
15
- }
16
-
17
- const FILE_EXTENSIONS: Partial<Record<PackageType, string>> = {
18
- tool: '.tool.yaml',
19
- pipeline: '.pipeline.yaml',
20
- integration: '.integration.yaml',
21
- agent: '.agent.yaml',
22
- skill: '.skill.md',
23
- };
24
-
25
- interface AuditOptions {
26
- studioDir?: string;
27
- cwd?: string;
28
- }
29
-
30
- export async function auditPackages(options: AuditOptions = {}): Promise<AuditResult[]> {
31
- const studioDir = options.studioDir ??
32
- (await findStudioDir(options.cwd ?? process.cwd()) ?? resolve(process.cwd(), '.studio'));
33
- const lockfile = new RegistryLockfile(studioDir);
34
- const installed = await lockfile.list();
35
- const results: AuditResult[] = [];
36
-
37
- for (const entry of installed) {
38
- const type = entry.type as PackageType;
39
- const destDir = resolve(studioDir, INSTALL_DIRS[type]);
40
-
41
- if (type === 'template' || type === 'plugin') {
42
- // Directory packages: skip SHA check (would need to hash the full tree)
43
- results.push({ name: entry.name, ok: true, status: 'ok' });
44
- continue;
45
- }
46
-
47
- const ext = FILE_EXTENSIONS[type] ?? '.yaml';
48
- const filePath = resolve(destDir, `${entry.name}${ext}`);
49
-
50
- if (!existsSync(filePath)) {
51
- results.push({ name: entry.name, ok: false, status: 'missing' });
52
- continue;
53
- }
54
-
55
- const content = await readFile(filePath, 'utf8');
56
- const actual = createHash('sha256').update(content).digest('hex');
57
- const ok = actual === entry.sha256;
58
- results.push({ name: entry.name, ok, status: ok ? 'ok' : 'tampered' });
59
- }
60
-
61
- return results;
62
- }
63
-
64
- export async function auditCommand(): Promise<void> {
65
- const results = await auditPackages();
66
-
67
- if (results.length === 0) {
68
- console.log(chalk.gray('No packages installed.'));
69
- return;
70
- }
71
-
72
- let hasIssues = false;
73
- for (const r of results) {
74
- if (r.ok) {
75
- console.log(chalk.green(` ✓ ${r.name}`));
76
- } else {
77
- hasIssues = true;
78
- const label = r.status === 'missing' ? chalk.red('MISSING') : chalk.red('TAMPERED');
79
- console.log(` ✗ ${r.name} — ${label}`);
80
- }
81
- }
82
-
83
- if (hasIssues) {
84
- console.log(chalk.yellow('\nRun: studio registry update <name> to reinstall affected packages'));
85
- process.exit(1);
86
- }
87
- }
@@ -1,63 +0,0 @@
1
- import { Command } from 'commander';
2
- import { searchCommand, browseCommand } from './search.js';
3
- import { installCommand } from './install.js';
4
- import { removeCommand } from './remove.js';
5
- import { updateCommand, outdatedCommand } from './update.js';
6
- import { publishCommand } from './publish.js';
7
- import { auditCommand } from './audit.js';
8
- import { syncRegistry } from './sync.js';
9
-
10
- export function createRegistryCommand(): Command {
11
- const registry = new Command('registry')
12
- .description('Manage community registry packages');
13
-
14
- registry
15
- .command('search <query>')
16
- .description('Search packages in the registry')
17
- .option('--type <type>', 'Filter by type: tool, template, pipeline, integration, agent, plugin, skill')
18
- .action((query: string, options: { type?: string }) => searchCommand(query, options));
19
-
20
- registry
21
- .command('browse')
22
- .description('Browse most popular packages')
23
- .action(() => browseCommand());
24
-
25
- registry
26
- .command('install <name>')
27
- .description('Install a package (use name@version for a specific version)')
28
- .option('--force', 'Reinstall even if already installed')
29
- .action((name: string, options: { force?: boolean }) => installCommand(name, options));
30
-
31
- registry
32
- .command('remove <name>')
33
- .description('Uninstall a package')
34
- .action((name: string) => removeCommand(name));
35
-
36
- registry
37
- .command('update <name>')
38
- .description('Update an installed package to latest')
39
- .action((name: string) => updateCommand(name));
40
-
41
- registry
42
- .command('outdated')
43
- .description('List packages with available updates')
44
- .action(() => outdatedCommand());
45
-
46
- registry
47
- .command('publish <path>')
48
- .description('Publish a package to the community registry via GitHub PR')
49
- .option('--dry-run', 'Validate only, do not create PR')
50
- .action((path: string, options: { dryRun?: boolean }) => publishCommand(path, options));
51
-
52
- registry
53
- .command('audit')
54
- .description('Verify SHA256 integrity of installed packages')
55
- .action(() => auditCommand());
56
-
57
- registry
58
- .command('sync')
59
- .description('Force refresh the registry index cache')
60
- .action(() => syncRegistry({ force: true }));
61
-
62
- return registry;
63
- }