@studio-foundation/cli 0.3.0-beta.1
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/ARCHITECTURE.md +40 -0
- package/LICENSE +663 -0
- package/README.md +165 -0
- package/dist/commands/api.d.ts +16 -0
- package/dist/commands/api.d.ts.map +1 -0
- package/dist/commands/api.js +118 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/config.d.ts +32 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +390 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/init.d.ts +93 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +728 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +22 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/integrations.d.ts +14 -0
- package/dist/commands/integrations.d.ts.map +1 -0
- package/dist/commands/integrations.js +282 -0
- package/dist/commands/integrations.js.map +1 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +161 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/logs.d.ts +7 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +98 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/ollama.d.ts +6 -0
- package/dist/commands/ollama.d.ts.map +1 -0
- package/dist/commands/ollama.js +161 -0
- package/dist/commands/ollama.js.map +1 -0
- package/dist/commands/project.d.ts +38 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +137 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/registry/audit.d.ts +13 -0
- package/dist/commands/registry/audit.d.ts.map +1 -0
- package/dist/commands/registry/audit.js +65 -0
- package/dist/commands/registry/audit.js.map +1 -0
- package/dist/commands/registry/index.d.ts +3 -0
- package/dist/commands/registry/index.d.ts.map +1 -0
- package/dist/commands/registry/index.js +53 -0
- package/dist/commands/registry/index.js.map +1 -0
- package/dist/commands/registry/install.d.ts +17 -0
- package/dist/commands/registry/install.d.ts.map +1 -0
- package/dist/commands/registry/install.js +153 -0
- package/dist/commands/registry/install.js.map +1 -0
- package/dist/commands/registry/publish.d.ts +8 -0
- package/dist/commands/registry/publish.d.ts.map +1 -0
- package/dist/commands/registry/publish.js +94 -0
- package/dist/commands/registry/publish.js.map +1 -0
- package/dist/commands/registry/remove.d.ts +8 -0
- package/dist/commands/registry/remove.d.ts.map +1 -0
- package/dist/commands/registry/remove.js +82 -0
- package/dist/commands/registry/remove.js.map +1 -0
- package/dist/commands/registry/search.d.ts +9 -0
- package/dist/commands/registry/search.d.ts.map +1 -0
- package/dist/commands/registry/search.js +68 -0
- package/dist/commands/registry/search.js.map +1 -0
- package/dist/commands/registry/sync.d.ts +8 -0
- package/dist/commands/registry/sync.d.ts.map +1 -0
- package/dist/commands/registry/sync.js +19 -0
- package/dist/commands/registry/sync.js.map +1 -0
- package/dist/commands/registry/update.d.ts +15 -0
- package/dist/commands/registry/update.d.ts.map +1 -0
- package/dist/commands/registry/update.js +48 -0
- package/dist/commands/registry/update.js.map +1 -0
- package/dist/commands/replay.d.ts +36 -0
- package/dist/commands/replay.d.ts.map +1 -0
- package/dist/commands/replay.js +453 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/run.d.ts +18 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +406 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +144 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/template/index.d.ts +2 -0
- package/dist/commands/template/index.d.ts.map +1 -0
- package/dist/commands/template/index.js +57 -0
- package/dist/commands/template/index.js.map +1 -0
- package/dist/commands/template/validate.d.ts +8 -0
- package/dist/commands/template/validate.d.ts.map +1 -0
- package/dist/commands/template/validate.js +135 -0
- package/dist/commands/template/validate.js.map +1 -0
- package/dist/commands/templates.d.ts +14 -0
- package/dist/commands/templates.d.ts.map +1 -0
- package/dist/commands/templates.js +86 -0
- package/dist/commands/templates.js.map +1 -0
- package/dist/commands/tools.d.ts +12 -0
- package/dist/commands/tools.d.ts.map +1 -0
- package/dist/commands/tools.js +200 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commands/users.d.ts +4 -0
- package/dist/commands/users.d.ts.map +1 -0
- package/dist/commands/users.js +123 -0
- package/dist/commands/users.js.map +1 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +39 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +178 -0
- package/dist/index.js.map +1 -0
- package/dist/models-cache.d.ts +13 -0
- package/dist/models-cache.d.ts.map +1 -0
- package/dist/models-cache.js +106 -0
- package/dist/models-cache.js.map +1 -0
- package/dist/output/file-changes.d.ts +15 -0
- package/dist/output/file-changes.d.ts.map +1 -0
- package/dist/output/file-changes.js +77 -0
- package/dist/output/file-changes.js.map +1 -0
- package/dist/output/formatter.d.ts +6 -0
- package/dist/output/formatter.d.ts.map +1 -0
- package/dist/output/formatter.js +70 -0
- package/dist/output/formatter.js.map +1 -0
- package/dist/output/formatters.d.ts +42 -0
- package/dist/output/formatters.d.ts.map +1 -0
- package/dist/output/formatters.js +280 -0
- package/dist/output/formatters.js.map +1 -0
- package/dist/output/logger.d.ts +3 -0
- package/dist/output/logger.d.ts.map +1 -0
- package/dist/output/logger.js +15 -0
- package/dist/output/logger.js.map +1 -0
- package/dist/output/parallel-progress.d.ts +33 -0
- package/dist/output/parallel-progress.d.ts.map +1 -0
- package/dist/output/parallel-progress.js +85 -0
- package/dist/output/parallel-progress.js.map +1 -0
- package/dist/output/progress.d.ts +32 -0
- package/dist/output/progress.d.ts.map +1 -0
- package/dist/output/progress.js +393 -0
- package/dist/output/progress.js.map +1 -0
- package/dist/provider-validator.d.ts +15 -0
- package/dist/provider-validator.d.ts.map +1 -0
- package/dist/provider-validator.js +85 -0
- package/dist/provider-validator.js.map +1 -0
- package/dist/registry/cache.d.ts +10 -0
- package/dist/registry/cache.d.ts.map +1 -0
- package/dist/registry/cache.js +36 -0
- package/dist/registry/cache.js.map +1 -0
- package/dist/registry/client.d.ts +19 -0
- package/dist/registry/client.d.ts.map +1 -0
- package/dist/registry/client.js +63 -0
- package/dist/registry/client.js.map +1 -0
- package/dist/registry/lockfile.d.ts +17 -0
- package/dist/registry/lockfile.d.ts.map +1 -0
- package/dist/registry/lockfile.js +61 -0
- package/dist/registry/lockfile.js.map +1 -0
- package/dist/registry/resolver.d.ts +13 -0
- package/dist/registry/resolver.d.ts.map +1 -0
- package/dist/registry/resolver.js +79 -0
- package/dist/registry/resolver.js.map +1 -0
- package/dist/registry/types.d.ts +59 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +14 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/run-logger.d.ts +8 -0
- package/dist/run-logger.d.ts.map +1 -0
- package/dist/run-logger.js +53 -0
- package/dist/run-logger.js.map +1 -0
- package/dist/run-store-factory.d.ts +9 -0
- package/dist/run-store-factory.d.ts.map +1 -0
- package/dist/run-store-factory.js +26 -0
- package/dist/run-store-factory.js.map +1 -0
- package/dist/studio-dir.d.ts +7 -0
- package/dist/studio-dir.d.ts.map +1 -0
- package/dist/studio-dir.js +27 -0
- package/dist/studio-dir.js.map +1 -0
- package/dist/utils/input-wizard.d.ts +4 -0
- package/dist/utils/input-wizard.d.ts.map +1 -0
- package/dist/utils/input-wizard.js +56 -0
- package/dist/utils/input-wizard.js.map +1 -0
- package/dist/utils/placeholders.d.ts +6 -0
- package/dist/utils/placeholders.d.ts.map +1 -0
- package/dist/utils/placeholders.js +12 -0
- package/dist/utils/placeholders.js.map +1 -0
- package/package.json +43 -0
- package/src/commands/api.ts +126 -0
- package/src/commands/config.ts +433 -0
- package/src/commands/init.ts +879 -0
- package/src/commands/install.ts +23 -0
- package/src/commands/integrations.ts +332 -0
- package/src/commands/list.ts +197 -0
- package/src/commands/logs.ts +119 -0
- package/src/commands/ollama.ts +168 -0
- package/src/commands/project.ts +167 -0
- package/src/commands/registry/audit.ts +87 -0
- package/src/commands/registry/index.ts +63 -0
- package/src/commands/registry/install.ts +222 -0
- package/src/commands/registry/publish.ts +112 -0
- package/src/commands/registry/remove.ts +89 -0
- package/src/commands/registry/search.ts +93 -0
- package/src/commands/registry/sync.ts +24 -0
- package/src/commands/registry/update.ts +63 -0
- package/src/commands/replay.ts +559 -0
- package/src/commands/run.ts +454 -0
- package/src/commands/status.ts +163 -0
- package/src/commands/template/index.ts +59 -0
- package/src/commands/template/validate.ts +175 -0
- package/src/commands/templates.ts +99 -0
- package/src/commands/tools.ts +227 -0
- package/src/commands/users.ts +127 -0
- package/src/commands/validate.ts +46 -0
- package/src/config.ts +101 -0
- package/src/index.ts +201 -0
- package/src/models-cache.ts +127 -0
- package/src/output/file-changes.ts +97 -0
- package/src/output/formatter.ts +85 -0
- package/src/output/formatters.ts +303 -0
- package/src/output/logger.ts +18 -0
- package/src/output/parallel-progress.ts +103 -0
- package/src/output/progress.ts +414 -0
- package/src/provider-validator.ts +109 -0
- package/src/registry/cache.ts +46 -0
- package/src/registry/client.ts +84 -0
- package/src/registry/lockfile.ts +68 -0
- package/src/registry/resolver.ts +113 -0
- package/src/registry/types.ts +66 -0
- package/src/run-logger.ts +66 -0
- package/src/run-store-factory.ts +30 -0
- package/src/studio-dir.ts +28 -0
- package/src/utils/input-wizard.ts +73 -0
- package/src/utils/placeholders.ts +10 -0
- package/templates/.studiorc.yaml +16 -0
- package/templates/projects/blank/metadata.json +7 -0
- package/templates/studio-config.yaml +14 -0
- package/tests/__stubs__/studio-runner.ts +104 -0
- package/tests/commands/api.test.ts +110 -0
- package/tests/commands/config.test.ts +221 -0
- package/tests/commands/init.test.ts +919 -0
- package/tests/commands/install.test.ts +52 -0
- package/tests/commands/integrations.test.ts +158 -0
- package/tests/commands/ollama.test.ts +139 -0
- package/tests/commands/project.test.ts +179 -0
- package/tests/commands/registry/audit.test.ts +56 -0
- package/tests/commands/registry/install.test.ts +200 -0
- package/tests/commands/registry/publish.test.ts +56 -0
- package/tests/commands/registry/remove.test.ts +103 -0
- package/tests/commands/registry/search.test.ts +44 -0
- package/tests/commands/registry/sync.test.ts +37 -0
- package/tests/commands/registry/update.test.ts +62 -0
- package/tests/commands/replay.test.ts +283 -0
- package/tests/commands/template/validate.test.ts +150 -0
- package/tests/commands/templates.test.ts +42 -0
- package/tests/commands/tools.test.ts +106 -0
- package/tests/config.test.ts +142 -0
- package/tests/formatter.test.ts +158 -0
- package/tests/integration/sigint.test.ts +188 -0
- package/tests/models-cache.test.ts +250 -0
- package/tests/output/file-changes.test.ts +178 -0
- package/tests/output/formatters.test.ts +448 -0
- package/tests/output/progress-spinner.test.ts +232 -0
- package/tests/output/progress-timer.test.ts +230 -0
- package/tests/provider-validator.test.ts +182 -0
- package/tests/registry/cache.test.ts +66 -0
- package/tests/registry/client.test.ts +70 -0
- package/tests/registry/lockfile.test.ts +87 -0
- package/tests/registry/resolver.test.ts +122 -0
- package/tests/run-logger-events.test.ts +326 -0
- package/tests/run-logger.test.ts +64 -0
- package/tests/run-store-factory.test.ts +51 -0
- package/tests/studio-dir.test.ts +31 -0
- package/tests/utils/input-wizard.test.ts +153 -0
- package/tests/utils/placeholders.test.ts +36 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|