@outputai/cli 0.1.2 → 0.1.3-dev.0

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 (278) hide show
  1. package/dist/api/generated/api.d.ts +820 -0
  2. package/dist/api/generated/api.js +226 -0
  3. package/dist/api/http_client.d.ts +27 -0
  4. package/dist/api/http_client.js +71 -0
  5. package/dist/api/orval_post_process.d.ts +11 -0
  6. package/dist/api/orval_post_process.js +46 -0
  7. package/dist/api/parser.d.ts +17 -0
  8. package/dist/api/parser.js +68 -0
  9. package/dist/assets/config/costs.yml +309 -0
  10. package/dist/assets/docker/docker-compose-dev.yml +146 -0
  11. package/dist/commands/credentials/edit.d.ts +10 -0
  12. package/dist/commands/credentials/edit.js +67 -0
  13. package/dist/commands/credentials/edit.spec.d.ts +1 -0
  14. package/dist/commands/credentials/edit.spec.js +73 -0
  15. package/dist/commands/credentials/get.d.ts +13 -0
  16. package/dist/commands/credentials/get.js +46 -0
  17. package/dist/commands/credentials/get.spec.d.ts +1 -0
  18. package/dist/commands/credentials/get.spec.js +74 -0
  19. package/dist/commands/credentials/init.d.ts +11 -0
  20. package/dist/commands/credentials/init.js +45 -0
  21. package/dist/commands/credentials/init.spec.d.ts +1 -0
  22. package/dist/commands/credentials/init.spec.js +68 -0
  23. package/dist/commands/credentials/show.d.ts +10 -0
  24. package/dist/commands/credentials/show.js +33 -0
  25. package/dist/commands/credentials/show.spec.d.ts +1 -0
  26. package/dist/commands/credentials/show.spec.js +57 -0
  27. package/dist/commands/dev/eject.d.ts +11 -0
  28. package/dist/commands/dev/eject.js +58 -0
  29. package/dist/commands/dev/eject.spec.d.ts +1 -0
  30. package/dist/commands/dev/eject.spec.js +109 -0
  31. package/dist/commands/dev/index.d.ts +14 -0
  32. package/dist/commands/dev/index.js +173 -0
  33. package/dist/commands/dev/index.spec.d.ts +1 -0
  34. package/dist/commands/dev/index.spec.js +239 -0
  35. package/dist/commands/init.d.ts +12 -0
  36. package/dist/commands/init.js +37 -0
  37. package/dist/commands/init.spec.d.ts +1 -0
  38. package/dist/commands/init.spec.js +100 -0
  39. package/dist/commands/update.d.ts +14 -0
  40. package/dist/commands/update.js +120 -0
  41. package/dist/commands/update.spec.d.ts +1 -0
  42. package/dist/commands/update.spec.js +178 -0
  43. package/dist/commands/workflow/cost.d.ts +16 -0
  44. package/dist/commands/workflow/cost.js +71 -0
  45. package/dist/commands/workflow/cost.spec.d.ts +1 -0
  46. package/dist/commands/workflow/cost.spec.js +47 -0
  47. package/dist/commands/workflow/dataset/generate.d.ts +22 -0
  48. package/dist/commands/workflow/dataset/generate.js +143 -0
  49. package/dist/commands/workflow/dataset/list.d.ts +12 -0
  50. package/dist/commands/workflow/dataset/list.js +87 -0
  51. package/dist/commands/workflow/debug.d.ts +16 -0
  52. package/dist/commands/workflow/debug.js +60 -0
  53. package/dist/commands/workflow/debug.spec.d.ts +1 -0
  54. package/dist/commands/workflow/debug.spec.js +34 -0
  55. package/dist/commands/workflow/generate.d.ts +17 -0
  56. package/dist/commands/workflow/generate.js +85 -0
  57. package/dist/commands/workflow/generate.spec.d.ts +1 -0
  58. package/dist/commands/workflow/generate.spec.js +115 -0
  59. package/dist/commands/workflow/list.d.ts +22 -0
  60. package/dist/commands/workflow/list.js +152 -0
  61. package/dist/commands/workflow/list.spec.d.ts +1 -0
  62. package/dist/commands/workflow/list.spec.js +99 -0
  63. package/dist/commands/workflow/plan.d.ts +12 -0
  64. package/dist/commands/workflow/plan.js +66 -0
  65. package/dist/commands/workflow/plan.spec.d.ts +1 -0
  66. package/dist/commands/workflow/plan.spec.js +341 -0
  67. package/dist/commands/workflow/reset.d.ts +14 -0
  68. package/dist/commands/workflow/reset.js +51 -0
  69. package/dist/commands/workflow/result.d.ts +13 -0
  70. package/dist/commands/workflow/result.js +46 -0
  71. package/dist/commands/workflow/result.spec.d.ts +1 -0
  72. package/dist/commands/workflow/result.spec.js +23 -0
  73. package/dist/commands/workflow/run.d.ts +16 -0
  74. package/dist/commands/workflow/run.js +97 -0
  75. package/dist/commands/workflow/run.spec.d.ts +1 -0
  76. package/dist/commands/workflow/run.spec.js +110 -0
  77. package/dist/commands/workflow/runs/list.d.ts +14 -0
  78. package/dist/commands/workflow/runs/list.js +104 -0
  79. package/dist/commands/workflow/start.d.ts +15 -0
  80. package/dist/commands/workflow/start.js +62 -0
  81. package/dist/commands/workflow/start.spec.d.ts +1 -0
  82. package/dist/commands/workflow/start.spec.js +28 -0
  83. package/dist/commands/workflow/status.d.ts +13 -0
  84. package/dist/commands/workflow/status.js +57 -0
  85. package/dist/commands/workflow/status.spec.d.ts +1 -0
  86. package/dist/commands/workflow/status.spec.js +33 -0
  87. package/dist/commands/workflow/stop.d.ts +10 -0
  88. package/dist/commands/workflow/stop.js +31 -0
  89. package/dist/commands/workflow/stop.spec.d.ts +1 -0
  90. package/dist/commands/workflow/stop.spec.js +17 -0
  91. package/dist/commands/workflow/terminate.d.ts +13 -0
  92. package/dist/commands/workflow/terminate.js +39 -0
  93. package/dist/commands/workflow/test_eval.d.ts +20 -0
  94. package/dist/commands/workflow/test_eval.js +151 -0
  95. package/dist/config.d.ts +47 -0
  96. package/dist/config.js +47 -0
  97. package/dist/generated/framework_version.json +3 -0
  98. package/dist/hooks/init.d.ts +3 -0
  99. package/dist/hooks/init.js +30 -0
  100. package/dist/hooks/init.spec.d.ts +1 -0
  101. package/dist/hooks/init.spec.js +54 -0
  102. package/dist/index.d.ts +1 -0
  103. package/dist/index.js +1 -0
  104. package/dist/index.spec.d.ts +1 -0
  105. package/dist/index.spec.js +6 -0
  106. package/dist/services/claude_client.d.ts +30 -0
  107. package/dist/services/claude_client.integration.test.d.ts +1 -0
  108. package/dist/services/claude_client.integration.test.js +43 -0
  109. package/dist/services/claude_client.js +215 -0
  110. package/dist/services/claude_client.spec.d.ts +1 -0
  111. package/dist/services/claude_client.spec.js +145 -0
  112. package/dist/services/coding_agents.d.ts +36 -0
  113. package/dist/services/coding_agents.js +236 -0
  114. package/dist/services/coding_agents.spec.d.ts +1 -0
  115. package/dist/services/coding_agents.spec.js +256 -0
  116. package/dist/services/copy_assets.spec.d.ts +1 -0
  117. package/dist/services/copy_assets.spec.js +22 -0
  118. package/dist/services/cost_calculator.d.ts +18 -0
  119. package/dist/services/cost_calculator.js +359 -0
  120. package/dist/services/cost_calculator.spec.d.ts +1 -0
  121. package/dist/services/cost_calculator.spec.js +540 -0
  122. package/dist/services/credentials_service.d.ts +12 -0
  123. package/dist/services/credentials_service.integration.test.d.ts +1 -0
  124. package/dist/services/credentials_service.integration.test.js +66 -0
  125. package/dist/services/credentials_service.js +64 -0
  126. package/dist/services/credentials_service.spec.d.ts +1 -0
  127. package/dist/services/credentials_service.spec.js +106 -0
  128. package/dist/services/datasets.d.ts +20 -0
  129. package/dist/services/datasets.js +132 -0
  130. package/dist/services/docker.d.ts +39 -0
  131. package/dist/services/docker.js +160 -0
  132. package/dist/services/docker.spec.d.ts +1 -0
  133. package/dist/services/docker.spec.js +124 -0
  134. package/dist/services/env_configurator.d.ts +15 -0
  135. package/dist/services/env_configurator.js +163 -0
  136. package/dist/services/env_configurator.spec.d.ts +1 -0
  137. package/dist/services/env_configurator.spec.js +192 -0
  138. package/dist/services/generate_plan_name@v1.prompt +24 -0
  139. package/dist/services/messages.d.ts +9 -0
  140. package/dist/services/messages.js +338 -0
  141. package/dist/services/messages.spec.d.ts +1 -0
  142. package/dist/services/messages.spec.js +55 -0
  143. package/dist/services/npm_update_service.d.ts +6 -0
  144. package/dist/services/npm_update_service.js +87 -0
  145. package/dist/services/npm_update_service.spec.d.ts +1 -0
  146. package/dist/services/npm_update_service.spec.js +104 -0
  147. package/dist/services/project_scaffold.d.ts +31 -0
  148. package/dist/services/project_scaffold.js +212 -0
  149. package/dist/services/project_scaffold.spec.d.ts +1 -0
  150. package/dist/services/project_scaffold.spec.js +122 -0
  151. package/dist/services/s3_trace_downloader.d.ts +12 -0
  152. package/dist/services/s3_trace_downloader.js +57 -0
  153. package/dist/services/template_processor.d.ts +14 -0
  154. package/dist/services/template_processor.js +57 -0
  155. package/dist/services/trace_reader.d.ts +16 -0
  156. package/dist/services/trace_reader.js +57 -0
  157. package/dist/services/trace_reader.spec.d.ts +1 -0
  158. package/dist/services/trace_reader.spec.js +78 -0
  159. package/dist/services/version_check.d.ts +6 -0
  160. package/dist/services/version_check.js +52 -0
  161. package/dist/services/version_check.spec.d.ts +1 -0
  162. package/dist/services/version_check.spec.js +106 -0
  163. package/dist/services/workflow_builder.d.ts +16 -0
  164. package/dist/services/workflow_builder.js +86 -0
  165. package/dist/services/workflow_builder.spec.d.ts +1 -0
  166. package/dist/services/workflow_builder.spec.js +165 -0
  167. package/dist/services/workflow_generator.d.ts +5 -0
  168. package/dist/services/workflow_generator.js +40 -0
  169. package/dist/services/workflow_generator.spec.d.ts +1 -0
  170. package/dist/services/workflow_generator.spec.js +77 -0
  171. package/dist/services/workflow_planner.d.ts +15 -0
  172. package/dist/services/workflow_planner.js +48 -0
  173. package/dist/services/workflow_planner.spec.d.ts +1 -0
  174. package/dist/services/workflow_planner.spec.js +122 -0
  175. package/dist/services/workflow_runs.d.ts +14 -0
  176. package/dist/services/workflow_runs.js +25 -0
  177. package/dist/templates/agent_instructions/CLAUDE.md.template +19 -0
  178. package/dist/templates/agent_instructions/dotclaude/settings.json.template +29 -0
  179. package/dist/templates/project/.env.example.template +9 -0
  180. package/dist/templates/project/.gitignore.template +35 -0
  181. package/dist/templates/project/README.md.template +100 -0
  182. package/dist/templates/project/config/costs.yml.template +29 -0
  183. package/dist/templates/project/package.json.template +25 -0
  184. package/dist/templates/project/src/clients/jina.ts.template +30 -0
  185. package/dist/templates/project/src/shared/utils/string.ts.template +3 -0
  186. package/dist/templates/project/src/shared/utils/url.ts.template +15 -0
  187. package/dist/templates/project/src/workflows/blog_evaluator/evaluators.ts.template +23 -0
  188. package/dist/templates/project/src/workflows/blog_evaluator/prompts/signal_noise@v1.prompt.template +26 -0
  189. package/dist/templates/project/src/workflows/blog_evaluator/scenarios/paulgraham_hwh.json.template +3 -0
  190. package/dist/templates/project/src/workflows/blog_evaluator/steps.ts.template +27 -0
  191. package/dist/templates/project/src/workflows/blog_evaluator/types.ts.template +30 -0
  192. package/dist/templates/project/src/workflows/blog_evaluator/utils.ts.template +15 -0
  193. package/dist/templates/project/src/workflows/blog_evaluator/workflow.ts.template +27 -0
  194. package/dist/templates/project/tsconfig.json.template +20 -0
  195. package/dist/templates/workflow/README.md.template +216 -0
  196. package/dist/templates/workflow/evaluators.ts.template +21 -0
  197. package/dist/templates/workflow/prompts/example@v1.prompt.template +15 -0
  198. package/dist/templates/workflow/scenarios/test_input.json.template +3 -0
  199. package/dist/templates/workflow/steps.ts.template +20 -0
  200. package/dist/templates/workflow/types.ts.template +13 -0
  201. package/dist/templates/workflow/workflow.ts.template +23 -0
  202. package/dist/test_helpers/mocks.d.ts +38 -0
  203. package/dist/test_helpers/mocks.js +77 -0
  204. package/dist/types/cost.d.ts +149 -0
  205. package/dist/types/cost.js +6 -0
  206. package/dist/types/domain.d.ts +20 -0
  207. package/dist/types/domain.js +4 -0
  208. package/dist/types/errors.d.ts +68 -0
  209. package/dist/types/errors.js +100 -0
  210. package/dist/types/errors.spec.d.ts +1 -0
  211. package/dist/types/errors.spec.js +18 -0
  212. package/dist/types/generator.d.ts +26 -0
  213. package/dist/types/generator.js +1 -0
  214. package/dist/types/trace.d.ts +161 -0
  215. package/dist/types/trace.js +18 -0
  216. package/dist/utils/claude.d.ts +5 -0
  217. package/dist/utils/claude.js +19 -0
  218. package/dist/utils/claude.spec.d.ts +1 -0
  219. package/dist/utils/claude.spec.js +119 -0
  220. package/dist/utils/constants.d.ts +5 -0
  221. package/dist/utils/constants.js +4 -0
  222. package/dist/utils/cost_formatter.d.ts +5 -0
  223. package/dist/utils/cost_formatter.js +218 -0
  224. package/dist/utils/date_formatter.d.ts +23 -0
  225. package/dist/utils/date_formatter.js +49 -0
  226. package/dist/utils/env_loader.d.ts +1 -0
  227. package/dist/utils/env_loader.js +22 -0
  228. package/dist/utils/env_loader.spec.d.ts +1 -0
  229. package/dist/utils/env_loader.spec.js +43 -0
  230. package/dist/utils/error_handler.d.ts +8 -0
  231. package/dist/utils/error_handler.js +71 -0
  232. package/dist/utils/error_utils.d.ts +24 -0
  233. package/dist/utils/error_utils.js +87 -0
  234. package/dist/utils/file_system.d.ts +3 -0
  235. package/dist/utils/file_system.js +33 -0
  236. package/dist/utils/format_workflow_result.d.ts +5 -0
  237. package/dist/utils/format_workflow_result.js +18 -0
  238. package/dist/utils/format_workflow_result.spec.d.ts +1 -0
  239. package/dist/utils/format_workflow_result.spec.js +81 -0
  240. package/dist/utils/framework_version.d.ts +4 -0
  241. package/dist/utils/framework_version.js +4 -0
  242. package/dist/utils/framework_version.spec.d.ts +1 -0
  243. package/dist/utils/framework_version.spec.js +13 -0
  244. package/dist/utils/header_utils.d.ts +12 -0
  245. package/dist/utils/header_utils.js +29 -0
  246. package/dist/utils/header_utils.spec.d.ts +1 -0
  247. package/dist/utils/header_utils.spec.js +52 -0
  248. package/dist/utils/input_parser.d.ts +1 -0
  249. package/dist/utils/input_parser.js +19 -0
  250. package/dist/utils/output_formatter.d.ts +2 -0
  251. package/dist/utils/output_formatter.js +11 -0
  252. package/dist/utils/paths.d.ts +25 -0
  253. package/dist/utils/paths.js +36 -0
  254. package/dist/utils/process.d.ts +4 -0
  255. package/dist/utils/process.js +50 -0
  256. package/dist/utils/resolve_input.d.ts +1 -0
  257. package/dist/utils/resolve_input.js +22 -0
  258. package/dist/utils/scenario_resolver.d.ts +9 -0
  259. package/dist/utils/scenario_resolver.js +93 -0
  260. package/dist/utils/scenario_resolver.spec.d.ts +1 -0
  261. package/dist/utils/scenario_resolver.spec.js +214 -0
  262. package/dist/utils/secret_sanitizer.d.ts +1 -0
  263. package/dist/utils/secret_sanitizer.js +29 -0
  264. package/dist/utils/sleep.d.ts +5 -0
  265. package/dist/utils/sleep.js +5 -0
  266. package/dist/utils/template.d.ts +9 -0
  267. package/dist/utils/template.js +30 -0
  268. package/dist/utils/template.spec.d.ts +1 -0
  269. package/dist/utils/template.spec.js +77 -0
  270. package/dist/utils/trace_extractor.d.ts +27 -0
  271. package/dist/utils/trace_extractor.js +53 -0
  272. package/dist/utils/trace_formatter.d.ts +11 -0
  273. package/dist/utils/trace_formatter.js +402 -0
  274. package/dist/utils/validation.d.ts +13 -0
  275. package/dist/utils/validation.js +25 -0
  276. package/dist/utils/validation.spec.d.ts +1 -0
  277. package/dist/utils/validation.spec.js +140 -0
  278. package/package.json +4 -4
