sofia-cli 0.1.2 → 0.1.4

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 (136) hide show
  1. package/README.md +42 -20
  2. package/dist/infra/deploy.sh +193 -0
  3. package/dist/infra/gather-env.sh +211 -0
  4. package/dist/infra/infra/deploy.sh +193 -0
  5. package/dist/infra/infra/gather-env.sh +211 -0
  6. package/dist/infra/infra/main.bicep +90 -0
  7. package/dist/infra/infra/main.bicepparam +18 -0
  8. package/dist/infra/infra/resources.bicep +134 -0
  9. package/dist/infra/infra/teardown.sh +114 -0
  10. package/dist/infra/main.bicep +90 -0
  11. package/dist/infra/main.bicepparam +18 -0
  12. package/dist/infra/resources.bicep +134 -0
  13. package/dist/infra/teardown.sh +114 -0
  14. package/dist/src/cli/developCommand.js +0 -2
  15. package/dist/src/cli/index.js +8 -1
  16. package/dist/src/cli/workshopCommand.js +1 -1
  17. package/dist/src/develop/index.js +1 -1
  18. package/dist/src/develop/pocUtils.js +228 -0
  19. package/dist/src/develop/ralphLoop.js +8 -27
  20. package/dist/src/shared/data/cards.json +655 -670
  21. package/docs/architecture.md +2 -1
  22. package/package.json +5 -3
  23. package/src/cli/developCommand.ts +1 -3
  24. package/src/cli/index.ts +11 -1
  25. package/src/cli/workshopCommand.ts +21 -17
  26. package/src/develop/dynamicScaffolder.ts +36 -30
  27. package/src/develop/index.ts +13 -2
  28. package/src/develop/pocUtils.ts +296 -0
  29. package/src/develop/ralphLoop.ts +8 -28
  30. package/src/develop/templateRegistry.ts +19 -18
  31. package/src/shared/data/cards.json +655 -670
  32. package/tests/e2e/developE2e.spec.ts +3 -61
  33. package/tests/e2e/developFailureE2e.spec.ts +34 -38
  34. package/tests/integration/pocGithubMcp.spec.ts +29 -39
  35. package/tests/integration/pocLocalFallback.spec.ts +29 -39
  36. package/tests/integration/ralphLoopFlow.spec.ts +46 -66
  37. package/tests/integration/ralphLoopPartial.spec.ts +30 -37
  38. package/tests/unit/develop/githubMcpAdapter.spec.ts +0 -134
  39. package/tests/unit/develop/outputValidator.spec.ts +45 -21
  40. package/tests/unit/develop/ralphLoop.spec.ts +58 -94
  41. package/tsconfig.json +2 -1
  42. package/vitest.workspace.ts +5 -0
  43. package/dist/src/develop/pocScaffolder.js +0 -542
  44. package/dist/tests/e2e/developE2e.spec.js +0 -126
  45. package/dist/tests/e2e/developFailureE2e.spec.js +0 -247
  46. package/dist/tests/e2e/developPty.spec.js +0 -75
  47. package/dist/tests/e2e/discoveryWebSearchRelevance.spec.js +0 -84
  48. package/dist/tests/e2e/harness.spec.js +0 -83
  49. package/dist/tests/e2e/mcpLive.spec.js +0 -120
  50. package/dist/tests/e2e/newSession.e2e.spec.js +0 -177
  51. package/dist/tests/e2e/ralphLoopEnrichmentComparison.spec.js +0 -62
  52. package/dist/tests/e2e/workiqEnrichment.spec.js +0 -56
  53. package/dist/tests/e2e/zavaSimulation.spec.js +0 -452
  54. package/dist/tests/fixtures/test-fixture-project/src/add.js +0 -3
  55. package/dist/tests/fixtures/test-fixture-project/tests/failing.test.js +0 -6
  56. package/dist/tests/fixtures/test-fixture-project/tests/hanging.test.js +0 -8
  57. package/dist/tests/fixtures/test-fixture-project/tests/passing.test.js +0 -10
  58. package/dist/tests/fixtures/test-fixture-project/vitest.config.js +0 -6
  59. package/dist/tests/integration/autoStartConversation.spec.js +0 -138
  60. package/dist/tests/integration/defaultCommand.spec.js +0 -147
  61. package/dist/tests/integration/directCommandNonTty.spec.js +0 -224
  62. package/dist/tests/integration/directCommandTty.spec.js +0 -151
  63. package/dist/tests/integration/discoveryEnrichmentFlow.spec.js +0 -175
  64. package/dist/tests/integration/exportArtifacts.spec.js +0 -202
  65. package/dist/tests/integration/exportFallbackFlow.spec.js +0 -99
  66. package/dist/tests/integration/mcpDegradationFlow.spec.js +0 -190
  67. package/dist/tests/integration/mcpTransportFlow.spec.js +0 -139
  68. package/dist/tests/integration/newSessionFlow.spec.js +0 -343
  69. package/dist/tests/integration/pocGithubMcp.spec.js +0 -186
  70. package/dist/tests/integration/pocLocalFallback.spec.js +0 -171
  71. package/dist/tests/integration/pocScaffold.spec.js +0 -163
  72. package/dist/tests/integration/ralphLoopFlow.spec.js +0 -359
  73. package/dist/tests/integration/ralphLoopPartial.spec.js +0 -368
  74. package/dist/tests/integration/resumeAndBacktrack.spec.js +0 -247
  75. package/dist/tests/integration/spinnerLifecycle.spec.js +0 -220
  76. package/dist/tests/integration/summarizationFlow.spec.js +0 -115
  77. package/dist/tests/integration/testRunnerReal.spec.js +0 -52
  78. package/dist/tests/integration/webSearchAgent.spec.js +0 -128
  79. package/dist/tests/live/copilotSdkLive.spec.js +0 -107
  80. package/dist/tests/live/zavaFullWorkshop.spec.js +0 -392
  81. package/dist/tests/setup/loadEnv.js +0 -3
  82. package/dist/tests/unit/cli/developCommand.spec.js +0 -567
  83. package/dist/tests/unit/cli/directCommands.spec.js +0 -279
  84. package/dist/tests/unit/cli/envLoader.spec.js +0 -58
  85. package/dist/tests/unit/cli/ioContext.spec.js +0 -119
  86. package/dist/tests/unit/cli/preflight.spec.js +0 -108
  87. package/dist/tests/unit/cli/statusCommand.spec.js +0 -111
  88. package/dist/tests/unit/cli/workshopClientFallback.spec.js +0 -80
  89. package/dist/tests/unit/cli/workshopCommand.spec.js +0 -328
  90. package/dist/tests/unit/config/vitestEnvSetup.spec.js +0 -13
  91. package/dist/tests/unit/develop/checkpointState.spec.js +0 -315
  92. package/dist/tests/unit/develop/codeGenerator.spec.js +0 -355
  93. package/dist/tests/unit/develop/githubMcpAdapter.spec.js +0 -231
  94. package/dist/tests/unit/develop/mcpContextEnricher.spec.js +0 -433
  95. package/dist/tests/unit/develop/outputValidator.spec.js +0 -119
  96. package/dist/tests/unit/develop/pocScaffolder.spec.js +0 -353
  97. package/dist/tests/unit/develop/ralphLoop.spec.js +0 -1248
  98. package/dist/tests/unit/develop/templateRegistry.spec.js +0 -85
  99. package/dist/tests/unit/develop/testRunner.spec.js +0 -249
  100. package/dist/tests/unit/infraBicep.spec.js +0 -92
  101. package/dist/tests/unit/infraDeploy.spec.js +0 -82
  102. package/dist/tests/unit/infraTeardown.spec.js +0 -63
  103. package/dist/tests/unit/logging/logger.spec.js +0 -43
  104. package/dist/tests/unit/loop/conversationLoop.spec.js +0 -592
  105. package/dist/tests/unit/loop/phaseSummarizer.spec.js +0 -141
  106. package/dist/tests/unit/loop/streamingMarkdown.spec.js +0 -147
  107. package/dist/tests/unit/mcp/mcpManager.spec.js +0 -279
  108. package/dist/tests/unit/mcp/mcpTransport.spec.js +0 -529
  109. package/dist/tests/unit/mcp/retryPolicy.spec.js +0 -218
  110. package/dist/tests/unit/mcp/timeoutValidation.spec.js +0 -46
  111. package/dist/tests/unit/mcp/webSearch.spec.js +0 -567
  112. package/dist/tests/unit/phases/contextSummarizer.spec.js +0 -140
  113. package/dist/tests/unit/phases/discoveryEnricher.repeatCalls.spec.js +0 -93
  114. package/dist/tests/unit/phases/discoveryEnricher.spec.js +0 -411
  115. package/dist/tests/unit/phases/phaseExtractors.spec.js +0 -352
  116. package/dist/tests/unit/phases/phaseHandlers.spec.js +0 -425
  117. package/dist/tests/unit/prompts/promptLoader.spec.js +0 -118
  118. package/dist/tests/unit/schemas/pocSchemas.spec.js +0 -412
  119. package/dist/tests/unit/schemas/session.spec.js +0 -257
  120. package/dist/tests/unit/sessions/exportPaths.spec.js +0 -31
  121. package/dist/tests/unit/sessions/exportWriter.spec.js +0 -655
  122. package/dist/tests/unit/sessions/sessionManager.spec.js +0 -151
  123. package/dist/tests/unit/sessions/sessionStore.spec.js +0 -116
  124. package/dist/tests/unit/shared/activitySpinner.spec.js +0 -175
  125. package/dist/tests/unit/shared/cardsLoader.spec.js +0 -76
  126. package/dist/tests/unit/shared/copilotClient.spec.js +0 -155
  127. package/dist/tests/unit/shared/errorClassifier.spec.js +0 -131
  128. package/dist/tests/unit/shared/events.spec.js +0 -55
  129. package/dist/tests/unit/shared/markdownRenderer.spec.js +0 -35
  130. package/dist/tests/unit/shared/markdownRendererChunks.spec.js +0 -70
  131. package/dist/tests/unit/shared/tableRenderer.spec.js +0 -34
  132. package/dist/vitest.config.js +0 -14
  133. package/dist/vitest.live.config.js +0 -18
  134. package/src/develop/pocScaffolder.ts +0 -646
  135. package/tests/integration/pocScaffold.spec.ts +0 -220
  136. package/tests/unit/develop/pocScaffolder.spec.ts +0 -451
