@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.
Files changed (277) hide show
  1. package/ARCHITECTURE.md +40 -0
  2. package/LICENSE +663 -0
  3. package/README.md +165 -0
  4. package/dist/commands/api.d.ts +16 -0
  5. package/dist/commands/api.d.ts.map +1 -0
  6. package/dist/commands/api.js +118 -0
  7. package/dist/commands/api.js.map +1 -0
  8. package/dist/commands/config.d.ts +32 -0
  9. package/dist/commands/config.d.ts.map +1 -0
  10. package/dist/commands/config.js +390 -0
  11. package/dist/commands/config.js.map +1 -0
  12. package/dist/commands/init.d.ts +93 -0
  13. package/dist/commands/init.d.ts.map +1 -0
  14. package/dist/commands/init.js +728 -0
  15. package/dist/commands/init.js.map +1 -0
  16. package/dist/commands/install.d.ts +2 -0
  17. package/dist/commands/install.d.ts.map +1 -0
  18. package/dist/commands/install.js +22 -0
  19. package/dist/commands/install.js.map +1 -0
  20. package/dist/commands/integrations.d.ts +14 -0
  21. package/dist/commands/integrations.d.ts.map +1 -0
  22. package/dist/commands/integrations.js +282 -0
  23. package/dist/commands/integrations.js.map +1 -0
  24. package/dist/commands/list.d.ts +9 -0
  25. package/dist/commands/list.d.ts.map +1 -0
  26. package/dist/commands/list.js +161 -0
  27. package/dist/commands/list.js.map +1 -0
  28. package/dist/commands/logs.d.ts +7 -0
  29. package/dist/commands/logs.d.ts.map +1 -0
  30. package/dist/commands/logs.js +98 -0
  31. package/dist/commands/logs.js.map +1 -0
  32. package/dist/commands/ollama.d.ts +6 -0
  33. package/dist/commands/ollama.d.ts.map +1 -0
  34. package/dist/commands/ollama.js +161 -0
  35. package/dist/commands/ollama.js.map +1 -0
  36. package/dist/commands/project.d.ts +38 -0
  37. package/dist/commands/project.d.ts.map +1 -0
  38. package/dist/commands/project.js +137 -0
  39. package/dist/commands/project.js.map +1 -0
  40. package/dist/commands/registry/audit.d.ts +13 -0
  41. package/dist/commands/registry/audit.d.ts.map +1 -0
  42. package/dist/commands/registry/audit.js +65 -0
  43. package/dist/commands/registry/audit.js.map +1 -0
  44. package/dist/commands/registry/index.d.ts +3 -0
  45. package/dist/commands/registry/index.d.ts.map +1 -0
  46. package/dist/commands/registry/index.js +53 -0
  47. package/dist/commands/registry/index.js.map +1 -0
  48. package/dist/commands/registry/install.d.ts +17 -0
  49. package/dist/commands/registry/install.d.ts.map +1 -0
  50. package/dist/commands/registry/install.js +153 -0
  51. package/dist/commands/registry/install.js.map +1 -0
  52. package/dist/commands/registry/publish.d.ts +8 -0
  53. package/dist/commands/registry/publish.d.ts.map +1 -0
  54. package/dist/commands/registry/publish.js +94 -0
  55. package/dist/commands/registry/publish.js.map +1 -0
  56. package/dist/commands/registry/remove.d.ts +8 -0
  57. package/dist/commands/registry/remove.d.ts.map +1 -0
  58. package/dist/commands/registry/remove.js +82 -0
  59. package/dist/commands/registry/remove.js.map +1 -0
  60. package/dist/commands/registry/search.d.ts +9 -0
  61. package/dist/commands/registry/search.d.ts.map +1 -0
  62. package/dist/commands/registry/search.js +68 -0
  63. package/dist/commands/registry/search.js.map +1 -0
  64. package/dist/commands/registry/sync.d.ts +8 -0
  65. package/dist/commands/registry/sync.d.ts.map +1 -0
  66. package/dist/commands/registry/sync.js +19 -0
  67. package/dist/commands/registry/sync.js.map +1 -0
  68. package/dist/commands/registry/update.d.ts +15 -0
  69. package/dist/commands/registry/update.d.ts.map +1 -0
  70. package/dist/commands/registry/update.js +48 -0
  71. package/dist/commands/registry/update.js.map +1 -0
  72. package/dist/commands/replay.d.ts +36 -0
  73. package/dist/commands/replay.d.ts.map +1 -0
  74. package/dist/commands/replay.js +453 -0
  75. package/dist/commands/replay.js.map +1 -0
  76. package/dist/commands/run.d.ts +18 -0
  77. package/dist/commands/run.d.ts.map +1 -0
  78. package/dist/commands/run.js +406 -0
  79. package/dist/commands/run.js.map +1 -0
  80. package/dist/commands/status.d.ts +6 -0
  81. package/dist/commands/status.d.ts.map +1 -0
  82. package/dist/commands/status.js +144 -0
  83. package/dist/commands/status.js.map +1 -0
  84. package/dist/commands/template/index.d.ts +2 -0
  85. package/dist/commands/template/index.d.ts.map +1 -0
  86. package/dist/commands/template/index.js +57 -0
  87. package/dist/commands/template/index.js.map +1 -0
  88. package/dist/commands/template/validate.d.ts +8 -0
  89. package/dist/commands/template/validate.d.ts.map +1 -0
  90. package/dist/commands/template/validate.js +135 -0
  91. package/dist/commands/template/validate.js.map +1 -0
  92. package/dist/commands/templates.d.ts +14 -0
  93. package/dist/commands/templates.d.ts.map +1 -0
  94. package/dist/commands/templates.js +86 -0
  95. package/dist/commands/templates.js.map +1 -0
  96. package/dist/commands/tools.d.ts +12 -0
  97. package/dist/commands/tools.d.ts.map +1 -0
  98. package/dist/commands/tools.js +200 -0
  99. package/dist/commands/tools.js.map +1 -0
  100. package/dist/commands/users.d.ts +4 -0
  101. package/dist/commands/users.d.ts.map +1 -0
  102. package/dist/commands/users.js +123 -0
  103. package/dist/commands/users.js.map +1 -0
  104. package/dist/commands/validate.d.ts +2 -0
  105. package/dist/commands/validate.d.ts.map +1 -0
  106. package/dist/commands/validate.js +39 -0
  107. package/dist/commands/validate.js.map +1 -0
  108. package/dist/config.d.ts +36 -0
  109. package/dist/config.d.ts.map +1 -0
  110. package/dist/config.js +64 -0
  111. package/dist/config.js.map +1 -0
  112. package/dist/index.d.ts +3 -0
  113. package/dist/index.d.ts.map +1 -0
  114. package/dist/index.js +178 -0
  115. package/dist/index.js.map +1 -0
  116. package/dist/models-cache.d.ts +13 -0
  117. package/dist/models-cache.d.ts.map +1 -0
  118. package/dist/models-cache.js +106 -0
  119. package/dist/models-cache.js.map +1 -0
  120. package/dist/output/file-changes.d.ts +15 -0
  121. package/dist/output/file-changes.d.ts.map +1 -0
  122. package/dist/output/file-changes.js +77 -0
  123. package/dist/output/file-changes.js.map +1 -0
  124. package/dist/output/formatter.d.ts +6 -0
  125. package/dist/output/formatter.d.ts.map +1 -0
  126. package/dist/output/formatter.js +70 -0
  127. package/dist/output/formatter.js.map +1 -0
  128. package/dist/output/formatters.d.ts +42 -0
  129. package/dist/output/formatters.d.ts.map +1 -0
  130. package/dist/output/formatters.js +280 -0
  131. package/dist/output/formatters.js.map +1 -0
  132. package/dist/output/logger.d.ts +3 -0
  133. package/dist/output/logger.d.ts.map +1 -0
  134. package/dist/output/logger.js +15 -0
  135. package/dist/output/logger.js.map +1 -0
  136. package/dist/output/parallel-progress.d.ts +33 -0
  137. package/dist/output/parallel-progress.d.ts.map +1 -0
  138. package/dist/output/parallel-progress.js +85 -0
  139. package/dist/output/parallel-progress.js.map +1 -0
  140. package/dist/output/progress.d.ts +32 -0
  141. package/dist/output/progress.d.ts.map +1 -0
  142. package/dist/output/progress.js +393 -0
  143. package/dist/output/progress.js.map +1 -0
  144. package/dist/provider-validator.d.ts +15 -0
  145. package/dist/provider-validator.d.ts.map +1 -0
  146. package/dist/provider-validator.js +85 -0
  147. package/dist/provider-validator.js.map +1 -0
  148. package/dist/registry/cache.d.ts +10 -0
  149. package/dist/registry/cache.d.ts.map +1 -0
  150. package/dist/registry/cache.js +36 -0
  151. package/dist/registry/cache.js.map +1 -0
  152. package/dist/registry/client.d.ts +19 -0
  153. package/dist/registry/client.d.ts.map +1 -0
  154. package/dist/registry/client.js +63 -0
  155. package/dist/registry/client.js.map +1 -0
  156. package/dist/registry/lockfile.d.ts +17 -0
  157. package/dist/registry/lockfile.d.ts.map +1 -0
  158. package/dist/registry/lockfile.js +61 -0
  159. package/dist/registry/lockfile.js.map +1 -0
  160. package/dist/registry/resolver.d.ts +13 -0
  161. package/dist/registry/resolver.d.ts.map +1 -0
  162. package/dist/registry/resolver.js +79 -0
  163. package/dist/registry/resolver.js.map +1 -0
  164. package/dist/registry/types.d.ts +59 -0
  165. package/dist/registry/types.d.ts.map +1 -0
  166. package/dist/registry/types.js +14 -0
  167. package/dist/registry/types.js.map +1 -0
  168. package/dist/run-logger.d.ts +8 -0
  169. package/dist/run-logger.d.ts.map +1 -0
  170. package/dist/run-logger.js +53 -0
  171. package/dist/run-logger.js.map +1 -0
  172. package/dist/run-store-factory.d.ts +9 -0
  173. package/dist/run-store-factory.d.ts.map +1 -0
  174. package/dist/run-store-factory.js +26 -0
  175. package/dist/run-store-factory.js.map +1 -0
  176. package/dist/studio-dir.d.ts +7 -0
  177. package/dist/studio-dir.d.ts.map +1 -0
  178. package/dist/studio-dir.js +27 -0
  179. package/dist/studio-dir.js.map +1 -0
  180. package/dist/utils/input-wizard.d.ts +4 -0
  181. package/dist/utils/input-wizard.d.ts.map +1 -0
  182. package/dist/utils/input-wizard.js +56 -0
  183. package/dist/utils/input-wizard.js.map +1 -0
  184. package/dist/utils/placeholders.d.ts +6 -0
  185. package/dist/utils/placeholders.d.ts.map +1 -0
  186. package/dist/utils/placeholders.js +12 -0
  187. package/dist/utils/placeholders.js.map +1 -0
  188. package/package.json +43 -0
  189. package/src/commands/api.ts +126 -0
  190. package/src/commands/config.ts +433 -0
  191. package/src/commands/init.ts +879 -0
  192. package/src/commands/install.ts +23 -0
  193. package/src/commands/integrations.ts +332 -0
  194. package/src/commands/list.ts +197 -0
  195. package/src/commands/logs.ts +119 -0
  196. package/src/commands/ollama.ts +168 -0
  197. package/src/commands/project.ts +167 -0
  198. package/src/commands/registry/audit.ts +87 -0
  199. package/src/commands/registry/index.ts +63 -0
  200. package/src/commands/registry/install.ts +222 -0
  201. package/src/commands/registry/publish.ts +112 -0
  202. package/src/commands/registry/remove.ts +89 -0
  203. package/src/commands/registry/search.ts +93 -0
  204. package/src/commands/registry/sync.ts +24 -0
  205. package/src/commands/registry/update.ts +63 -0
  206. package/src/commands/replay.ts +559 -0
  207. package/src/commands/run.ts +454 -0
  208. package/src/commands/status.ts +163 -0
  209. package/src/commands/template/index.ts +59 -0
  210. package/src/commands/template/validate.ts +175 -0
  211. package/src/commands/templates.ts +99 -0
  212. package/src/commands/tools.ts +227 -0
  213. package/src/commands/users.ts +127 -0
  214. package/src/commands/validate.ts +46 -0
  215. package/src/config.ts +101 -0
  216. package/src/index.ts +201 -0
  217. package/src/models-cache.ts +127 -0
  218. package/src/output/file-changes.ts +97 -0
  219. package/src/output/formatter.ts +85 -0
  220. package/src/output/formatters.ts +303 -0
  221. package/src/output/logger.ts +18 -0
  222. package/src/output/parallel-progress.ts +103 -0
  223. package/src/output/progress.ts +414 -0
  224. package/src/provider-validator.ts +109 -0
  225. package/src/registry/cache.ts +46 -0
  226. package/src/registry/client.ts +84 -0
  227. package/src/registry/lockfile.ts +68 -0
  228. package/src/registry/resolver.ts +113 -0
  229. package/src/registry/types.ts +66 -0
  230. package/src/run-logger.ts +66 -0
  231. package/src/run-store-factory.ts +30 -0
  232. package/src/studio-dir.ts +28 -0
  233. package/src/utils/input-wizard.ts +73 -0
  234. package/src/utils/placeholders.ts +10 -0
  235. package/templates/.studiorc.yaml +16 -0
  236. package/templates/projects/blank/metadata.json +7 -0
  237. package/templates/studio-config.yaml +14 -0
  238. package/tests/__stubs__/studio-runner.ts +104 -0
  239. package/tests/commands/api.test.ts +110 -0
  240. package/tests/commands/config.test.ts +221 -0
  241. package/tests/commands/init.test.ts +919 -0
  242. package/tests/commands/install.test.ts +52 -0
  243. package/tests/commands/integrations.test.ts +158 -0
  244. package/tests/commands/ollama.test.ts +139 -0
  245. package/tests/commands/project.test.ts +179 -0
  246. package/tests/commands/registry/audit.test.ts +56 -0
  247. package/tests/commands/registry/install.test.ts +200 -0
  248. package/tests/commands/registry/publish.test.ts +56 -0
  249. package/tests/commands/registry/remove.test.ts +103 -0
  250. package/tests/commands/registry/search.test.ts +44 -0
  251. package/tests/commands/registry/sync.test.ts +37 -0
  252. package/tests/commands/registry/update.test.ts +62 -0
  253. package/tests/commands/replay.test.ts +283 -0
  254. package/tests/commands/template/validate.test.ts +150 -0
  255. package/tests/commands/templates.test.ts +42 -0
  256. package/tests/commands/tools.test.ts +106 -0
  257. package/tests/config.test.ts +142 -0
  258. package/tests/formatter.test.ts +158 -0
  259. package/tests/integration/sigint.test.ts +188 -0
  260. package/tests/models-cache.test.ts +250 -0
  261. package/tests/output/file-changes.test.ts +178 -0
  262. package/tests/output/formatters.test.ts +448 -0
  263. package/tests/output/progress-spinner.test.ts +232 -0
  264. package/tests/output/progress-timer.test.ts +230 -0
  265. package/tests/provider-validator.test.ts +182 -0
  266. package/tests/registry/cache.test.ts +66 -0
  267. package/tests/registry/client.test.ts +70 -0
  268. package/tests/registry/lockfile.test.ts +87 -0
  269. package/tests/registry/resolver.test.ts +122 -0
  270. package/tests/run-logger-events.test.ts +326 -0
  271. package/tests/run-logger.test.ts +64 -0
  272. package/tests/run-store-factory.test.ts +51 -0
  273. package/tests/studio-dir.test.ts +31 -0
  274. package/tests/utils/input-wizard.test.ts +153 -0
  275. package/tests/utils/placeholders.test.ts +36 -0
  276. package/tsconfig.json +24 -0
  277. package/vitest.config.ts +20 -0