@@ -0,0 +1,212 @@
1
+ import { input, confirm } from '@inquirer/prompts';
2
+ import { ux } from '@oclif/core';
3
+ import { kebabCase, pascalCase } from 'change-case';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { FolderAlreadyExistsError, UserCancelledError, DirectoryCreationError } from '#types/errors.js';
7
+ import { createDirectory } from '#utils/file_system.js';
8
+ import { executeCommand, executeCommandWithMessages } from '#utils/process.js';
9
+ import { getFrameworkVersion } from '#utils/framework_version.js';
10
+ import { getErrorMessage, getErrorCode } from '#utils/error_utils.js';
11
+ import { isDockerInstalled } from '#services/docker.js';
12
+ import { isClaudeCliAvailable } from '#utils/claude.js';
13
+ import { configureEnvironmentVariables } from './env_configurator.js';
14
+ import { getTemplateFiles, processTemplateFile } from './template_processor.js';
15
+ import { initializeAgentConfig } from './coding_agents.js';
16
+ import { getProjectSuccessMessage } from './messages.js';
17
+ /**
18
+ * Check for required dependencies (Docker and Claude CLI)
19
+ * Prompts user to continue if dependencies are missing
20
+ * @throws UserCancelledError if user declines to proceed without dependencies
21
+ */
22
+ export async function checkDependencies() {
23
+ const dockerInstalled = isDockerInstalled();
24
+ const claudeAvailable = isClaudeCliAvailable();
25
+ if (dockerInstalled && claudeAvailable) {
26
+ return;
27
+ }
28
+ const missingDeps = [];
29
+ if (!dockerInstalled) {
30
+ missingDeps.push('Docker (https://docs.docker.com/)');
31
+ }
32
+ if (!claudeAvailable) {
33
+ missingDeps.push('Claude CLI (https://code.claude.com/)');
34
+ }
35
+ const depList = missingDeps.join('\n - ');
36
+ const message = `The following dependencies are missing:\n - ${depList}\n\n` +
37
+ 'Some features may not work correctly without these dependencies.';
38
+ ux.warn(message);
39
+ try {
40
+ const shouldProceed = await confirm({
41
+ message: 'Would you like to proceed anyway?',
42
+ default: false
43
+ });
44
+ if (!shouldProceed) {
45
+ throw new UserCancelledError();
46
+ }
47
+ }
48
+ catch {
49
+ throw new UserCancelledError();
50
+ }
51
+ }
52
+ const promptForFolderName = async (projectName) => {
53
+ return await input({
54
+ message: 'What folder name should be used?',
55
+ default: kebabCase(projectName)
56
+ }) || kebabCase(projectName);
57
+ };
58
+ const promptForProjectName = async (defaultProjectName) => {
59
+ return await input({
60
+ message: 'What is your project name?',
61
+ default: defaultProjectName
62
+ }) || defaultProjectName;
63
+ };
64
+ const generateProjectDescription = (projectName) => {
65
+ return `AI Agents & Workflows built with Output.ai for ${kebabCase(projectName)}`;
66
+ };
67
+ /**
68
+ * Get project configuration from user input
69
+ * @param userFolderNameArg - Optional folder name to skip folder name prompt
70
+ */
71
+ export const getProjectConfig = async (userFolderNameArg) => {
72
+ const defaultProjectName = 'my-outputai-workflows';
73
+ try {
74
+ const projectName = userFolderNameArg ?
75
+ userFolderNameArg :
76
+ await promptForProjectName(defaultProjectName);
77
+ const folderName = userFolderNameArg ?
78
+ userFolderNameArg :
79
+ await promptForFolderName(projectName);
80
+ const description = generateProjectDescription(projectName);
81
+ return {
82
+ projectName,
83
+ folderName,
84
+ projectPath: path.resolve(process.cwd(), folderName),
85
+ description
86
+ };
87
+ }
88
+ catch {
89
+ throw new UserCancelledError();
90
+ }
91
+ };
92
+ async function scaffoldProjectFiles(projectPath, projectName, description) {
93
+ const __filename = fileURLToPath(import.meta.url);
94
+ const __dirname = path.dirname(__filename);
95
+ const templatesDir = path.join(__dirname, '..', 'templates', 'project');
96
+ // Get framework version for dynamic template injection
97
+ const frameworkVersion = await getFrameworkVersion();
98
+ const templateVars = {
99
+ projectName: kebabCase(projectName),
100
+ ProjectName: pascalCase(projectName),
101
+ description: description || `An Output.ai workflow for ${projectName}`,
102
+ frameworkVersion: frameworkVersion.framework
103
+ };
104
+ const templateFiles = await getTemplateFiles(templatesDir);
105
+ await Promise.all(templateFiles.map(templateFile => processTemplateFile(templateFile, projectPath, templateVars)));
106
+ return templateFiles.map(f => f.outputName);
107
+ }
108
+ async function executeNpmInstall(projectPath) {
109
+ await executeCommand('npm', ['install'], projectPath);
110
+ }
111
+ async function initializeAgents(projectPath) {
112
+ await initializeAgentConfig({ projectRoot: projectPath, force: false });
113
+ }
114
+ /**
115
+ * Format error message for init errors
116
+ * Single responsibility: only format error messages, no cleanup logic
117
+ */
118
+ function formatInitError(error, projectPath) {
119
+ if (error instanceof UserCancelledError) {
120
+ return error.message;
121
+ }
122
+ if (error instanceof FolderAlreadyExistsError) {
123
+ return error.message;
124
+ }
125
+ const errorCode = getErrorCode(error);
126
+ const pathSuffix = projectPath ? ` at ${projectPath}` : '';
127
+ switch (errorCode) {
128
+ case 'EEXIST': return 'Folder already exists';
129
+ case 'EACCES': return `Permission denied${pathSuffix}`;
130
+ case 'ENOSPC': return `Not enough disk space${pathSuffix}`;
131
+ case 'EPERM': return `Operation not permitted${pathSuffix}`;
132
+ case 'ENOENT': return `Required file or directory not found${pathSuffix}`;
133
+ default: {
134
+ const originalMessage = error instanceof Error ? error.message : String(error);
135
+ return `Failed to create project${pathSuffix}: ${originalMessage}`;
136
+ }
137
+ }
138
+ }
139
+ /**
140
+ * Create a SIGINT handler for cleanup during init
141
+ * Exits immediately without prompting to avoid race conditions
142
+ * @param projectPath - Path to the project folder
143
+ * @param folderCreated - Whether the folder has been created
144
+ */
145
+ export function createSigintHandler(projectPath, folderCreated) {
146
+ return () => {
147
+ ux.stdout('\n');
148
+ if (folderCreated) {
149
+ ux.warn(`Incomplete project folder may exist at: ${projectPath}`);
150
+ ux.warn(`Run: rm -rf "${projectPath}" to clean up`);
151
+ }
152
+ process.exit(130);
153
+ };
154
+ }
155
+ function handleRunInitError(error, projectPath, projectFolderCreated) {
156
+ const errorMessage = formatInitError(error, projectPath);
157
+ if (projectFolderCreated && projectPath) {
158
+ ux.warn(`Incomplete project folder may exist at: ${projectPath}`);
159
+ ux.warn(`Run: rm -rf "${projectPath}" to clean up`);
160
+ }
161
+ throw new Error(errorMessage);
162
+ }
163
+ /**
164
+ * Run the init command workflow
165
+ * @param skipEnv - Whether to skip environment configuration prompts
166
+ * @param folderName - Optional folder name to skip folder name prompt
167
+ */
168
+ export async function runInit(skipEnv = false, folderName) {
169
+ // Track state for SIGINT cleanup using an object to avoid let
170
+ const state = {
171
+ projectFolderCreated: false,
172
+ projectPath: ''
173
+ };
174
+ // Create and register SIGINT handler
175
+ const sigintHandler = () => {
176
+ const handler = createSigintHandler(state.projectPath, state.projectFolderCreated);
177
+ handler();
178
+ };
179
+ process.on('SIGINT', sigintHandler);
180
+ try {
181
+ // Check dependencies first
182
+ await checkDependencies();
183
+ const config = await getProjectConfig(folderName);
184
+ state.projectPath = config.projectPath;
185
+ try {
186
+ createDirectory(config.projectPath);
187
+ state.projectFolderCreated = true;
188
+ }
189
+ catch (error) {
190
+ throw new DirectoryCreationError(getErrorMessage(error), config.projectPath);
191
+ }
192
+ ux.stdout(`Created project folder: ${config.folderName}`);
193
+ const filesCreated = await scaffoldProjectFiles(config.projectPath, config.projectName, config.description);
194
+ ux.stdout(`Created ${filesCreated.length} project files`);
195
+ const envConfigured = await configureEnvironmentVariables(config.projectPath, skipEnv);
196
+ if (envConfigured) {
197
+ ux.stdout('Environment variables configured in .env');
198
+ }
199
+ await executeCommandWithMessages(() => initializeAgents(config.projectPath), 'Initializing agent system...', 'Agent system initialized');
200
+ const installSuccess = await executeCommandWithMessages(() => executeNpmInstall(config.projectPath), 'Installing dependencies...', 'Dependencies installed');
201
+ const nextSteps = getProjectSuccessMessage(config.folderName, installSuccess, envConfigured);
202
+ ux.stdout('Project created successfully!');
203
+ ux.stdout(nextSteps);
204
+ }
205
+ catch (error) {
206
+ handleRunInitError(error, state.projectPath || null, state.projectFolderCreated);
207
+ }
208
+ finally {
209
+ // Remove SIGINT handler on completion (success or error)
210
+ process.removeListener('SIGINT', sigintHandler);
211
+ }
212
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,122 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { getProjectConfig, checkDependencies, createSigintHandler } from './project_scaffold.js';
3
+ import { UserCancelledError } from '#types/errors.js';
4
+ // Mock the framework version utility
5
+ vi.mock('#utils/framework_version.js', () => ({
6
+ getFrameworkVersion: vi.fn().mockResolvedValue({
7
+ framework: '0.1.1'
8
+ })
9
+ }));
10
+ // Mock other dependencies
11
+ vi.mock('@inquirer/prompts', () => ({
12
+ input: vi.fn(),
13
+ confirm: vi.fn()
14
+ }));
15
+ vi.mock('#utils/file_system.js');
16
+ vi.mock('#utils/process.js');
17
+ vi.mock('./env_configurator.js', () => ({
18
+ configureEnvironmentVariables: vi.fn().mockResolvedValue(false)
19
+ }));
20
+ vi.mock('./template_processor.js');
21
+ vi.mock('./coding_agents.js');
22
+ vi.mock('#services/docker.js', () => ({
23
+ isDockerInstalled: vi.fn().mockReturnValue(true)
24
+ }));
25
+ vi.mock('#utils/claude.js', () => ({
26
+ isClaudeCliAvailable: vi.fn().mockReturnValue(true)
27
+ }));
28
+ vi.mock('@oclif/core', () => ({
29
+ ux: {
30
+ stdout: vi.fn(),
31
+ warn: vi.fn()
32
+ }
33
+ }));
34
+ describe('project_scaffold', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+ describe('getProjectConfig', () => {
39
+ it('should skip all prompts when folderName is provided', async () => {
40
+ const { input } = await import('@inquirer/prompts');
41
+ const config = await getProjectConfig('my-project');
42
+ expect(config.folderName).toBe('my-project');
43
+ expect(config.projectName).toBe('my-project');
44
+ expect(input).not.toHaveBeenCalled();
45
+ });
46
+ it('should auto-generate description when folderName provided', async () => {
47
+ const config = await getProjectConfig('test-folder');
48
+ expect(config.description).toBe('AI Agents & Workflows built with Output.ai for test-folder');
49
+ });
50
+ it('should prompt for project name and folder name when not provided', async () => {
51
+ const { input } = await import('@inquirer/prompts');
52
+ vi.mocked(input)
53
+ .mockResolvedValueOnce('Test Project')
54
+ .mockResolvedValueOnce('test-project');
55
+ const config = await getProjectConfig();
56
+ expect(config.projectName).toBe('Test Project');
57
+ expect(config.folderName).toBe('test-project');
58
+ expect(config.description).toBe('AI Agents & Workflows built with Output.ai for test-project');
59
+ expect(input).toHaveBeenCalledTimes(2);
60
+ });
61
+ });
62
+ describe('checkDependencies', () => {
63
+ it('should not prompt when all dependencies are available', async () => {
64
+ const { isDockerInstalled } = await import('#services/docker.js');
65
+ const { isClaudeCliAvailable } = await import('#utils/claude.js');
66
+ const { confirm } = await import('@inquirer/prompts');
67
+ vi.mocked(isDockerInstalled).mockReturnValue(true);
68
+ vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
69
+ await checkDependencies();
70
+ expect(confirm).not.toHaveBeenCalled();
71
+ });
72
+ it('should prompt user when docker is missing', async () => {
73
+ const { isDockerInstalled } = await import('#services/docker.js');
74
+ const { isClaudeCliAvailable } = await import('#utils/claude.js');
75
+ const { confirm } = await import('@inquirer/prompts');
76
+ vi.mocked(isDockerInstalled).mockReturnValue(false);
77
+ vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
78
+ vi.mocked(confirm).mockResolvedValue(true);
79
+ await checkDependencies();
80
+ expect(confirm).toHaveBeenCalledWith(expect.objectContaining({
81
+ message: expect.stringContaining('proceed')
82
+ }));
83
+ });
84
+ it('should throw UserCancelledError when user declines to proceed', async () => {
85
+ const { isDockerInstalled } = await import('#services/docker.js');
86
+ const { isClaudeCliAvailable } = await import('#utils/claude.js');
87
+ const { confirm } = await import('@inquirer/prompts');
88
+ vi.mocked(isDockerInstalled).mockReturnValue(false);
89
+ vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
90
+ vi.mocked(confirm).mockResolvedValue(false);
91
+ await expect(checkDependencies()).rejects.toThrow(UserCancelledError);
92
+ });
93
+ });
94
+ describe('SIGINT handler', () => {
95
+ it('should show cleanup message when project folder was created', async () => {
96
+ const { ux } = await import('@oclif/core');
97
+ const handler = createSigintHandler('/test/project', true);
98
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
99
+ handler();
100
+ expect(ux.warn).toHaveBeenCalledWith(expect.stringContaining('/test/project'));
101
+ expect(ux.warn).toHaveBeenCalledWith(expect.stringContaining('rm -rf'));
102
+ expect(exitSpy).toHaveBeenCalledWith(130);
103
+ exitSpy.mockRestore();
104
+ });
105
+ it('should exit immediately without warning when folder not created', async () => {
106
+ const { ux } = await import('@oclif/core');
107
+ const handler = createSigintHandler('/nonexistent', false);
108
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
109
+ handler();
110
+ expect(ux.warn).not.toHaveBeenCalled();
111
+ expect(exitSpy).toHaveBeenCalledWith(130);
112
+ exitSpy.mockRestore();
113
+ });
114
+ it('should exit with code 130 (SIGINT convention)', async () => {
115
+ const handler = createSigintHandler('/test/project', true);
116
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
117
+ handler();
118
+ expect(exitSpy).toHaveBeenCalledWith(130);
119
+ exitSpy.mockRestore();
120
+ });
121
+ });
122
+ });
@@ -0,0 +1,12 @@
1
+ import type { TraceData } from '#types/trace.js';
2
+ interface RemoteTraceInfo {
3
+ key: string;
4
+ lastModified?: Date;
5
+ size?: number;
6
+ }
7
+ export declare function listRemoteTraces(workflowName: string, options?: {
8
+ limit?: number;
9
+ since?: Date;
10
+ }): Promise<RemoteTraceInfo[]>;
11
+ export declare function downloadRemoteTrace(key: string): Promise<TraceData>;
12
+ export {};
@@ -0,0 +1,57 @@
1
+ import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';
2
+ import { config } from '#config.js';
3
+ function getS3Config() {
4
+ const { bucket, region, accessKeyId, secretAccessKey } = config.s3;
5
+ if (!bucket || !region || !accessKeyId || !secretAccessKey) {
6
+ throw new Error('Missing S3 configuration. Set OUTPUT_TRACE_REMOTE_S3_BUCKET, OUTPUT_AWS_REGION, ' +
7
+ 'OUTPUT_AWS_ACCESS_KEY_ID, and OUTPUT_AWS_SECRET_ACCESS_KEY environment variables.');
8
+ }
9
+ return { bucket, region, accessKeyId, secretAccessKey };
10
+ }
11
+ function createS3Client(s3Config) {
12
+ return new S3Client({
13
+ region: s3Config.region,
14
+ credentials: {
15
+ accessKeyId: s3Config.accessKeyId,
16
+ secretAccessKey: s3Config.secretAccessKey
17
+ }
18
+ });
19
+ }
20
+ export async function listRemoteTraces(workflowName, options = {}) {
21
+ const s3Config = getS3Config();
22
+ const client = createS3Client(s3Config);
23
+ const limit = options.limit ?? 20;
24
+ const command = new ListObjectsV2Command({
25
+ Bucket: s3Config.bucket,
26
+ Prefix: `${workflowName}/`,
27
+ MaxKeys: limit
28
+ });
29
+ const response = await client.send(command);
30
+ const contents = response.Contents ?? [];
31
+ return contents
32
+ .filter(obj => {
33
+ if (!obj.Key) {
34
+ return false;
35
+ }
36
+ if (options.since && obj.LastModified && obj.LastModified < options.since) {
37
+ return false;
38
+ }
39
+ return true;
40
+ })
41
+ .map(obj => ({
42
+ key: obj.Key,
43
+ lastModified: obj.LastModified,
44
+ size: obj.Size
45
+ }));
46
+ }
47
+ export async function downloadRemoteTrace(key) {
48
+ const s3Config = getS3Config();
49
+ const client = createS3Client(s3Config);
50
+ const command = new GetObjectCommand({ Bucket: s3Config.bucket, Key: key });
51
+ const response = await client.send(command);
52
+ const body = await response.Body?.transformToString();
53
+ if (!body) {
54
+ throw new Error(`Empty response for S3 object: ${key}`);
55
+ }
56
+ return JSON.parse(body);
57
+ }
@@ -0,0 +1,14 @@
1
+ import type { TemplateFile } from '#types/generator.js';
2
+ /**
3
+ * Get list of template files from a directory
4
+ * Automatically discovers all .template files (including in subdirectories) and derives output names
5
+ */
6
+ export declare function getTemplateFiles(templatesDir: string): Promise<TemplateFile[]>;
7
+ /**
8
+ * Process a single template file
9
+ */
10
+ export declare function processTemplateFile(templateFile: TemplateFile, targetDir: string, variables: Record<string, string>): Promise<void>;
11
+ /**
12
+ * Process all template files
13
+ */
14
+ export declare function processAllTemplates(templateFiles: TemplateFile[], targetDir: string, variables: Record<string, string>): Promise<string[]>;
@@ -0,0 +1,57 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { processTemplate } from '#utils/template.js';
4
+ const TEMPLATE_EXTENSION = '.template';
5
+ const isTemplateFile = (file) => file.endsWith(TEMPLATE_EXTENSION);
6
+ const fileToTemplateFile = (file, templatesDir, relativePath = '') => {
7
+ const fullPath = relativePath ? path.join(relativePath, file) : file;
8
+ return {
9
+ name: file,
10
+ path: path.join(templatesDir, relativePath, file),
11
+ outputName: fullPath.replace(TEMPLATE_EXTENSION, '')
12
+ };
13
+ };
14
+ const processEntry = async (entry, templatesDir, relativePath, getFilesRecursively) => {
15
+ if (entry.isDirectory()) {
16
+ const subDir = relativePath ? path.join(relativePath, entry.name) : entry.name;
17
+ return getFilesRecursively(templatesDir, subDir);
18
+ }
19
+ if (entry.isFile() && isTemplateFile(entry.name)) {
20
+ return [fileToTemplateFile(entry.name, templatesDir, relativePath)];
21
+ }
22
+ return [];
23
+ };
24
+ async function getTemplateFilesRecursive(templatesDir, relativePath = '') {
25
+ const fullPath = relativePath ? path.join(templatesDir, relativePath) : templatesDir;
26
+ const entries = await fs.readdir(fullPath, { withFileTypes: true });
27
+ const results = await Promise.all(entries.map(entry => processEntry(entry, templatesDir, relativePath, getTemplateFilesRecursive)));
28
+ return results.flatMap(x => x);
29
+ }
30
+ /**
31
+ * Get list of template files from a directory
32
+ * Automatically discovers all .template files (including in subdirectories) and derives output names
33
+ */
34
+ export async function getTemplateFiles(templatesDir) {
35
+ return getTemplateFilesRecursive(templatesDir);
36
+ }
37
+ /**
38
+ * Process a single template file
39
+ */
40
+ export async function processTemplateFile(templateFile, targetDir, variables) {
41
+ const templateContent = await fs.readFile(templateFile.path, 'utf-8');
42
+ const processedContent = processTemplate(templateContent, variables);
43
+ const outputPath = path.join(targetDir, templateFile.outputName);
44
+ // Create parent directories if they don't exist
45
+ const outputDir = path.dirname(outputPath);
46
+ await fs.mkdir(outputDir, { recursive: true });
47
+ await fs.writeFile(outputPath, processedContent, 'utf-8');
48
+ }
49
+ /**
50
+ * Process all template files
51
+ */
52
+ export async function processAllTemplates(templateFiles, targetDir, variables) {
53
+ return Promise.all(templateFiles.map(async (templateFile) => {
54
+ await processTemplateFile(templateFile, targetDir, variables);
55
+ return templateFile.outputName;
56
+ }));
57
+ }
@@ -0,0 +1,16 @@
1
+ import type { TraceData } from '#types/trace.js';
2
+ export type { TraceData };
3
+ export interface TraceLocation {
4
+ path: string;
5
+ isRemote: boolean;
6
+ }
7
+ export interface TraceResult {
8
+ data: TraceData;
9
+ location: TraceLocation;
10
+ }
11
+ /**
12
+ * Get trace data from workflow ID using the API
13
+ * The API handles S3 fetching - CLI only needs to read local files when necessary
14
+ * @returns Both the trace data and the location it was fetched from
15
+ */
16
+ export declare function getTrace(workflowId: string): Promise<TraceResult>;
@@ -0,0 +1,57 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { getWorkflowIdTraceLog } from '#api/generated/api.js';
3
+ import { getErrorCode } from '#utils/error_utils.js';
4
+ /**
5
+ * Read and parse trace file from local path
6
+ */
7
+ async function readLocalTraceFile(path) {
8
+ try {
9
+ const content = await readFile(path, 'utf-8');
10
+ return JSON.parse(content);
11
+ }
12
+ catch (error) {
13
+ if (getErrorCode(error) === 'ENOENT') {
14
+ throw new Error(`Trace file not found at path: ${path}`);
15
+ }
16
+ if (error instanceof SyntaxError) {
17
+ throw new Error(`Invalid JSON in trace file: ${path}`);
18
+ }
19
+ throw error;
20
+ }
21
+ }
22
+ /**
23
+ * Get trace data from workflow ID using the API
24
+ * The API handles S3 fetching - CLI only needs to read local files when necessary
25
+ * @returns Both the trace data and the location it was fetched from
26
+ */
27
+ export async function getTrace(workflowId) {
28
+ const response = await getWorkflowIdTraceLog(workflowId);
29
+ if (response.status === 404) {
30
+ throw new Error(`Workflow not found or no trace available: ${workflowId}`);
31
+ }
32
+ if (response.status === 500) {
33
+ const errorData = response.data;
34
+ const errorMessage = errorData?.error || 'Failed to fetch trace from API';
35
+ throw new Error(`API error (500): ${errorMessage}`);
36
+ }
37
+ if (response.status !== 200) {
38
+ const errorResponse = response;
39
+ throw new Error(`Unexpected API response status: ${errorResponse.status}`);
40
+ }
41
+ const data = response.data;
42
+ if (data.source === 'remote') {
43
+ return {
44
+ data: data.data,
45
+ location: { path: 'remote', isRemote: true }
46
+ };
47
+ }
48
+ if (data.source === 'local') {
49
+ const localPath = data.localPath;
50
+ const traceData = await readLocalTraceFile(localPath);
51
+ return {
52
+ data: traceData,
53
+ location: { path: localPath, isRemote: false }
54
+ };
55
+ }
56
+ throw new Error('Invalid trace log response format');
57
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
2
+ const mockGetWorkflowIdTraceLog = vi.fn();
3
+ vi.mock('#api/generated/api.js', () => ({
4
+ getWorkflowIdTraceLog: (...args) => mockGetWorkflowIdTraceLog(...args)
5
+ }));
6
+ const mockReadFile = vi.fn();
7
+ vi.mock('node:fs/promises', () => ({
8
+ readFile: (...args) => mockReadFile(...args)
9
+ }));
10
+ describe('trace_reader', () => {
11
+ beforeEach(() => {
12
+ mockGetWorkflowIdTraceLog.mockReset();
13
+ mockReadFile.mockReset();
14
+ });
15
+ afterEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+ describe('getTrace', () => {
19
+ it('should return trace data directly for remote source', async () => {
20
+ const mockTraceData = {
21
+ root: { workflowName: 'test', workflowId: 'wf-123', startTime: Date.now() },
22
+ children: []
23
+ };
24
+ mockGetWorkflowIdTraceLog.mockResolvedValue({
25
+ status: 200,
26
+ data: {
27
+ source: 'remote',
28
+ data: mockTraceData
29
+ }
30
+ });
31
+ const { getTrace } = await import('./trace_reader.js');
32
+ const result = await getTrace('wf-123');
33
+ expect(result.data).toEqual(mockTraceData);
34
+ expect(result.location.isRemote).toBe(true);
35
+ expect(mockReadFile).not.toHaveBeenCalled();
36
+ });
37
+ it('should read local file for local source', async () => {
38
+ const mockTraceData = {
39
+ root: { workflowName: 'test', workflowId: 'wf-123', startTime: Date.now() },
40
+ children: []
41
+ };
42
+ mockGetWorkflowIdTraceLog.mockResolvedValue({
43
+ status: 200,
44
+ data: {
45
+ source: 'local',
46
+ localPath: '/path/to/trace.json'
47
+ }
48
+ });
49
+ mockReadFile.mockResolvedValue(JSON.stringify(mockTraceData));
50
+ const { getTrace } = await import('./trace_reader.js');
51
+ const result = await getTrace('wf-123');
52
+ expect(result.data).toEqual(mockTraceData);
53
+ expect(result.location.isRemote).toBe(false);
54
+ expect(result.location.path).toBe('/path/to/trace.json');
55
+ expect(mockReadFile).toHaveBeenCalledWith('/path/to/trace.json', 'utf-8');
56
+ });
57
+ it('should throw error when API returns 404', async () => {
58
+ mockGetWorkflowIdTraceLog.mockResolvedValue({
59
+ status: 404,
60
+ data: { error: 'Not found' }
61
+ });
62
+ const { getTrace } = await import('./trace_reader.js');
63
+ await expect(getTrace('wf-123'))
64
+ .rejects
65
+ .toThrow('Workflow not found or no trace available: wf-123');
66
+ });
67
+ it('should throw error when API returns 500', async () => {
68
+ mockGetWorkflowIdTraceLog.mockResolvedValue({
69
+ status: 500,
70
+ data: { error: 'S3 access denied' }
71
+ });
72
+ const { getTrace } = await import('./trace_reader.js');
73
+ await expect(getTrace('wf-123'))
74
+ .rejects
75
+ .toThrow('S3 access denied');
76
+ });
77
+ });
78
+ });
@@ -0,0 +1,6 @@
1
+ export interface VersionCheckResult {
2
+ updateAvailable: boolean;
3
+ currentVersion: string;
4
+ latestVersion: string;
5
+ }
6
+ export declare function checkForUpdate(currentVersion: string, cacheDir?: string): Promise<VersionCheckResult>;