@@ -1,542 +0,0 @@
1
- /**
2
- * PoC Scaffolder.
3
- *
4
- * Generates the initial proof-of-concept project structure from a
5
- * `node-ts-vitest` template. Creates all required files for a working
6
- * TypeScript + Vitest project.
7
- *
8
- * Contract: specs/002-poc-generation/contracts/poc-output.md
9
- */
10
- import { writeFile, mkdir, access } from 'node:fs/promises';
11
- import { join, dirname } from 'node:path';
12
- import { execSync } from 'node:child_process';
13
- // ── Helpers ──────────────────────────────────────────────────────────────────
14
- /**
15
- * Convert an idea title to a kebab-case project name.
16
- */
17
- export function toKebabCase(title) {
18
- return title
19
- .toLowerCase()
20
- .replace(/[^a-z0-9]+/g, '-')
21
- .replace(/^-+|-+$/g, '')
22
- .substring(0, 64);
23
- }
24
- /**
25
- * Check if a file exists.
26
- */
27
- async function fileExists(filePath) {
28
- try {
29
- await access(filePath);
30
- return true;
31
- }
32
- catch {
33
- return false;
34
- }
35
- }
36
- // ── Template definitions ─────────────────────────────────────────────────────
37
- const NODE_TS_VITEST_TEMPLATE = [
38
- {
39
- path: '.gitignore',
40
- skipIfExists: false,
41
- content: `node_modules/
42
- dist/
43
- coverage/
44
- *.tsbuildinfo
45
- .env
46
- `,
47
- },
48
- {
49
- path: 'package.json',
50
- skipIfExists: true,
51
- content: (ctx) => JSON.stringify({
52
- name: ctx.projectName,
53
- version: '0.1.0',
54
- private: true,
55
- type: 'module',
56
- scripts: {
57
- build: 'tsc',
58
- start: 'node dist/index.js',
59
- test: 'vitest run',
60
- },
61
- dependencies: {},
62
- devDependencies: {
63
- typescript: '^5.0.0',
64
- vitest: '^3.0.0',
65
- '@types/node': '^20.0.0',
66
- },
67
- }, null, 2) + '\n',
68
- },
69
- {
70
- path: 'tsconfig.json',
71
- skipIfExists: true,
72
- content: JSON.stringify({
73
- compilerOptions: {
74
- target: 'ES2022',
75
- module: 'Node16',
76
- moduleResolution: 'Node16',
77
- strict: true,
78
- outDir: 'dist',
79
- rootDir: 'src',
80
- declaration: true,
81
- esModuleInterop: true,
82
- skipLibCheck: true,
83
- },
84
- include: ['src'],
85
- }, null, 2) + '\n',
86
- },
87
- {
88
- path: 'README.md',
89
- skipIfExists: true,
90
- content: (ctx) => `# ${ctx.ideaTitle}
91
-
92
- ${ctx.ideaDescription}
93
-
94
- ## Generated by
95
-
96
- sofIA — AI Discovery Workshop CLI
97
- Session: \`${ctx.sessionId}\`
98
- Generated: ${new Date().toISOString()}
99
-
100
- ## Prerequisites
101
-
102
- - Node.js 20+
103
- - npm 9+
104
-
105
- ## Quick Start
106
-
107
- \`\`\`bash
108
- npm install
109
- npm test
110
- npm start
111
- \`\`\`
112
-
113
- ## How It Works
114
-
115
- ${ctx.planSummary}
116
-
117
- ## Technology Stack
118
-
119
- - **Language**: ${ctx.techStack.language}
120
- - **Runtime**: ${ctx.techStack.runtime}
121
- - **Test Runner**: ${ctx.techStack.testRunner}
122
- ${ctx.techStack.framework ? `- **Framework**: ${ctx.techStack.framework}\n` : ''}
123
- ## Workshop Context
124
-
125
- This project was selected and designed through a structured AI Discovery Workshop.
126
- See [WORKSHOP.md](WORKSHOP.md) for the full decision history, including business
127
- context, ideation, evaluation, selection rationale, and implementation plan.
128
-
129
- Detailed per-phase documentation is in [\`docs/workshop/\`](docs/workshop/).
130
- `,
131
- },
132
- {
133
- path: 'src/index.ts',
134
- skipIfExists: true,
135
- content: (ctx) => `/**
136
- * ${ctx.ideaTitle}
137
- *
138
- * ${ctx.ideaDescription}
139
- *
140
- * Generated by sofIA (session: ${ctx.sessionId})
141
- */
142
-
143
- /**
144
- * Main entry point for the ${ctx.projectName} PoC.
145
- *
146
- * TODO: Implement the core functionality described in the plan:
147
- * ${ctx.planSummary.substring(0, 300)}
148
- *
149
- * The test file (tests/index.test.ts) describes the expected behavior.
150
- * Implement the required functions to make the tests pass.
151
- */
152
-
153
- export async function main(input?: unknown): Promise<{ status: string; processed?: boolean }> {
154
- // TODO: Implement based on the plan and test requirements
155
- throw new Error('Not implemented yet. Ralph loop will implement this based on tests.');
156
- }
157
-
158
- // Run if called directly
159
- const isMain = process.argv[1]?.endsWith('index.js') || process.argv[1]?.endsWith('index.ts');
160
- if (isMain) {
161
- main()
162
- .then((result) => console.log('PoC result:', result))
163
- .catch((err) => console.error('PoC error:', err));
164
- }
165
- `,
166
- },
167
- {
168
- path: 'tests/index.test.ts',
169
- skipIfExists: true,
170
- content: (ctx) => `/**
171
- * Tests for ${ctx.ideaTitle} PoC.
172
- *
173
- * These tests describe the expected behavior of the implementation.
174
- * They are intentionally failing initially — the Ralph loop will
175
- * iteratively fix the implementation until they pass.
176
- *
177
- * Generated by sofIA (session: ${ctx.sessionId})
178
- * Plan Summary: ${ctx.planSummary.substring(0, 200)}
179
- */
180
- import { describe, it, expect } from 'vitest';
181
-
182
- describe('${ctx.projectName} PoC', () => {
183
- describe('Core Module Exports', () => {
184
- it('should export the main entry point', async () => {
185
- // This test will fail until src/index.ts exports the required functions
186
- const module = await import('../src/index.js');
187
- expect(module).toBeDefined();
188
- expect(typeof module.main).toBe('function');
189
- });
190
-
191
- it('should export core functionality modules', async () => {
192
- // Uncomment and modify based on your architecture:
193
- // const module = await import('../src/index.js');
194
- // expect(module.processData).toBeDefined();
195
- // expect(module.analyzeResults).toBeDefined();
196
- // expect(module.generateOutput).toBeDefined();
197
-
198
- // Placeholder: Replace with actual module exports from your plan
199
- const module = await import('../src/index.js');
200
- expect(module.main).toBeDefined();
201
- });
202
- });
203
-
204
- describe('Main Functionality', () => {
205
- it('should execute the main workflow', async () => {
206
- // This test describes the core PoC workflow from the plan
207
- const { main } = await import('../src/index.js');
208
-
209
- // TODO: Ralph loop will implement this based on the plan summary
210
- const result = await main();
211
-
212
- expect(result).toBeDefined();
213
- expect(result).toHaveProperty('status');
214
- expect(result.status).toBe('success');
215
- });
216
-
217
- it('should handle input data correctly', async () => {
218
- // Test data processing as described in the plan
219
- const { main } = await import('../src/index.js');
220
-
221
- const testInput = { data: 'test' };
222
- const result = await main(testInput);
223
-
224
- expect(result).toBeDefined();
225
- expect(result).toHaveProperty('processed');
226
- });
227
- });
228
-
229
- describe('Error Handling', () => {
230
- it('should handle invalid input gracefully', async () => {
231
- const { main } = await import('../src/index.js');
232
-
233
- // Test error cases
234
- await expect(async () => {
235
- await main(null);
236
- }).rejects.toThrow();
237
- });
238
- });
239
- });
240
- `,
241
- },
242
- {
243
- path: '.sofia-metadata.json',
244
- skipIfExists: false,
245
- content: (ctx) => JSON.stringify({
246
- sessionId: ctx.sessionId,
247
- featureSpec: '002-poc-generation',
248
- generatedAt: new Date().toISOString(),
249
- ideaTitle: ctx.ideaTitle,
250
- totalIterations: 0,
251
- finalStatus: null,
252
- terminationReason: null,
253
- techStack: {
254
- language: ctx.techStack.language.toLowerCase(),
255
- runtime: ctx.techStack.runtime.toLowerCase(),
256
- testRunner: ctx.techStack.testRunner,
257
- },
258
- }, null, 2) + '\n',
259
- },
260
- ];
261
- // ── PocScaffolder ────────────────────────────────────────────────────────────
262
- /**
263
- * Generates a PoC project scaffold from the `node-ts-vitest` template.
264
- *
265
- * Accepts a ScaffoldContext derived from a workshop session and creates
266
- * all required files. Files that already exist and have `skipIfExists: true`
267
- * are skipped to allow resuming a partial scaffold.
268
- */
269
- export class PocScaffolder {
270
- template;
271
- templateId;
272
- constructor(templateOrFiles) {
273
- if (templateOrFiles && 'files' in templateOrFiles && 'id' in templateOrFiles) {
274
- // TemplateEntry
275
- const entry = templateOrFiles;
276
- this.template = entry.files;
277
- this.templateId = entry.id;
278
- }
279
- else {
280
- this.template = templateOrFiles ?? NODE_TS_VITEST_TEMPLATE;
281
- this.templateId = undefined;
282
- }
283
- }
284
- /**
285
- * Build a ScaffoldContext from a workshop session.
286
- */
287
- static buildContext(session, outputDir, templateEntry) {
288
- const idea = session.ideas?.find((i) => i.id === session.selection?.ideaId);
289
- const ideaTitle = idea?.title ?? 'AI PoC';
290
- const ideaDescription = idea?.description ?? 'A proof-of-concept AI application.';
291
- const planSummary = session.plan?.architectureNotes
292
- ? session.plan.architectureNotes
293
- : (session.plan?.milestones?.map((m) => m.title).join(', ') ?? 'See plan for details');
294
- const techStack = templateEntry?.techStack
295
- ? { ...templateEntry.techStack }
296
- : {
297
- language: 'TypeScript',
298
- runtime: 'Node.js 20',
299
- testRunner: 'npm test',
300
- buildCommand: 'npm run build',
301
- framework: undefined,
302
- };
303
- // Infer framework from plan if present
304
- if (session.plan?.architectureNotes) {
305
- const notes = session.plan.architectureNotes.toLowerCase();
306
- if (notes.includes('express'))
307
- techStack.framework = 'Express';
308
- else if (notes.includes('fastapi'))
309
- techStack.framework = 'FastAPI';
310
- else if (notes.includes('next'))
311
- techStack.framework = 'Next.js';
312
- }
313
- return {
314
- projectName: toKebabCase(ideaTitle),
315
- ideaTitle,
316
- ideaDescription,
317
- techStack,
318
- planSummary,
319
- sessionId: session.sessionId,
320
- outputDir,
321
- };
322
- }
323
- /**
324
- * Scaffold the PoC project into the output directory.
325
- *
326
- * Creates all template files, respecting skipIfExists.
327
- * Returns lists of created and skipped files.
328
- */
329
- async scaffold(context) {
330
- await mkdir(context.outputDir, { recursive: true });
331
- const createdFiles = [];
332
- const skippedFiles = [];
333
- for (const templateFile of this.template) {
334
- const fullPath = join(context.outputDir, templateFile.path);
335
- const shouldSkip = templateFile.skipIfExists !== false;
336
- if (shouldSkip && (await fileExists(fullPath))) {
337
- skippedFiles.push(templateFile.path);
338
- continue;
339
- }
340
- // Ensure parent directory exists
341
- const parentDir = dirname(fullPath);
342
- await mkdir(parentDir, { recursive: true });
343
- // Generate content
344
- const content = typeof templateFile.content === 'function'
345
- ? templateFile.content(context)
346
- : templateFile.content;
347
- await writeFile(fullPath, content, 'utf-8');
348
- createdFiles.push(templateFile.path);
349
- }
350
- // FR-022: Add templateId to metadata if available
351
- const metadataPath = join(context.outputDir, '.sofia-metadata.json');
352
- if (await fileExists(metadataPath)) {
353
- try {
354
- const { readFile: rf } = await import('node:fs/promises');
355
- const raw = await rf(metadataPath, 'utf-8');
356
- const metadata = JSON.parse(raw);
357
- if (this.templateId) {
358
- metadata.templateId = this.templateId;
359
- }
360
- await writeFile(metadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf-8');
361
- }
362
- catch {
363
- // Ignore metadata update errors
364
- }
365
- }
366
- return { createdFiles, skippedFiles, context };
367
- }
368
- /**
369
- * Scan scaffold files for TODO markers and update .sofia-metadata.json.
370
- * Called after scaffolding to track initial TODO count (FR-022).
371
- */
372
- static async scanAndRecordTodos(outputDir) {
373
- const { readFile: rf, readdir, stat } = await import('node:fs/promises');
374
- const markers = [];
375
- async function scanDir(dir, base) {
376
- let entries;
377
- try {
378
- entries = await readdir(dir);
379
- }
380
- catch {
381
- return;
382
- }
383
- for (const entry of entries) {
384
- if (entry === 'node_modules' || entry === '.git' || entry === 'dist')
385
- continue;
386
- const full = join(dir, entry);
387
- const rel = base ? `${base}/${entry}` : entry;
388
- let s;
389
- try {
390
- s = await stat(full);
391
- }
392
- catch {
393
- continue;
394
- }
395
- if (s.isDirectory()) {
396
- await scanDir(full, rel);
397
- }
398
- else if (s.isFile()) {
399
- try {
400
- const content = await rf(full, 'utf-8');
401
- const lines = content.split('\n');
402
- for (let i = 0; i < lines.length; i++) {
403
- if (lines[i].includes('TODO:')) {
404
- markers.push(`${rel}:${i + 1}: ${lines[i].trim()}`);
405
- }
406
- }
407
- }
408
- catch {
409
- // skip binary or unreadable files
410
- }
411
- }
412
- }
413
- }
414
- await scanDir(outputDir, '');
415
- const todos = {
416
- totalInitial: markers.length,
417
- remaining: markers.length,
418
- markers,
419
- };
420
- // Update .sofia-metadata.json with TODO info
421
- const metadataPath = join(outputDir, '.sofia-metadata.json');
422
- try {
423
- const raw = await rf(metadataPath, 'utf-8');
424
- const metadata = JSON.parse(raw);
425
- metadata.todos = todos;
426
- await writeFile(metadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf-8');
427
- }
428
- catch {
429
- // Metadata file may not exist yet
430
- }
431
- return todos;
432
- }
433
- /**
434
- * Get the list of file paths in the template.
435
- */
436
- getTemplateFiles() {
437
- return this.template.map((f) => f.path);
438
- }
439
- /**
440
- * Initialize a local git repository in the output directory.
441
- * Creates an initial commit with all scaffold files.
442
- *
443
- * @param outputDir The directory to initialize git in
444
- * @returns true if successful, false otherwise
445
- */
446
- static async initializeGitRepo(outputDir) {
447
- try {
448
- // Check if git is already initialized
449
- const gitDir = join(outputDir, '.git');
450
- const exists = await fileExists(gitDir);
451
- if (exists) {
452
- return true; // Already initialized
453
- }
454
- // Initialize git repository
455
- execSync('git init', { cwd: outputDir, stdio: 'ignore' });
456
- // Stage all files
457
- execSync('git add .', { cwd: outputDir, stdio: 'ignore' });
458
- // Create initial commit
459
- execSync('git commit -m "chore: initial scaffold from sofIA"', {
460
- cwd: outputDir,
461
- stdio: 'ignore',
462
- env: {
463
- ...process.env,
464
- GIT_AUTHOR_NAME: 'sofIA',
465
- GIT_AUTHOR_EMAIL: 'sofia@workshop.local',
466
- GIT_COMMITTER_NAME: 'sofIA',
467
- GIT_COMMITTER_EMAIL: 'sofia@workshop.local',
468
- },
469
- });
470
- return true;
471
- }
472
- catch (_err) {
473
- // Git initialization failed - not critical, just return false
474
- return false;
475
- }
476
- }
477
- }
478
- /**
479
- * Validate that a scaffold directory meets the poc-output contract requirements.
480
- */
481
- export async function validatePocOutput(outputDir) {
482
- const requiredFiles = [
483
- 'package.json',
484
- 'README.md',
485
- 'tsconfig.json',
486
- '.gitignore',
487
- '.sofia-metadata.json',
488
- ];
489
- const missingFiles = [];
490
- const errors = [];
491
- // Check required files
492
- for (const file of requiredFiles) {
493
- const exists = await fileExists(join(outputDir, file));
494
- if (!exists) {
495
- missingFiles.push(file);
496
- }
497
- }
498
- // Check package.json has test script
499
- if (!missingFiles.includes('package.json')) {
500
- try {
501
- const { readFile } = await import('node:fs/promises');
502
- const pkgContent = await readFile(join(outputDir, 'package.json'), 'utf-8');
503
- const pkg = JSON.parse(pkgContent);
504
- if (!pkg.scripts?.test) {
505
- errors.push('package.json is missing "test" script');
506
- }
507
- }
508
- catch {
509
- errors.push('package.json is not valid JSON');
510
- }
511
- }
512
- // Check for at least one .ts file in src/
513
- const { readdir } = await import('node:fs/promises');
514
- let hasSrcTs = false;
515
- try {
516
- const srcFiles = await readdir(join(outputDir, 'src'));
517
- hasSrcTs = srcFiles.some((f) => f.endsWith('.ts'));
518
- }
519
- catch {
520
- // src/ doesn't exist
521
- }
522
- if (!hasSrcTs) {
523
- errors.push('No TypeScript files found in src/');
524
- }
525
- // Check for at least one .test.ts file in tests/
526
- let hasTestFile = false;
527
- try {
528
- const testFiles = await readdir(join(outputDir, 'tests'));
529
- hasTestFile = testFiles.some((f) => f.endsWith('.test.ts'));
530
- }
531
- catch {
532
- // tests/ doesn't exist
533
- }
534
- if (!hasTestFile) {
535
- errors.push('No test files found in tests/');
536
- }
537
- return {
538
- valid: missingFiles.length === 0 && errors.length === 0,
539
- missingFiles,
540
- errors,
541
- };
542
- }
@@ -1,126 +0,0 @@
1
- /**
2
- * T041: E2E happy-path test for `sofia dev` command.
3
- *
4
- * Runs `sofia dev --session <fixtureId>` as a subprocess;
5
- * verifies exit code 0; verifies output directory has required files;
6
- * verifies session JSON updated with poc state.
7
- *
8
- * Note: This E2E test uses a fake CopilotClient (no real LLM calls).
9
- * It validates the CLI plumbing, argument parsing, and file creation.
10
- */
11
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
12
- import { mkdtemp, rm, readFile, writeFile, mkdir } from 'node:fs/promises';
13
- import { join } from 'node:path';
14
- import { tmpdir } from 'node:os';
15
- import { createRequire } from 'node:module';
16
- import { buildCli } from '../../src/cli/index.js';
17
- import { validateSessionForDevelop } from '../../src/cli/developCommand.js';
18
- import { PocScaffolder } from '../../src/develop/pocScaffolder.js';
19
- import { validatePocOutput } from '../../src/develop/pocScaffolder.js';
20
- const require = createRequire(import.meta.url);
21
- const fixtureSession = require('../fixtures/completedSession.json');
22
- describe('E2E: sofia dev command', () => {
23
- let workDir;
24
- beforeEach(async () => {
25
- workDir = await mkdtemp(join(tmpdir(), 'sofia-e2e-dev-'));
26
- // Create a .sofia/sessions dir for the session store
27
- await mkdir(join(workDir, '.sofia', 'sessions'), { recursive: true });
28
- // Write the fixture session
29
- await writeFile(join(workDir, '.sofia', 'sessions', `${fixtureSession.sessionId}.json`), JSON.stringify(fixtureSession), 'utf-8');
30
- });
31
- afterEach(async () => {
32
- await rm(workDir, { recursive: true, force: true });
33
- });
34
- it('CLI registers dev command with correct description', () => {
35
- const program = buildCli();
36
- const commands = program.commands.map((c) => c.name());
37
- expect(commands).toContain('dev');
38
- });
39
- it('dev command shows in help output', () => {
40
- const program = buildCli();
41
- const devCmd = program.commands.find((c) => c.name() === 'dev');
42
- expect(devCmd).toBeDefined();
43
- expect(devCmd?.description()).toContain('proof-of-concept');
44
- });
45
- it('dev command has --max-iterations option', () => {
46
- const program = buildCli();
47
- const devCmd = program.commands.find((c) => c.name() === 'dev');
48
- const options = devCmd?.options.map((o) => o.long);
49
- expect(options).toContain('--max-iterations');
50
- });
51
- it('dev command has --output option', () => {
52
- const program = buildCli();
53
- const devCmd = program.commands.find((c) => c.name() === 'dev');
54
- const options = devCmd?.options.map((o) => o.long);
55
- expect(options).toContain('--output');
56
- });
57
- it('dev command has --force option', () => {
58
- const program = buildCli();
59
- const devCmd = program.commands.find((c) => c.name() === 'dev');
60
- const options = devCmd?.options.map((o) => o.long);
61
- expect(options).toContain('--force');
62
- });
63
- describe('validateSessionForDevelop (session readiness)', () => {
64
- it('fixture session passes validation (has selection + plan)', () => {
65
- const error = validateSessionForDevelop(fixtureSession);
66
- expect(error).toBeNull();
67
- });
68
- it('fixture session has a valid selection', () => {
69
- expect(fixtureSession.selection).toBeDefined();
70
- expect(fixtureSession.selection.confirmedByUser).toBe(true);
71
- });
72
- it('fixture session has a valid plan with milestones', () => {
73
- expect(fixtureSession.plan).toBeDefined();
74
- expect(fixtureSession.plan.milestones.length).toBeGreaterThan(0);
75
- });
76
- });
77
- describe('PocScaffolder with fixture session', () => {
78
- let outputDir;
79
- beforeEach(async () => {
80
- outputDir = await mkdtemp(join(tmpdir(), 'sofia-e2e-poc-'));
81
- });
82
- afterEach(async () => {
83
- await rm(outputDir, { recursive: true, force: true });
84
- });
85
- it('scaffolds valid PoC output from fixture session', async () => {
86
- const scaffolder = new PocScaffolder();
87
- const ctx = PocScaffolder.buildContext(fixtureSession, outputDir);
88
- await scaffolder.scaffold(ctx);
89
- const validation = await validatePocOutput(outputDir);
90
- expect(validation.valid).toBe(true);
91
- });
92
- it('generated package.json has correct project name from fixture', async () => {
93
- const scaffolder = new PocScaffolder();
94
- const ctx = PocScaffolder.buildContext(fixtureSession, outputDir);
95
- await scaffolder.scaffold(ctx);
96
- const pkgContent = await readFile(join(outputDir, 'package.json'), 'utf-8');
97
- const pkg = JSON.parse(pkgContent);
98
- expect(pkg.name).toBe('ai-powered-route-optimizer');
99
- });
100
- it('session JSON would be updated with poc state after loop', () => {
101
- // Verify the shape of poc state that RalphLoop would produce
102
- const expectedPocShape = {
103
- repoSource: 'local',
104
- iterations: expect.arrayContaining([
105
- expect.objectContaining({
106
- outcome: 'scaffold',
107
- }),
108
- ]),
109
- };
110
- // This test verifies the schema is correct
111
- const poc = {
112
- repoSource: 'local',
113
- repoPath: outputDir,
114
- iterations: [
115
- {
116
- iteration: 1,
117
- startedAt: new Date().toISOString(),
118
- outcome: 'scaffold',
119
- filesChanged: ['package.json', 'src/index.ts'],
120
- },
121
- ],
122
- };
123
- expect(poc).toMatchObject(expectedPocShape);
124
- });
125
- });
126
- });