@@ -0,0 +1,728 @@
1
+ import { mkdir, writeFile, readFile, access, rename, readdir, lstat, copyFile, cp } from 'node:fs/promises';
2
+ import { resolve, join, basename, dirname } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import os from 'node:os';
5
+ import * as yaml from 'js-yaml';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import { input, select, password, confirm, checkbox } from '@inquirer/prompts';
9
+ import { applyPlaceholders } from '../utils/placeholders.js';
10
+ import { listTemplates } from './templates.js';
11
+ import { validateApiKeyLive } from '../provider-validator.js';
12
+ import { getAvailableModels } from '../models-cache.js';
13
+ import { toolsAddDirect } from './tools.js';
14
+ import { listAvailableToolTemplates } from '@studio-foundation/runner';
15
+ import { installPackage } from './registry/install.js';
16
+ const TEMPLATES_DIR = resolve(import.meta.dirname, '../../templates');
17
+ function formatBackupTimestamp() {
18
+ const d = new Date();
19
+ const pad = (n) => String(n).padStart(2, '0');
20
+ return (`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-` +
21
+ `${pad(d.getHours())}h${pad(d.getMinutes())}m${pad(d.getSeconds())}s`);
22
+ }
23
+ /**
24
+ * Rename .studio/ to .studio.backup-<timestamp>/ in `cwd`.
25
+ * Returns the absolute path to the backup directory.
26
+ */
27
+ export async function backupStudioDir(cwd) {
28
+ const studioDir = resolve(cwd, '.studio');
29
+ const backupDir = resolve(cwd, `.studio.backup-${formatBackupTimestamp()}`);
30
+ await rename(studioDir, backupDir);
31
+ return backupDir;
32
+ }
33
+ const GITIGNORE_ENTRIES = ['.studio/config.yaml', '.studio/runs/', '*.keymap.json'];
34
+ const STUDIO_SUBDIRS = ['pipelines', 'agents', 'contracts', 'tools', 'inputs'];
35
+ /**
36
+ * Create or populate .studio/ with subdirs.
37
+ * If installedTemplateDir is provided, copies studio-specific subdirs (pipelines, agents, etc.)
38
+ * from that directory into .studio/.
39
+ * If withTools is false, creates an empty tools/ dir instead of copying from template.
40
+ */
41
+ async function copyTemplateToStudio(studioDir, installedTemplateDir, options = {}) {
42
+ const withTools = options.withTools ?? true;
43
+ await mkdir(studioDir, { recursive: true });
44
+ if (installedTemplateDir) {
45
+ // Copy only the studio-specific subdirs into .studio/ — never the app scaffold
46
+ // files (src/, prisma/, package.json, README.md); those are handled by generateAppFiles.
47
+ for (const sub of STUDIO_SUBDIRS) {
48
+ if (!withTools && sub === 'tools')
49
+ continue;
50
+ const srcDir = join(installedTemplateDir, sub);
51
+ const destDir = join(studioDir, sub);
52
+ const srcExists = await access(srcDir).then(() => true).catch(() => false);
53
+ if (srcExists) {
54
+ await cp(srcDir, destDir, { recursive: true });
55
+ }
56
+ // Always ensure the subdir exists even if the template doesn't include it
57
+ await mkdir(destDir, { recursive: true });
58
+ }
59
+ if (!withTools) {
60
+ await mkdir(join(studioDir, 'tools'), { recursive: true });
61
+ }
62
+ }
63
+ else {
64
+ for (const sub of STUDIO_SUBDIRS) {
65
+ await mkdir(join(studioDir, sub), { recursive: true });
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * Create the full .studio/ directory structure in `cwd`.
71
+ * If templateName is provided and is not 'blank', installs the template from the registry
72
+ * to .studio/projects/<name>/ and copies studio-specific subdirs (pipelines, agents, etc.)
73
+ * from the installed location into .studio/.
74
+ * Throws if .studio/ already exists anywhere in the directory tree.
75
+ */
76
+ export async function createStudioStructure(cwd, templateName, withTools = true) {
77
+ // Only check cwd — don't walk up. A parent .studio/ (e.g. ~/.studio)
78
+ // should NOT prevent initializing a new project in a subdirectory.
79
+ const localStudio = resolve(cwd, '.studio');
80
+ const alreadyHere = await access(localStudio).then(() => true).catch(() => false);
81
+ if (alreadyHere) {
82
+ throw new Error(`Studio is already initialized at ${localStudio}\n` +
83
+ `If you want to reinitialize, delete the .studio/ directory first.`);
84
+ }
85
+ const studioDir = resolve(cwd, '.studio');
86
+ let installedTemplateDir;
87
+ if (templateName && templateName !== 'blank') {
88
+ // Install template from registry to .studio/projects/<name>/
89
+ // interactive: false — we're under a spinner, prompts would hang
90
+ await installPackage(templateName, { studioDir, interactive: false });
91
+ installedTemplateDir = resolve(studioDir, 'projects', templateName);
92
+ // Verify the install actually placed files (guards against mock/network failures)
93
+ const installed = await access(installedTemplateDir).then(() => true).catch(() => false);
94
+ if (!installed) {
95
+ throw new Error(`Template '${templateName}' not found. Run 'studio templates list' to see available templates.`);
96
+ }
97
+ }
98
+ else if (templateName === 'blank') {
99
+ // blank has no registry equivalent — validate it still "exists" as a concept
100
+ const localBlankDir = resolve(TEMPLATES_DIR, 'projects', 'blank');
101
+ const blankExists = await access(localBlankDir).then(() => true).catch(() => false);
102
+ if (!blankExists) {
103
+ throw new Error(`Template '${templateName}' not found. Run 'studio templates list' to see available templates.`);
104
+ }
105
+ // blank creates empty dirs — no installedTemplateDir to copy from
106
+ }
107
+ await copyTemplateToStudio(studioDir, installedTemplateDir, { withTools });
108
+ // Create runs/logs/
109
+ await mkdir(join(studioDir, 'runs', 'logs'), { recursive: true });
110
+ // Write registry.lock.json (empty, committed)
111
+ await writeFile(join(studioDir, 'registry.lock.json'), '{"installed":{}}\n', 'utf-8');
112
+ // Copy config template (only if config.yaml doesn't already exist)
113
+ const configPath = join(studioDir, 'config.yaml');
114
+ const configExists = await access(configPath)
115
+ .then(() => true)
116
+ .catch(() => false);
117
+ if (!configExists) {
118
+ const template = await readFile(resolve(TEMPLATES_DIR, 'studio-config.yaml'), 'utf-8');
119
+ await writeFile(configPath, template, 'utf-8');
120
+ }
121
+ // Update .gitignore
122
+ await updateGitignore(cwd);
123
+ }
124
+ async function updateGitignore(cwd) {
125
+ const gitignorePath = resolve(cwd, '.gitignore');
126
+ let existing = '';
127
+ try {
128
+ existing = await readFile(gitignorePath, 'utf-8');
129
+ }
130
+ catch {
131
+ // file doesn't exist yet
132
+ }
133
+ const toAdd = GITIGNORE_ENTRIES.filter((entry) => !existing.includes(entry));
134
+ if (toAdd.length === 0)
135
+ return;
136
+ const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
137
+ const addition = '\n# Studio (generated)\n' + toAdd.join('\n') + '\n';
138
+ await writeFile(gitignorePath, existing + separator + addition, 'utf-8');
139
+ }
140
+ const DEFAULT_MODELS = {
141
+ anthropic: 'claude-sonnet-4-20250514',
142
+ openai: 'gpt-4o',
143
+ ollama: 'llama3.3',
144
+ };
145
+ export function detectOllamaInstalled() {
146
+ // Native binary
147
+ if (spawnSync('ollama', ['--version'], { stdio: 'ignore' }).status === 0)
148
+ return true;
149
+ // Running as a Docker container (common setup: `docker exec ollama ollama`)
150
+ return spawnSync('docker', ['exec', 'ollama', 'ollama', '--version'], { stdio: 'ignore' }).status === 0;
151
+ }
152
+ const RAM_16GB = 16 * 1024 ** 3;
153
+ export function hasAdequateRam() {
154
+ try {
155
+ return os.totalmem() >= RAM_16GB;
156
+ }
157
+ catch {
158
+ return false;
159
+ }
160
+ }
161
+ /**
162
+ * Write provider credentials and defaults into .studio/config.yaml.
163
+ * Parses the existing config, sets the provider key, then rewrites.
164
+ * Comments from the original template are lost — accepted for Phase 1.
165
+ */
166
+ export async function writeProviderToConfig(studioDir, provider, credentials, model) {
167
+ const configPath = join(studioDir, 'config.yaml');
168
+ let raw = '';
169
+ try {
170
+ raw = await readFile(configPath, 'utf-8');
171
+ }
172
+ catch {
173
+ // Config doesn't exist yet — start from empty
174
+ }
175
+ const parsed = (yaml.load(raw) ?? {});
176
+ // Set provider key
177
+ if (!parsed.providers || typeof parsed.providers !== 'object') {
178
+ parsed.providers = {};
179
+ }
180
+ if (provider === 'ollama') {
181
+ parsed.providers[provider] =
182
+ credentials?.baseUrl ? { baseUrl: credentials.baseUrl } : {};
183
+ }
184
+ else {
185
+ parsed.providers[provider] =
186
+ credentials?.apiKey ? { apiKey: credentials.apiKey } : {};
187
+ }
188
+ // Set defaults
189
+ parsed.defaults = {
190
+ provider,
191
+ model: model ?? DEFAULT_MODELS[provider] ?? 'claude-sonnet-4-20250514',
192
+ };
193
+ await writeFile(configPath, yaml.dump(parsed), 'utf-8');
194
+ }
195
+ /**
196
+ * Direct init (non-interactive) — creates .studio/ structure and writes config.
197
+ * @deprecated Use generateFullApp() for full app generation (src/, prisma/, git init).
198
+ * This function only creates the .studio/ workspace without app scaffold files.
199
+ * Kept for backward compatibility.
200
+ */
201
+ export async function directInit(cwd, templateName, provider, apiKey, noTools = false) {
202
+ await createStudioStructure(cwd, templateName, !noTools);
203
+ if (provider !== 'later') {
204
+ const studioDir = resolve(cwd, '.studio');
205
+ await writeProviderToConfig(studioDir, provider, { apiKey: apiKey || undefined });
206
+ }
207
+ }
208
+ /**
209
+ * Validate API key format without making a network call.
210
+ * Returns true if valid, or an error string to display.
211
+ */
212
+ export function validateApiKeyFormat(provider, key) {
213
+ if (provider === 'anthropic') {
214
+ if (!key.startsWith('sk-ant-')) {
215
+ return 'Anthropic API keys must start with sk-ant-';
216
+ }
217
+ }
218
+ else if (provider === 'openai') {
219
+ if (!key.startsWith('sk-')) {
220
+ return 'OpenAI API keys must start with sk-';
221
+ }
222
+ }
223
+ return true;
224
+ }
225
+ /**
226
+ * Validate a project name for use as a directory name.
227
+ * Returns true if valid, or an error string to display.
228
+ */
229
+ export function validateProjectName(name) {
230
+ if (!name.trim())
231
+ return 'Project name cannot be empty';
232
+ if (/\s/.test(name))
233
+ return 'Project name cannot contain spaces';
234
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9\-_.]*$/.test(name))
235
+ return 'Project name must start with a letter or digit and contain only letters, digits, hyphens, underscores, or dots';
236
+ return true;
237
+ }
238
+ // App scaffold items to copy from template root → target root
239
+ const APP_SCAFFOLD_ITEMS = ['src', 'prisma', 'package.json', 'README.md'];
240
+ /**
241
+ * Copy app scaffold files (src/, prisma/, package.json, README.md) from
242
+ * `templateDir` to `targetDir`, applying placeholder replacement to all
243
+ * text file contents. Items missing from the template are silently skipped.
244
+ *
245
+ * @returns List of top-level items that were generated (e.g. ['src/', 'package.json'])
246
+ */
247
+ export async function generateAppFiles(templateDir, targetDir, vars) {
248
+ const generated = [];
249
+ for (const item of APP_SCAFFOLD_ITEMS) {
250
+ const src = join(templateDir, item);
251
+ const dest = join(targetDir, item);
252
+ let stat;
253
+ try {
254
+ stat = await lstat(src);
255
+ }
256
+ catch {
257
+ continue; // Not present in this template — skip
258
+ }
259
+ if (stat.isDirectory()) {
260
+ await copyDirWithPlaceholders(src, dest, vars);
261
+ generated.push(item + '/');
262
+ }
263
+ else {
264
+ const content = await readFile(src, 'utf-8');
265
+ const replaced = applyPlaceholders(content, vars);
266
+ await writeFile(dest, replaced, 'utf-8');
267
+ generated.push(item);
268
+ }
269
+ }
270
+ return generated;
271
+ }
272
+ const TEXT_EXTENSIONS = new Set([
273
+ '.ts', '.js', '.json', '.md', '.yaml', '.yml',
274
+ '.prisma', '.txt', '.env', '.gitignore', '.sh',
275
+ ]);
276
+ /**
277
+ * Recursively copy a directory, applying placeholder replacement to known text
278
+ * file extensions. All other files (binary or unknown) are copied as-is.
279
+ */
280
+ async function copyDirWithPlaceholders(src, dest, vars) {
281
+ await mkdir(dest, { recursive: true });
282
+ const entries = await readdir(src, { withFileTypes: true });
283
+ for (const entry of entries) {
284
+ const srcPath = join(src, entry.name);
285
+ const destPath = join(dest, entry.name);
286
+ if (entry.isDirectory()) {
287
+ await copyDirWithPlaceholders(srcPath, destPath, vars);
288
+ }
289
+ else {
290
+ const ext = entry.name.includes('.') ? '.' + entry.name.split('.').pop() : '';
291
+ if (TEXT_EXTENSIONS.has(ext)) {
292
+ const content = await readFile(srcPath, 'utf-8');
293
+ const replaced = applyPlaceholders(content, vars);
294
+ await writeFile(destPath, replaced, 'utf-8');
295
+ }
296
+ else {
297
+ await copyFile(srcPath, destPath);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ /**
303
+ * Run `git init` in `cwd` unless `.git/` already exists.
304
+ * Returns true if git was initialized, false if skipped.
305
+ * Throws if `git init` exits with non-zero status.
306
+ */
307
+ export async function initGitRepo(cwd) {
308
+ const gitDir = join(cwd, '.git');
309
+ const alreadyGit = await access(gitDir).then(() => true).catch(() => false);
310
+ if (alreadyGit)
311
+ return false;
312
+ const result = spawnSync('git', ['init'], { cwd, encoding: 'utf-8' });
313
+ if (result.status !== 0) {
314
+ const stderr = (result.stderr ?? '').trim();
315
+ throw new Error(`git init failed: ${stderr}`);
316
+ }
317
+ return true;
318
+ }
319
+ /**
320
+ * Generate a complete app from a template:
321
+ * 1. Creates .studio/ workspace and installs the template from the registry to .studio/projects/<name>/
322
+ * 2. Copies app scaffold files (src/, prisma/, package.json, README.md) from the installed template
323
+ * 3. Initializes a git repository (unless skipGit)
324
+ *
325
+ * Does NOT write provider config — call writeProviderToConfig separately.
326
+ *
327
+ * @returns `{ gitInitialized, generatedFiles }` — gitInitialized is true if git was initialized,
328
+ * generatedFiles is the list of top-level scaffold items actually created (e.g. ['src/', 'package.json']).
329
+ */
330
+ export async function generateFullApp(cwd, projectName, templateName, options = {}) {
331
+ const studioDir = resolve(cwd, '.studio');
332
+ // 1. Create .studio/ workspace and install template from registry to .studio/projects/<name>/
333
+ // createStudioStructure handles both the directory creation and registry install.
334
+ await createStudioStructure(cwd, templateName, !options.noTools);
335
+ // 2. Copy app scaffold files from installed template to project root with placeholder replacement
336
+ const installedTemplateDir = resolve(studioDir, 'projects', templateName);
337
+ const vars = {
338
+ PROJECT_NAME: projectName,
339
+ TEMPLATE_NAME: templateName,
340
+ YEAR: String(new Date().getFullYear()),
341
+ };
342
+ const generatedFiles = await generateAppFiles(installedTemplateDir, cwd, vars);
343
+ // 3. Initialize git repo (unless already initialized or skipped)
344
+ let gitInitialized = false;
345
+ if (!options.skipGit) {
346
+ gitInitialized = await initGitRepo(cwd);
347
+ }
348
+ return { gitInitialized, generatedFiles };
349
+ }
350
+ export function detectHardware() {
351
+ const totalRamGb = os.totalmem() / (1024 ** 3);
352
+ const hasDocker = spawnSync('docker', ['--version'], { stdio: 'ignore' }).status === 0;
353
+ const hasNativeOllama = spawnSync('ollama', ['--version'], { stdio: 'ignore' }).status === 0;
354
+ return {
355
+ totalRamGb,
356
+ hasDocker,
357
+ hasNativeOllama,
358
+ ollamaAvailable: hasDocker || hasNativeOllama,
359
+ };
360
+ }
361
+ function detectPackageManager() {
362
+ const check = (cmd) => spawnSync(cmd, ['--version'], { stdio: 'ignore' }).status === 0;
363
+ if (check('pnpm'))
364
+ return 'pnpm';
365
+ if (check('yarn'))
366
+ return 'yarn';
367
+ if (check('bun'))
368
+ return 'bun';
369
+ return 'npm';
370
+ }
371
+ function printTemplateCard(template) {
372
+ const lines = [template.description];
373
+ if (template.pipelines?.length) {
374
+ lines.push(`Pipelines: ${template.pipelines.join(', ')}`);
375
+ }
376
+ if (template.tools_included?.length) {
377
+ lines.push(`Tools: ${template.tools_included.join(', ')}`);
378
+ }
379
+ const minWidth = `─ ${template.name} `.length;
380
+ const innerWidth = Math.max(minWidth, ...lines.map((l) => l.length));
381
+ const bar = '─'.repeat(innerWidth + 4);
382
+ const templateLabel = `─ ${template.name} `;
383
+ const rightBar = '─'.repeat(Math.max(0, bar.length - templateLabel.length));
384
+ console.log('');
385
+ console.log(` ${templateLabel}${rightBar}`);
386
+ for (const line of lines) {
387
+ console.log(` │ ${line}`);
388
+ }
389
+ console.log(` ${bar}`);
390
+ console.log('');
391
+ }
392
+ export async function initCommand(nameArg, options = {}) {
393
+ try {
394
+ const cwd = process.cwd();
395
+ // ── Exists detection (cwd only — parent .studio/ doesn't block init) ──
396
+ const localStudio = resolve(cwd, '.studio');
397
+ const existing = await access(localStudio).then(() => localStudio).catch(() => null);
398
+ if (existing && !options.force) {
399
+ console.error(chalk.red(` ✗ Studio is already initialized in this directory`));
400
+ console.log('');
401
+ console.log('To reconfigure:');
402
+ console.log(` ${chalk.cyan('studio config add-provider')} # Add/update LLM provider`);
403
+ console.log(` ${chalk.cyan('studio tools add')} # Install additional tools`);
404
+ console.log('');
405
+ console.log('To start fresh:');
406
+ console.log(` ${chalk.cyan('studio init --force')} # ⚠ Backs up existing config`);
407
+ process.exit(1);
408
+ }
409
+ // ── Force: backup existing .studio/ ──────────────────────────────
410
+ if (existing && options.force) {
411
+ if (!options.yes) {
412
+ const confirmed = await confirm({
413
+ message: '⚠ This will backup your existing .studio/ directory. Continue?',
414
+ default: false,
415
+ });
416
+ if (!confirmed) {
417
+ console.log('Aborted.');
418
+ process.exit(0);
419
+ }
420
+ }
421
+ const backupPath = await backupStudioDir(dirname(existing));
422
+ const backupName = backupPath.split('/').at(-1);
423
+ console.log('');
424
+ console.log(chalk.green(` ✓ Backed up to ${backupName}/`));
425
+ console.log('');
426
+ }
427
+ // ── Direct mode (all flags provided) vs Wizard ────────────────────
428
+ const isDirectMode = !!(options.template && options.provider);
429
+ if (isDirectMode) {
430
+ // Validate required flags
431
+ if (options.provider !== 'later' && options.provider !== 'ollama' && !options.apiKey) {
432
+ console.error('Error: --api-key is required when --provider is not "later" or "ollama"');
433
+ process.exit(1);
434
+ }
435
+ if (options.provider !== 'later' && options.provider !== 'ollama' && options.apiKey) {
436
+ const validation = validateApiKeyFormat(options.provider, options.apiKey);
437
+ if (validation !== true) {
438
+ console.error(`Error: ${validation}`);
439
+ process.exit(1);
440
+ }
441
+ process.stdout.write('Validating API key...');
442
+ const result = await validateApiKeyLive(options.provider, options.apiKey);
443
+ if (result.status === 'valid') {
444
+ console.log(chalk.green(' ✓'));
445
+ }
446
+ else if (result.status === 'warning') {
447
+ console.log(chalk.yellow(` ⚠ ${result.message}`));
448
+ }
449
+ else {
450
+ console.error(chalk.red(` ✗ ${result.error}`));
451
+ process.exit(1);
452
+ }
453
+ }
454
+ const spinner = ora('Creating project...').start();
455
+ let gitInitialized = false;
456
+ let generatedFiles = [];
457
+ try {
458
+ const projectName = nameArg ?? options.project ?? basename(cwd);
459
+ ({ gitInitialized, generatedFiles } = await generateFullApp(cwd, projectName, options.template, {
460
+ noTools: options.tools === false,
461
+ }));
462
+ if (options.provider !== 'later') {
463
+ const studioDir = resolve(cwd, '.studio');
464
+ // For ollama: no apiKey needed — uses DEFAULT_MODELS['ollama'] ('llama3.3') as model fallback
465
+ await writeProviderToConfig(studioDir, options.provider, { apiKey: options.apiKey || undefined });
466
+ }
467
+ spinner.stop();
468
+ }
469
+ catch (err) {
470
+ spinner.fail('Failed');
471
+ throw err;
472
+ }
473
+ console.log(chalk.green(` ✓ .studio/config.yaml`));
474
+ console.log(chalk.green(` ✓ .studio/pipelines/`));
475
+ for (const f of generatedFiles) {
476
+ console.log(chalk.green(` ✓ ${f}`));
477
+ }
478
+ if (gitInitialized) {
479
+ console.log(chalk.green(` ✓ git initialized`));
480
+ }
481
+ console.log(chalk.green(` ✓ Updated .gitignore`));
482
+ console.log('');
483
+ const templates = await listTemplates();
484
+ const selectedTemplate = templates.find((t) => t.name === options.template);
485
+ const firstPipeline = selectedTemplate?.pipelines?.[0] ?? 'your-pipeline';
486
+ console.log(chalk.bold('Done! Next steps:'));
487
+ console.log(` ${chalk.cyan('npm install')}`);
488
+ console.log(` ${chalk.cyan(`studio run ${firstPipeline} --input "..."`)}`);
489
+ if (options.provider === 'later') {
490
+ console.log('');
491
+ console.log('Set your provider first:');
492
+ console.log(` ${chalk.cyan('studio config set provider ollama')} # local, no API key`);
493
+ console.log(` ${chalk.cyan('studio config set provider anthropic --api-key $ANTHROPIC_API_KEY')}`);
494
+ }
495
+ console.log('');
496
+ return;
497
+ }
498
+ // ── Wizard mode ───────────────────────────────────────────────────
499
+ // Non-interactive terminal fallback
500
+ if (!process.stdin.isTTY) {
501
+ console.error('stdin is not a TTY. Use flags for non-interactive init:');
502
+ console.error(' studio init --template <type> --name <project> --provider <provider> --api-key <key>');
503
+ process.exit(1);
504
+ }
505
+ console.log('');
506
+ console.log(chalk.bold(' ╭─────────────────────────────────╮'));
507
+ console.log(chalk.bold(' │ Studio — Create App │'));
508
+ console.log(chalk.bold(' ╰─────────────────────────────────╯'));
509
+ console.log('');
510
+ // Step 1: Template selection (first — drives everything else)
511
+ const templates = await listTemplates();
512
+ const templateChoices = templates.map((t) => ({
513
+ value: t.name,
514
+ name: `${t.name} — ${t.description}`,
515
+ }));
516
+ const templateName = await select({
517
+ message: 'What type of app are you building?',
518
+ choices: templateChoices,
519
+ default: options.template,
520
+ });
521
+ // Show template details card
522
+ const selectedTemplateMeta = templates.find((t) => t.name === templateName);
523
+ if (selectedTemplateMeta) {
524
+ printTemplateCard(selectedTemplateMeta);
525
+ }
526
+ // Step 2: Project name (with validation)
527
+ const defaultName = nameArg ?? options.project ?? basename(cwd);
528
+ const projectName = await input({
529
+ message: 'Project name:',
530
+ default: defaultName,
531
+ validate: validateProjectName,
532
+ });
533
+ // Step 3: Provider — detect Ollama and hardware first
534
+ const ollamaInstalled = detectOllamaInstalled();
535
+ const ramOk = hasAdequateRam();
536
+ const providerChoices = [];
537
+ if (ollamaInstalled && !ramOk) {
538
+ // Ollama present but hardware limited: warn, cloud providers first
539
+ console.log('');
540
+ console.log(chalk.yellow(' ⚠ Ollama detected but your system has less than 16GB RAM.'));
541
+ console.log(chalk.gray(' Results may be slow or limited for code generation.'));
542
+ console.log(chalk.gray(' You can switch to Ollama later: studio config set provider ollama'));
543
+ console.log('');
544
+ providerChoices.push({ value: 'anthropic', name: 'Anthropic (Claude)' }, { value: 'openai', name: 'OpenAI (GPT)' }, { value: 'ollama', name: 'Ollama (llama3.3) — installed but limited hardware' }, { value: 'later', name: 'Configure later' });
545
+ }
546
+ else if (ollamaInstalled) {
547
+ // Ollama present + good hardware: Ollama first, pre-selected
548
+ providerChoices.push({ value: 'ollama', name: 'Ollama (llama3.3) — runs locally, no API key needed' }, { value: 'anthropic', name: 'Anthropic (Claude)' }, { value: 'openai', name: 'OpenAI (GPT)' }, { value: 'later', name: 'Configure later' });
549
+ }
550
+ else {
551
+ // Ollama not installed: show as disabled, cloud providers selectable
552
+ providerChoices.push({
553
+ value: 'ollama-disabled',
554
+ name: 'Ollama (not installed — run: ollama pull llama3.3)',
555
+ disabled: 'not installed',
556
+ }, { value: 'anthropic', name: 'Anthropic (Claude)' }, { value: 'openai', name: 'OpenAI (GPT)' }, { value: 'later', name: 'Configure later' });
557
+ }
558
+ const providerDefault = ollamaInstalled && ramOk ? 'ollama'
559
+ : 'anthropic';
560
+ const provider = await select({
561
+ message: 'LLM Provider:',
562
+ choices: providerChoices,
563
+ default: providerDefault,
564
+ });
565
+ // Guard: 'ollama-disabled' is shown as disabled in the UI but defend against it anyway
566
+ if (provider === 'ollama-disabled') {
567
+ console.log(chalk.yellow('\n Ollama is not installed. Please install it first:'));
568
+ console.log(chalk.gray(' https://ollama.ai'));
569
+ console.log(chalk.gray(' Then re-run: studio init'));
570
+ process.exit(0);
571
+ }
572
+ // Step 4: API Key — skip for Ollama (runs locally, no key needed)
573
+ let apiKey;
574
+ if (provider !== 'later' && provider !== 'ollama') {
575
+ const providerLabel = provider === 'anthropic' ? 'Anthropic' : 'OpenAI';
576
+ while (true) {
577
+ apiKey = await password({
578
+ message: `${providerLabel} API Key:`,
579
+ validate: (value) => validateApiKeyFormat(provider, value),
580
+ });
581
+ const spinner = ora('Validating...').start();
582
+ const result = await validateApiKeyLive(provider, apiKey);
583
+ spinner.stop();
584
+ if (result.status === 'valid') {
585
+ console.log(chalk.green(' ✓ Valid'));
586
+ break;
587
+ }
588
+ else if (result.status === 'warning') {
589
+ console.log(chalk.yellow(` ⚠ ${result.message}`));
590
+ break;
591
+ }
592
+ else {
593
+ console.log(chalk.red(` ✗ ${result.error}`));
594
+ console.log(chalk.gray(' Please try again.'));
595
+ }
596
+ }
597
+ }
598
+ // Step 5: Choose default model
599
+ let selectedModel;
600
+ if (provider === 'ollama') {
601
+ // Don't call remote API — Ollama runs locally. Default is llama3.3.
602
+ // User can change later with: studio config set default.model <name>
603
+ selectedModel = await input({
604
+ message: 'Default model:',
605
+ default: 'llama3.3',
606
+ });
607
+ }
608
+ else if (provider !== 'later' && apiKey) {
609
+ const models = await getAvailableModels(provider, apiKey);
610
+ const fallback = DEFAULT_MODELS[provider] ?? 'claude-sonnet-4-20250514';
611
+ if (models.length > 0) {
612
+ const choices = [
613
+ ...models.map((m) => ({ value: m, name: m })),
614
+ { value: '__custom__', name: 'Enter custom model ID' },
615
+ ];
616
+ const chosen = await select({
617
+ message: 'Default model:',
618
+ choices,
619
+ default: models.includes(fallback) ? fallback : models[0],
620
+ });
621
+ if (chosen === '__custom__') {
622
+ selectedModel = await input({ message: 'Model ID:', default: fallback });
623
+ }
624
+ else {
625
+ selectedModel = chosen;
626
+ }
627
+ }
628
+ else {
629
+ selectedModel = await input({ message: 'Default model:', default: fallback });
630
+ }
631
+ }
632
+ // Step 6: Tool selection
633
+ const availableTools = await listAvailableToolTemplates();
634
+ let selectedTools = [];
635
+ if (availableTools.length > 0) {
636
+ const recommended = new Set(selectedTemplateMeta?.tools_included ?? []);
637
+ const toolChoices = availableTools.map((t) => ({
638
+ value: t.name,
639
+ name: `${t.name} — ${t.description}`,
640
+ checked: recommended.has(t.name),
641
+ }));
642
+ console.log('');
643
+ selectedTools = await checkbox({
644
+ message: 'Select tools to install:',
645
+ choices: toolChoices,
646
+ });
647
+ }
648
+ // Step 7: Install dependencies preference
649
+ const pkgManager = detectPackageManager();
650
+ const installNow = await confirm({
651
+ message: `Install dependencies now? (uses ${pkgManager})`,
652
+ default: false,
653
+ });
654
+ // Step 8: Generate app (without tools — we install them below)
655
+ console.log('');
656
+ const spinner = ora('Creating project...').start();
657
+ const studioDir = resolve(cwd, '.studio');
658
+ let gitInitialized = false;
659
+ let generatedFiles = [];
660
+ try {
661
+ ({ gitInitialized, generatedFiles } = await generateFullApp(cwd, projectName, templateName, { noTools: true }));
662
+ if (provider !== 'later') {
663
+ await writeProviderToConfig(studioDir, provider, provider === 'ollama' ? {} : { apiKey }, selectedModel);
664
+ }
665
+ spinner.stop();
666
+ }
667
+ catch (err) {
668
+ spinner.fail('Failed');
669
+ throw err;
670
+ }
671
+ // Step 9: Install selected tools
672
+ if (selectedTools.length > 0) {
673
+ await toolsAddDirect(studioDir, selectedTools);
674
+ }
675
+ // Step 10: Install dependencies (if requested)
676
+ if (installNow) {
677
+ const installSpinner = ora(`Running ${pkgManager} install...`).start();
678
+ const installResult = spawnSync(pkgManager, ['install'], { cwd, encoding: 'utf-8' });
679
+ if (installResult.status === 0) {
680
+ installSpinner.succeed('Dependencies installed');
681
+ }
682
+ else {
683
+ installSpinner.warn(`Install failed — run \`${pkgManager} install\` manually`);
684
+ if (installResult.stderr) {
685
+ console.error(installResult.stderr);
686
+ }
687
+ }
688
+ }
689
+ // Step 11: Success output
690
+ console.log(chalk.green(` ✓ .studio/config.yaml`));
691
+ console.log(chalk.green(` ✓ .studio/pipelines/`));
692
+ for (const f of generatedFiles) {
693
+ console.log(chalk.green(` ✓ ${f}`));
694
+ }
695
+ if (gitInitialized) {
696
+ console.log(chalk.green(` ✓ git initialized`));
697
+ }
698
+ console.log(chalk.green(` ✓ Updated .gitignore`));
699
+ if (selectedTools.length > 0) {
700
+ console.log(chalk.green(` ✓ Installed tools: ${selectedTools.join(', ')}`));
701
+ }
702
+ console.log('');
703
+ // Step 12: Next steps
704
+ const firstPipeline = selectedTemplateMeta?.pipelines?.[0] ?? 'your-pipeline';
705
+ console.log(chalk.bold('Done! Next steps:'));
706
+ if (!installNow) {
707
+ console.log(` ${chalk.cyan(`${pkgManager} install`)}`);
708
+ }
709
+ console.log(` ${chalk.cyan(`studio run ${firstPipeline} --input "..."`)}`);
710
+ if (provider === 'later') {
711
+ console.log('');
712
+ console.log('Set your provider first:');
713
+ console.log(` ${chalk.cyan('studio config set provider ollama')} # local, no API key`);
714
+ console.log(` ${chalk.cyan('studio config set provider anthropic --api-key $ANTHROPIC_API_KEY')}`);
715
+ }
716
+ console.log('');
717
+ }
718
+ catch (error) {
719
+ // Graceful exit on Ctrl+C
720
+ if (error instanceof Error && error.name === 'ExitPromptError') {
721
+ console.log('\nAborted.');
722
+ process.exit(0);
723
+ }
724
+ console.error('Error:', error instanceof Error ? error.message : error);
725
+ process.exit(1);
726
+ }
727
+ }
728
+ //# sourceMappingURL=init.js.map