@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.
Files changed (88) hide show
  1. package/package.json +9 -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/tests/__stubs__/studio-runner.ts +0 -104
  50. package/tests/commands/api.test.ts +0 -110
  51. package/tests/commands/config.test.ts +0 -221
  52. package/tests/commands/init.test.ts +0 -919
  53. package/tests/commands/install.test.ts +0 -52
  54. package/tests/commands/integrations.test.ts +0 -158
  55. package/tests/commands/ollama.test.ts +0 -139
  56. package/tests/commands/project.test.ts +0 -179
  57. package/tests/commands/registry/audit.test.ts +0 -56
  58. package/tests/commands/registry/install.test.ts +0 -200
  59. package/tests/commands/registry/publish.test.ts +0 -56
  60. package/tests/commands/registry/remove.test.ts +0 -103
  61. package/tests/commands/registry/search.test.ts +0 -44
  62. package/tests/commands/registry/sync.test.ts +0 -37
  63. package/tests/commands/registry/update.test.ts +0 -62
  64. package/tests/commands/replay.test.ts +0 -283
  65. package/tests/commands/template/validate.test.ts +0 -150
  66. package/tests/commands/templates.test.ts +0 -42
  67. package/tests/commands/tools.test.ts +0 -106
  68. package/tests/config.test.ts +0 -142
  69. package/tests/formatter.test.ts +0 -158
  70. package/tests/integration/sigint.test.ts +0 -188
  71. package/tests/models-cache.test.ts +0 -250
  72. package/tests/output/file-changes.test.ts +0 -178
  73. package/tests/output/formatters.test.ts +0 -448
  74. package/tests/output/progress-spinner.test.ts +0 -232
  75. package/tests/output/progress-timer.test.ts +0 -230
  76. package/tests/provider-validator.test.ts +0 -182
  77. package/tests/registry/cache.test.ts +0 -66
  78. package/tests/registry/client.test.ts +0 -70
  79. package/tests/registry/lockfile.test.ts +0 -87
  80. package/tests/registry/resolver.test.ts +0 -122
  81. package/tests/run-logger-events.test.ts +0 -326
  82. package/tests/run-logger.test.ts +0 -64
  83. package/tests/run-store-factory.test.ts +0 -51
  84. package/tests/studio-dir.test.ts +0 -31
  85. package/tests/utils/input-wizard.test.ts +0 -153
  86. package/tests/utils/placeholders.test.ts +0 -36
  87. package/tsconfig.json +0 -24
  88. 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
- }