@codeyam/codeyam-cli 0.1.0-staging.ea73141 → 0.1.0-staging.f3b710d

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 (137) hide show
  1. package/analyzer-template/.build-info.json +7 -7
  2. package/analyzer-template/log.txt +3 -3
  3. package/analyzer-template/package.json +2 -2
  4. package/analyzer-template/packages/aws/package.json +5 -5
  5. package/analyzer-template/packages/database/package.json +1 -1
  6. package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js +39 -3
  7. package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js.map +1 -1
  8. package/codeyam-cli/src/commands/editor.js +365 -5
  9. package/codeyam-cli/src/commands/editor.js.map +1 -1
  10. package/codeyam-cli/src/commands/init.js +20 -0
  11. package/codeyam-cli/src/commands/init.js.map +1 -1
  12. package/codeyam-cli/src/utils/__tests__/devServerState.test.js +93 -1
  13. package/codeyam-cli/src/utils/__tests__/devServerState.test.js.map +1 -1
  14. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +136 -0
  15. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
  16. package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js +98 -1
  17. package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js.map +1 -1
  18. package/codeyam-cli/src/utils/__tests__/editorRoadmap.test.js +1108 -0
  19. package/codeyam-cli/src/utils/__tests__/editorRoadmap.test.js.map +1 -0
  20. package/codeyam-cli/src/utils/__tests__/envFile.test.js +125 -0
  21. package/codeyam-cli/src/utils/__tests__/envFile.test.js.map +1 -0
  22. package/codeyam-cli/src/utils/__tests__/handoffContext.test.js +500 -0
  23. package/codeyam-cli/src/utils/__tests__/handoffContext.test.js.map +1 -0
  24. package/codeyam-cli/src/utils/designSystemShowcase.js +810 -0
  25. package/codeyam-cli/src/utils/designSystemShowcase.js.map +1 -0
  26. package/codeyam-cli/src/utils/devServerState.js +32 -0
  27. package/codeyam-cli/src/utils/devServerState.js.map +1 -1
  28. package/codeyam-cli/src/utils/editorAudit.js +6 -1
  29. package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
  30. package/codeyam-cli/src/utils/editorRoadmap.js +574 -0
  31. package/codeyam-cli/src/utils/editorRoadmap.js.map +1 -0
  32. package/codeyam-cli/src/utils/editorScenarios.js +10 -0
  33. package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
  34. package/codeyam-cli/src/utils/envFile.js +90 -0
  35. package/codeyam-cli/src/utils/envFile.js.map +1 -0
  36. package/codeyam-cli/src/utils/handoffContext.js +257 -0
  37. package/codeyam-cli/src/utils/handoffContext.js.map +1 -0
  38. package/codeyam-cli/src/utils/install-skills.js +36 -6
  39. package/codeyam-cli/src/utils/install-skills.js.map +1 -1
  40. package/codeyam-cli/src/utils/techStackConfig.js +38 -0
  41. package/codeyam-cli/src/utils/techStackConfig.js.map +1 -0
  42. package/codeyam-cli/src/utils/techStackConfig.test.js +85 -0
  43. package/codeyam-cli/src/utils/techStackConfig.test.js.map +1 -0
  44. package/codeyam-cli/src/webserver/__tests__/api.interactive-switch-scenario.test.js +1 -0
  45. package/codeyam-cli/src/webserver/__tests__/api.interactive-switch-scenario.test.js.map +1 -1
  46. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js +119 -1
  47. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js.map +1 -1
  48. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +354 -1
  49. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -1
  50. package/codeyam-cli/src/webserver/app/lib/database.js.map +1 -1
  51. package/codeyam-cli/src/webserver/build/client/assets/{CopyButton-CLe80MMu.js → CopyButton-DTBZZfSk.js} +1 -1
  52. package/codeyam-cli/src/webserver/build/client/assets/{EntityItem-Crt_KN_U.js → EntityItem-BxclONWq.js} +1 -1
  53. package/codeyam-cli/src/webserver/build/client/assets/{EntityTypeIcon-CD7lGABo.js → EntityTypeIcon-BsnEOJZ_.js} +1 -1
  54. package/codeyam-cli/src/webserver/build/client/assets/{InlineSpinner-CgTNOhnu.js → InlineSpinner-ByaELMbv.js} +1 -1
  55. package/codeyam-cli/src/webserver/build/client/assets/{InteractivePreview-DtYTSPL2.js → InteractivePreview-6WjVfhxX.js} +2 -2
  56. package/codeyam-cli/src/webserver/build/client/assets/{LibraryFunctionPreview-D3s1MFkb.js → LibraryFunctionPreview-ChX-Hp7W.js} +1 -1
  57. package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-CM5zg40N.js → LogViewer-C-9zQdXg.js} +1 -1
  58. package/codeyam-cli/src/webserver/build/client/assets/{MiniClaudeChat-CQENLSrF.js → MiniClaudeChat-Bs2_Oua4.js} +2 -2
  59. package/codeyam-cli/src/webserver/build/client/assets/{ReportIssueModal-C2PLkej3.js → ReportIssueModal-DQsceHVv.js} +1 -1
  60. package/codeyam-cli/src/webserver/build/client/assets/{SafeScreenshot-DanvyBPb.js → SafeScreenshot-DThcm_9M.js} +1 -1
  61. package/codeyam-cli/src/webserver/build/client/assets/{ScenarioViewer-CefgqbCr.js → ScenarioViewer-Cl4oOA3A.js} +1 -1
  62. package/codeyam-cli/src/webserver/build/client/assets/{Spinner-Bc8BG-Lw.js → Spinner-CIil5-gb.js} +1 -1
  63. package/codeyam-cli/src/webserver/build/client/assets/{ViewportInspectBar-BA_Ry-rs.js → ViewportInspectBar-BqkA9zyZ.js} +1 -1
  64. package/codeyam-cli/src/webserver/build/client/assets/{_index-C1YkzTAV.js → _index-DnOgyseQ.js} +1 -1
  65. package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-yH46LLUz.js → activity.(_tab)-DqM9hbNE.js} +1 -1
  66. package/codeyam-cli/src/webserver/build/client/assets/{addon-web-links-CHx25PAe.js → addon-web-links-C58dYPwR.js} +1 -1
  67. package/codeyam-cli/src/webserver/build/client/assets/{agent-transcripts-Bg3e7q4S.js → agent-transcripts-B8NCeOrm.js} +1 -1
  68. package/codeyam-cli/src/webserver/build/client/assets/api.editor-database-verify-l0sNRNKZ.js +1 -0
  69. package/codeyam-cli/src/webserver/build/client/assets/api.editor-github-verify-l0sNRNKZ.js +1 -0
  70. package/codeyam-cli/src/webserver/build/client/assets/api.editor-handoff-l0sNRNKZ.js +1 -0
  71. package/codeyam-cli/src/webserver/build/client/assets/api.editor-hosting-verify-l0sNRNKZ.js +1 -0
  72. package/codeyam-cli/src/webserver/build/client/assets/api.editor-roadmap-l0sNRNKZ.js +1 -0
  73. package/codeyam-cli/src/webserver/build/client/assets/{book-open-CL-lMgHh.js → book-open-BFSIqZgO.js} +1 -1
  74. package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-GmAjGS9-.js → chevron-down-B9fDzFVh.js} +1 -1
  75. package/codeyam-cli/src/webserver/build/client/assets/chunk-UVKPFVEO-Bmq2apuh.js +43 -0
  76. package/codeyam-cli/src/webserver/build/client/assets/{circle-check-DFcQkN5j.js → circle-check-DLPObLUx.js} +1 -1
  77. package/codeyam-cli/src/webserver/build/client/assets/{copy-C6iF61Xs.js → copy-DXEmO0TD.js} +1 -1
  78. package/codeyam-cli/src/webserver/build/client/assets/{createLucideIcon-4ImjHTVC.js → createLucideIcon-BwyFiRot.js} +1 -1
  79. package/codeyam-cli/src/webserver/build/client/assets/{dev.empty-CRepiabR.js → dev.empty-iRhRIFlp.js} +1 -1
  80. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-BZPBzV73.js +1 -0
  81. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DhtVC4aI.js +161 -0
  82. package/codeyam-cli/src/webserver/build/client/assets/{editorPreview-CluPkvXJ.js → editorPreview-C6fEYHrh.js} +6 -6
  83. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-DYJRGiDI.js → entity._sha._-pc-vc6wO.js} +1 -1
  84. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-wdiwx5-Z.js → entity._sha.scenarios._scenarioId.dev-C8AyYgYT.js} +1 -1
  85. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.fullscreen-BrkN-40Y.js → entity._sha.scenarios._scenarioId.fullscreen-DziaVQX1.js} +1 -1
  86. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.create-scenario-DxfhekTZ.js → entity._sha_.create-scenario-BTcpgIpC.js} +1 -1
  87. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.edit._scenarioId-CRXJWmpB.js → entity._sha_.edit._scenarioId-D_O_ajfZ.js} +1 -1
  88. package/codeyam-cli/src/webserver/build/client/assets/{entry.client-SuW9syRS.js → entry.client-j1Vi0bco.js} +6 -6
  89. package/codeyam-cli/src/webserver/build/client/assets/{files-D-xGrg29.js → files-kuny2Q_s.js} +1 -1
  90. package/codeyam-cli/src/webserver/build/client/assets/{git-Bq_fbXP5.js → git-DgCZPMie.js} +1 -1
  91. package/codeyam-cli/src/webserver/build/client/assets/globals-L-aUIeux.css +1 -0
  92. package/codeyam-cli/src/webserver/build/client/assets/{index-Bp1l4hSv.js → index-BliGSSpl.js} +1 -1
  93. package/codeyam-cli/src/webserver/build/client/assets/{index-DE3jI_dv.js → index-SqjQKTdH.js} +1 -1
  94. package/codeyam-cli/src/webserver/build/client/assets/{index-CWV9XZiG.js → index-vyrZD2g4.js} +1 -1
  95. package/codeyam-cli/src/webserver/build/client/assets/{labs-B_IX45ih.js → labs-c3yLxSEp.js} +1 -1
  96. package/codeyam-cli/src/webserver/build/client/assets/{loader-circle-De-7qQ2u.js → loader-circle-D-q28GLF.js} +1 -1
  97. package/codeyam-cli/src/webserver/build/client/assets/manifest-79d0d81a.js +1 -0
  98. package/codeyam-cli/src/webserver/build/client/assets/{memory-Cx2xEx7s.js → memory-CEWIUC4t.js} +1 -1
  99. package/codeyam-cli/src/webserver/build/client/assets/{pause-CFxEKL1u.js → pause-BP6fitdh.js} +1 -1
  100. package/codeyam-cli/src/webserver/build/client/assets/{root-dKFRTYcy.js → root-L2V0jea7.js} +6 -6
  101. package/codeyam-cli/src/webserver/build/client/assets/{search-BdBb5aqc.js → search-BooqacKS.js} +1 -1
  102. package/codeyam-cli/src/webserver/build/client/assets/{settings-DdE-Untf.js → settings-BM0nbryO.js} +1 -1
  103. package/codeyam-cli/src/webserver/build/client/assets/{simulations-DSCdE99u.js → simulations-ovy6FjRY.js} +1 -1
  104. package/codeyam-cli/src/webserver/build/client/assets/{terminal-CrplD4b1.js → terminal-DHemCJIs.js} +1 -1
  105. package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-DqJ0j69l.js → triangle-alert-D87ekDl8.js} +1 -1
  106. package/codeyam-cli/src/webserver/build/client/assets/{useCustomSizes-DhXHbEjP.js → useCustomSizes-Dk0Tciqg.js} +1 -1
  107. package/codeyam-cli/src/webserver/build/client/assets/{useLastLogLine-D9QZKaLJ.js → useLastLogLine-C8QvIe05.js} +1 -1
  108. package/codeyam-cli/src/webserver/build/client/assets/{useReportContext-Cy5Qg_UR.js → useReportContext-jkCytuYz.js} +1 -1
  109. package/codeyam-cli/src/webserver/build/client/assets/{useToast-5HR2j9ZE.js → useToast-BgqkixU9.js} +1 -1
  110. package/codeyam-cli/src/webserver/build/server/assets/{analysisRunner-OLsM110H.js → analysisRunner-QgInFGdU.js} +1 -1
  111. package/codeyam-cli/src/webserver/build/server/assets/{index-WHdB6WTN.js → index-zblh9auj.js} +1 -1
  112. package/codeyam-cli/src/webserver/build/server/assets/init-DaE0CBjk.js +14 -0
  113. package/codeyam-cli/src/webserver/build/server/assets/server-build-CNvgz1cC.js +853 -0
  114. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  115. package/codeyam-cli/src/webserver/build-info.json +5 -5
  116. package/codeyam-cli/src/webserver/editorProxy.js +255 -20
  117. package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
  118. package/codeyam-cli/src/webserver/server.js +67 -0
  119. package/codeyam-cli/src/webserver/server.js.map +1 -1
  120. package/codeyam-cli/src/webserver/terminalServer.js +102 -11
  121. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
  122. package/codeyam-cli/templates/codeyam-editor-codex.md +61 -0
  123. package/codeyam-cli/templates/codeyam-editor-gemini.md +59 -0
  124. package/codeyam-cli/templates/editor-step-hook.py +4 -4
  125. package/codeyam-cli/templates/expo-react-native/MOBILE_SETUP.md +36 -1
  126. package/codeyam-cli/templates/expo-react-native/app.json +11 -0
  127. package/codeyam-cli/templates/expo-react-native/package.json +1 -0
  128. package/codeyam-cli/templates/nextjs-prisma-sqlite/gitignore +1 -0
  129. package/codeyam-cli/templates/seed-adapters/supabase.ts +185 -84
  130. package/package.json +1 -1
  131. package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-BAdwhyCx.js +0 -43
  132. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-Gbk_i5Js.js +0 -1
  133. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-CRxPi2BB.js +0 -96
  134. package/codeyam-cli/src/webserver/build/client/assets/globals-BsGHu8WX.css +0 -1
  135. package/codeyam-cli/src/webserver/build/client/assets/manifest-9032538f.js +0 -1
  136. package/codeyam-cli/src/webserver/build/server/assets/init-DbSiZoE6.js +0 -10
  137. package/codeyam-cli/src/webserver/build/server/assets/server-build-DZbLY6O_.js +0 -690
@@ -0,0 +1,1108 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { readRoadmap, writeRoadmap, getDefaultRoadmap, sanitizeRoadmapData, checkAutoDetections, countJournalEntries, getRecentJournalEntries, generateServiceDeployTasks, } from "../editorRoadmap.js";
5
+ let tmpDir;
6
+ beforeEach(() => {
7
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roadmap-test-'));
8
+ fs.mkdirSync(path.join(tmpDir, '.codeyam'), { recursive: true });
9
+ });
10
+ afterEach(() => {
11
+ fs.rmSync(tmpDir, { recursive: true, force: true });
12
+ });
13
+ // ── getDefaultRoadmap ───────────────────────────────────────────────────
14
+ describe('getDefaultRoadmap', () => {
15
+ it('returns plan and deploy arrays with predefined tasks', () => {
16
+ const defaults = getDefaultRoadmap();
17
+ expect(defaults.plan.length).toBeGreaterThanOrEqual(3);
18
+ expect(defaults.deploy.length).toBeGreaterThanOrEqual(5);
19
+ expect(defaults.plan.every((t) => !t.completed)).toBe(true);
20
+ expect(defaults.deploy.every((t) => !t.completed)).toBe(true);
21
+ });
22
+ it('deploy-hosting has autoDetect key in defaults', () => {
23
+ const defaults = getDefaultRoadmap();
24
+ const hosting = defaults.deploy.find((t) => t.id === 'deploy-hosting');
25
+ expect(hosting?.autoDetect).toBe('hosting-configured');
26
+ });
27
+ it('returns fresh copies on each call', () => {
28
+ const a = getDefaultRoadmap();
29
+ const b = getDefaultRoadmap();
30
+ a.plan[0].completed = true;
31
+ expect(b.plan[0].completed).toBe(false);
32
+ });
33
+ });
34
+ // ── readRoadmap / writeRoadmap ──────────────────────────────────────────
35
+ describe('readRoadmap', () => {
36
+ it('returns defaults when file does not exist', () => {
37
+ const data = readRoadmap(tmpDir);
38
+ expect(data.plan.length).toBeGreaterThan(0);
39
+ expect(data.deploy.length).toBeGreaterThan(0);
40
+ });
41
+ it('reads back written data', () => {
42
+ const custom = getDefaultRoadmap();
43
+ custom.plan[0].completed = true;
44
+ custom.plan[0].completedAt = '2026-03-30T00:00:00Z';
45
+ writeRoadmap(tmpDir, custom);
46
+ const read = readRoadmap(tmpDir);
47
+ expect(read.plan[0].completed).toBe(true);
48
+ expect(read.plan[0].completedAt).toBe('2026-03-30T00:00:00Z');
49
+ });
50
+ it('returns defaults for malformed JSON', () => {
51
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'roadmap.json'), 'not json');
52
+ const data = readRoadmap(tmpDir);
53
+ expect(data.plan.length).toBeGreaterThan(0);
54
+ });
55
+ });
56
+ describe('writeRoadmap', () => {
57
+ it('creates .codeyam directory if missing', () => {
58
+ const freshDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roadmap-fresh-'));
59
+ fs.mkdirSync(path.join(freshDir, '.codeyam'), { recursive: true });
60
+ writeRoadmap(freshDir, getDefaultRoadmap());
61
+ expect(fs.existsSync(path.join(freshDir, '.codeyam', 'roadmap.json'))).toBe(true);
62
+ fs.rmSync(freshDir, { recursive: true, force: true });
63
+ });
64
+ });
65
+ // ── sanitizeRoadmapData ─────────────────────────────────────────────────
66
+ describe('sanitizeRoadmapData', () => {
67
+ it('returns defaults for null input', () => {
68
+ const result = sanitizeRoadmapData(null);
69
+ expect(result.plan.length).toBeGreaterThan(0);
70
+ });
71
+ it('returns defaults for non-object input', () => {
72
+ const result = sanitizeRoadmapData('hello');
73
+ expect(result.plan.length).toBeGreaterThan(0);
74
+ });
75
+ it('drops invalid entries from arrays', () => {
76
+ const result = sanitizeRoadmapData({
77
+ plan: [
78
+ { id: 'valid', label: 'Valid task', completed: false },
79
+ { noId: true },
80
+ null,
81
+ 42,
82
+ ],
83
+ deploy: [],
84
+ });
85
+ // The custom valid entry is preserved; default tasks are merged in
86
+ expect(result.plan.find((t) => t.id === 'valid')).toBeDefined();
87
+ expect(result.plan.find((t) => t.id === 'valid').label).toBe('Valid task');
88
+ });
89
+ it('defaults completed to false when missing', () => {
90
+ const result = sanitizeRoadmapData({
91
+ plan: [{ id: 'a', label: 'A' }],
92
+ deploy: [],
93
+ });
94
+ expect(result.plan[0].completed).toBe(false);
95
+ });
96
+ it('preserves optional fields when valid', () => {
97
+ const result = sanitizeRoadmapData({
98
+ plan: [
99
+ {
100
+ id: 'a',
101
+ label: 'A',
102
+ completed: true,
103
+ completedAt: '2026-01-01',
104
+ autoDetect: 'test-key',
105
+ userCreated: true,
106
+ },
107
+ ],
108
+ deploy: [],
109
+ });
110
+ const task = result.plan.find((t) => t.id === 'a');
111
+ expect(task.completedAt).toBe('2026-01-01');
112
+ expect(task.autoDetect).toBe('test-key');
113
+ expect(task.userCreated).toBe(true);
114
+ });
115
+ it('uses defaults when plan/deploy are not arrays', () => {
116
+ const result = sanitizeRoadmapData({ plan: 'not array', deploy: 123 });
117
+ expect(result.plan.length).toBeGreaterThan(0);
118
+ expect(result.deploy.length).toBeGreaterThan(0);
119
+ });
120
+ it('patches autoDetect onto existing deploy-hosting tasks from defaults', () => {
121
+ // Simulates an existing roadmap.json that was saved before autoDetect was added
122
+ const result = sanitizeRoadmapData({
123
+ plan: [],
124
+ deploy: [
125
+ {
126
+ id: 'deploy-hosting',
127
+ label: 'Set up hosting provider',
128
+ completed: false,
129
+ },
130
+ ],
131
+ });
132
+ const hosting = result.deploy.find((t) => t.id === 'deploy-hosting');
133
+ expect(hosting?.autoDetect).toBe('hosting-configured');
134
+ });
135
+ });
136
+ // ── checkAutoDetections ─────────────────────────────────────────────────
137
+ describe('checkAutoDetections', () => {
138
+ it('marks project-title-exists when config has projectTitle', () => {
139
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ projectTitle: 'My App' }));
140
+ const todos = [
141
+ {
142
+ id: 'plan-project-name',
143
+ label: 'Define project name',
144
+ completed: false,
145
+ autoDetect: 'project-title-exists',
146
+ },
147
+ ];
148
+ const result = checkAutoDetections(tmpDir, todos);
149
+ expect(result[0].completed).toBe(true);
150
+ expect(result[0].completedAt).toBeDefined();
151
+ });
152
+ it('marks design-system-exists when file present', () => {
153
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'design-system.md'), '# Design');
154
+ const todos = [
155
+ {
156
+ id: 'plan-design-system',
157
+ label: 'Create design system',
158
+ completed: false,
159
+ autoDetect: 'design-system-exists',
160
+ },
161
+ ];
162
+ const result = checkAutoDetections(tmpDir, todos);
163
+ expect(result[0].completed).toBe(true);
164
+ });
165
+ it('does not mark design-system-exists when file missing', () => {
166
+ const todos = [
167
+ {
168
+ id: 'plan-design-system',
169
+ label: 'Create design system',
170
+ completed: false,
171
+ autoDetect: 'design-system-exists',
172
+ },
173
+ ];
174
+ const result = checkAutoDetections(tmpDir, todos);
175
+ expect(result[0].completed).toBe(false);
176
+ });
177
+ it('marks screen-sizes-configured when config has screen sizes', () => {
178
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
179
+ screenSizes: { Desktop: { width: 1280, height: 720 } },
180
+ }));
181
+ const todos = [
182
+ {
183
+ id: 'plan-screen-sizes',
184
+ label: 'Configure screen sizes',
185
+ completed: false,
186
+ autoDetect: 'screen-sizes-configured',
187
+ },
188
+ ];
189
+ const result = checkAutoDetections(tmpDir, todos);
190
+ expect(result[0].completed).toBe(true);
191
+ });
192
+ it('marks scenarios-exist when editor-scenarios has JSON files', () => {
193
+ const scenariosDir = path.join(tmpDir, '.codeyam', 'editor-scenarios');
194
+ fs.mkdirSync(scenariosDir, { recursive: true });
195
+ fs.writeFileSync(path.join(scenariosDir, 'test.json'), '{}');
196
+ const todos = [
197
+ {
198
+ id: 'plan-initial-scenarios',
199
+ label: 'Create initial scenarios',
200
+ completed: false,
201
+ autoDetect: 'scenarios-exist',
202
+ },
203
+ ];
204
+ const result = checkAutoDetections(tmpDir, todos);
205
+ expect(result[0].completed).toBe(true);
206
+ });
207
+ it('leaves tasks without autoDetect unchanged', () => {
208
+ const todos = [
209
+ {
210
+ id: 'manual',
211
+ label: 'Manual task',
212
+ completed: false,
213
+ },
214
+ ];
215
+ const result = checkAutoDetections(tmpDir, todos);
216
+ expect(result[0].completed).toBe(false);
217
+ });
218
+ it('un-completes auto-detect tasks when condition no longer met', () => {
219
+ const todos = [
220
+ {
221
+ id: 'plan-design-system',
222
+ label: 'Create design system',
223
+ completed: true,
224
+ completedAt: '2026-01-01',
225
+ autoDetect: 'design-system-exists',
226
+ },
227
+ ];
228
+ // No design-system.md file
229
+ const result = checkAutoDetections(tmpDir, todos);
230
+ expect(result[0].completed).toBe(false);
231
+ expect(result[0].completedAt).toBeUndefined();
232
+ });
233
+ it('marks hosting-configured when config has hosting.provider', () => {
234
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ hosting: { provider: 'railway' } }));
235
+ const todos = [
236
+ {
237
+ id: 'deploy-hosting',
238
+ label: 'Set up hosting provider',
239
+ completed: false,
240
+ autoDetect: 'hosting-configured',
241
+ },
242
+ ];
243
+ const result = checkAutoDetections(tmpDir, todos);
244
+ expect(result[0].completed).toBe(true);
245
+ expect(result[0].completedAt).toBeDefined();
246
+ });
247
+ it('does not mark hosting-configured when hosting.provider is missing', () => {
248
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ hosting: {} }));
249
+ const todos = [
250
+ {
251
+ id: 'deploy-hosting',
252
+ label: 'Set up hosting provider',
253
+ completed: false,
254
+ autoDetect: 'hosting-configured',
255
+ },
256
+ ];
257
+ const result = checkAutoDetections(tmpDir, todos);
258
+ expect(result[0].completed).toBe(false);
259
+ });
260
+ it('does not mark hosting-configured when no hosting field exists', () => {
261
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({}));
262
+ const todos = [
263
+ {
264
+ id: 'deploy-hosting',
265
+ label: 'Set up hosting provider',
266
+ completed: false,
267
+ autoDetect: 'hosting-configured',
268
+ },
269
+ ];
270
+ const result = checkAutoDetections(tmpDir, todos);
271
+ expect(result[0].completed).toBe(false);
272
+ });
273
+ it('does not mark hosting-configured for vercel without projectId', () => {
274
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ hosting: { provider: 'vercel' } }));
275
+ const todos = [
276
+ {
277
+ id: 'deploy-hosting',
278
+ label: 'Set up hosting provider',
279
+ completed: false,
280
+ autoDetect: 'hosting-configured',
281
+ },
282
+ ];
283
+ const result = checkAutoDetections(tmpDir, todos);
284
+ expect(result[0].completed).toBe(false);
285
+ });
286
+ it('marks hosting-configured for vercel with projectId', () => {
287
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
288
+ hosting: { provider: 'vercel', vercelProjectId: 'prj_123' },
289
+ }));
290
+ const todos = [
291
+ {
292
+ id: 'deploy-hosting',
293
+ label: 'Set up hosting provider',
294
+ completed: false,
295
+ autoDetect: 'hosting-configured',
296
+ },
297
+ ];
298
+ const result = checkAutoDetections(tmpDir, todos);
299
+ expect(result[0].completed).toBe(true);
300
+ });
301
+ it('marks hosting-configured for non-vercel providers with just provider name', () => {
302
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ hosting: { provider: 'railway' } }));
303
+ const todos = [
304
+ {
305
+ id: 'deploy-hosting',
306
+ label: 'Set up hosting provider',
307
+ completed: false,
308
+ autoDetect: 'hosting-configured',
309
+ },
310
+ ];
311
+ const result = checkAutoDetections(tmpDir, todos);
312
+ expect(result[0].completed).toBe(true);
313
+ });
314
+ it('handles unknown autoDetect keys gracefully', () => {
315
+ const todos = [
316
+ {
317
+ id: 'custom',
318
+ label: 'Custom',
319
+ completed: false,
320
+ autoDetect: 'unknown-checker',
321
+ },
322
+ ];
323
+ const result = checkAutoDetections(tmpDir, todos);
324
+ expect(result[0].completed).toBe(false);
325
+ });
326
+ it('marks tech-stack-configured when techStack has populated categories', () => {
327
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
328
+ techStack: {
329
+ languages: [{ name: 'TypeScript', version: '5' }],
330
+ frameworks: [{ name: 'Next.js', version: '15' }],
331
+ },
332
+ }));
333
+ const todos = [
334
+ {
335
+ id: 'plan-tech-stack',
336
+ label: 'Review tech stack',
337
+ completed: false,
338
+ autoDetect: 'tech-stack-configured',
339
+ },
340
+ ];
341
+ const result = checkAutoDetections(tmpDir, todos);
342
+ expect(result[0].completed).toBe(true);
343
+ });
344
+ it('marks tech-stack-configured via webapps fallback', () => {
345
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
346
+ webapps: [{ path: '.', framework: 'Next' }],
347
+ }));
348
+ const todos = [
349
+ {
350
+ id: 'plan-tech-stack',
351
+ label: 'Review tech stack',
352
+ completed: false,
353
+ autoDetect: 'tech-stack-configured',
354
+ },
355
+ ];
356
+ const result = checkAutoDetections(tmpDir, todos);
357
+ expect(result[0].completed).toBe(true);
358
+ });
359
+ it('does not mark tech-stack-configured when neither techStack nor webapps exist', () => {
360
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({}));
361
+ const todos = [
362
+ {
363
+ id: 'plan-tech-stack',
364
+ label: 'Review tech stack',
365
+ completed: false,
366
+ autoDetect: 'tech-stack-configured',
367
+ },
368
+ ];
369
+ const result = checkAutoDetections(tmpDir, todos);
370
+ expect(result[0].completed).toBe(false);
371
+ });
372
+ it('does not mark tech-stack-configured for empty categories', () => {
373
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
374
+ techStack: { languages: [], frameworks: [] },
375
+ }));
376
+ const todos = [
377
+ {
378
+ id: 'plan-tech-stack',
379
+ label: 'Review tech stack',
380
+ completed: false,
381
+ autoDetect: 'tech-stack-configured',
382
+ },
383
+ ];
384
+ const result = checkAutoDetections(tmpDir, todos);
385
+ expect(result[0].completed).toBe(false);
386
+ });
387
+ });
388
+ // ── countJournalEntries ─────────────────────────────────────────────────
389
+ describe('countJournalEntries', () => {
390
+ it('returns 0 when journal does not exist', () => {
391
+ expect(countJournalEntries(tmpDir)).toBe(0);
392
+ });
393
+ it('counts entries from journal index', () => {
394
+ const journalDir = path.join(tmpDir, '.codeyam', 'journal');
395
+ fs.mkdirSync(journalDir, { recursive: true });
396
+ fs.writeFileSync(path.join(journalDir, 'index.json'), JSON.stringify({
397
+ entries: [
398
+ { date: '2026-03-29', time: '10:00', title: 'Entry 1' },
399
+ { date: '2026-03-30', time: '11:00', title: 'Entry 2' },
400
+ { date: '2026-03-30', time: '12:00', title: 'Entry 3' },
401
+ ],
402
+ }));
403
+ expect(countJournalEntries(tmpDir)).toBe(3);
404
+ });
405
+ it('returns 0 for malformed journal', () => {
406
+ const journalDir = path.join(tmpDir, '.codeyam', 'journal');
407
+ fs.mkdirSync(journalDir, { recursive: true });
408
+ fs.writeFileSync(path.join(journalDir, 'index.json'), 'not json');
409
+ expect(countJournalEntries(tmpDir)).toBe(0);
410
+ });
411
+ });
412
+ // ── getRecentJournalEntries ─────────────────────────────────────────────
413
+ describe('getRecentJournalEntries', () => {
414
+ it('returns empty array when journal does not exist', () => {
415
+ expect(getRecentJournalEntries(tmpDir)).toEqual([]);
416
+ });
417
+ it('returns the 3 most recent entries (newest first)', () => {
418
+ const journalDir = path.join(tmpDir, '.codeyam', 'journal');
419
+ fs.mkdirSync(journalDir, { recursive: true });
420
+ fs.writeFileSync(path.join(journalDir, 'index.json'), JSON.stringify({
421
+ entries: [
422
+ { date: '2026-03-28', time: '2026-03-28T09:00:00Z', title: 'First' },
423
+ { date: '2026-03-29', time: '2026-03-29T10:00:00Z', title: 'Second' },
424
+ { date: '2026-03-29', time: '2026-03-29T14:00:00Z', title: 'Third' },
425
+ { date: '2026-03-30', time: '2026-03-30T11:00:00Z', title: 'Fourth' },
426
+ { date: '2026-03-30', time: '2026-03-30T15:00:00Z', title: 'Fifth' },
427
+ ],
428
+ }));
429
+ const result = getRecentJournalEntries(tmpDir, 3);
430
+ expect(result).toHaveLength(3);
431
+ // Newest first
432
+ expect(result[0].title).toBe('Fifth');
433
+ expect(result[1].title).toBe('Fourth');
434
+ expect(result[2].title).toBe('Third');
435
+ });
436
+ it('returns all entries when fewer than limit', () => {
437
+ const journalDir = path.join(tmpDir, '.codeyam', 'journal');
438
+ fs.mkdirSync(journalDir, { recursive: true });
439
+ fs.writeFileSync(path.join(journalDir, 'index.json'), JSON.stringify({
440
+ entries: [
441
+ {
442
+ date: '2026-03-30',
443
+ time: '2026-03-30T10:00:00Z',
444
+ title: 'Only one',
445
+ },
446
+ ],
447
+ }));
448
+ const result = getRecentJournalEntries(tmpDir, 3);
449
+ expect(result).toHaveLength(1);
450
+ expect(result[0].title).toBe('Only one');
451
+ });
452
+ it('returns empty array for malformed journal', () => {
453
+ const journalDir = path.join(tmpDir, '.codeyam', 'journal');
454
+ fs.mkdirSync(journalDir, { recursive: true });
455
+ fs.writeFileSync(path.join(journalDir, 'index.json'), 'not json');
456
+ expect(getRecentJournalEntries(tmpDir)).toEqual([]);
457
+ });
458
+ it('includes title, time, and type fields in returned entries', () => {
459
+ const journalDir = path.join(tmpDir, '.codeyam', 'journal');
460
+ fs.mkdirSync(journalDir, { recursive: true });
461
+ fs.writeFileSync(path.join(journalDir, 'index.json'), JSON.stringify({
462
+ entries: [
463
+ {
464
+ date: '2026-03-30',
465
+ time: '2026-03-30T10:00:00Z',
466
+ title: 'Add checkout page',
467
+ type: 'feature',
468
+ description: 'Built the checkout flow',
469
+ scenarios: [],
470
+ screenshot: null,
471
+ commitSha: 'abc123',
472
+ commitMessage: 'Add checkout page',
473
+ },
474
+ ],
475
+ }));
476
+ const result = getRecentJournalEntries(tmpDir, 3);
477
+ expect(result[0]).toEqual({
478
+ title: 'Add checkout page',
479
+ time: '2026-03-30T10:00:00Z',
480
+ type: 'feature',
481
+ description: 'Built the checkout flow',
482
+ userPrompt: undefined,
483
+ scenarioScreenshots: undefined,
484
+ modifiedFiles: undefined,
485
+ entityChangeStatus: undefined,
486
+ commitSha: 'abc123',
487
+ commitMessage: 'Add checkout page',
488
+ });
489
+ });
490
+ it('defaults limit to 3', () => {
491
+ const journalDir = path.join(tmpDir, '.codeyam', 'journal');
492
+ fs.mkdirSync(journalDir, { recursive: true });
493
+ fs.writeFileSync(path.join(journalDir, 'index.json'), JSON.stringify({
494
+ entries: [
495
+ { date: '2026-03-28', time: '2026-03-28T09:00:00Z', title: 'A' },
496
+ { date: '2026-03-29', time: '2026-03-29T10:00:00Z', title: 'B' },
497
+ { date: '2026-03-29', time: '2026-03-29T14:00:00Z', title: 'C' },
498
+ { date: '2026-03-30', time: '2026-03-30T11:00:00Z', title: 'D' },
499
+ { date: '2026-03-30', time: '2026-03-30T15:00:00Z', title: 'E' },
500
+ ],
501
+ }));
502
+ const result = getRecentJournalEntries(tmpDir);
503
+ expect(result).toHaveLength(3);
504
+ });
505
+ });
506
+ // ── generateServiceDeployTasks ─────────────────────────────────────────
507
+ describe('generateServiceDeployTasks', () => {
508
+ it('returns empty array when tech stack has no services with envKeys', () => {
509
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
510
+ techStack: {
511
+ languages: [{ name: 'TypeScript', url: '', description: '' }],
512
+ frameworks: [{ name: 'Next.js', url: '', description: '' }],
513
+ libraries: [{ name: 'Tailwind', url: '', description: '' }],
514
+ },
515
+ }));
516
+ const tasks = generateServiceDeployTasks(tmpDir);
517
+ expect(tasks).toEqual([]);
518
+ });
519
+ it('returns empty array when no tech stack exists', () => {
520
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({}));
521
+ const tasks = generateServiceDeployTasks(tmpDir);
522
+ expect(tasks).toEqual([]);
523
+ });
524
+ it('returns empty array when config.json does not exist', () => {
525
+ const tasks = generateServiceDeployTasks(tmpDir);
526
+ expect(tasks).toEqual([]);
527
+ });
528
+ it('generates tasks for databases with envKeys', () => {
529
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
530
+ techStack: {
531
+ databases: [
532
+ {
533
+ name: 'Neon',
534
+ url: 'https://neon.tech',
535
+ description: 'Serverless PostgreSQL',
536
+ envKeys: ['DATABASE_URL', 'DIRECT_URL'],
537
+ },
538
+ ],
539
+ },
540
+ }));
541
+ const tasks = generateServiceDeployTasks(tmpDir);
542
+ expect(tasks).toHaveLength(1);
543
+ expect(tasks[0].id).toBe('deploy-svc-neon');
544
+ expect(tasks[0].label).toBe('Set up Neon');
545
+ expect(tasks[0].autoDetect).toBe('svc-envkeys-neon');
546
+ expect(tasks[0].serviceRef).toEqual({
547
+ name: 'Neon',
548
+ category: 'databases',
549
+ });
550
+ expect(tasks[0].completed).toBe(false);
551
+ });
552
+ it('generates tasks for services with envKeys', () => {
553
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
554
+ techStack: {
555
+ services: [
556
+ {
557
+ name: 'Stripe',
558
+ url: 'https://stripe.com',
559
+ description: 'Payments',
560
+ envKeys: ['STRIPE_SECRET_KEY', 'STRIPE_PUBLISHABLE_KEY'],
561
+ },
562
+ ],
563
+ },
564
+ }));
565
+ const tasks = generateServiceDeployTasks(tmpDir);
566
+ expect(tasks).toHaveLength(1);
567
+ expect(tasks[0].id).toBe('deploy-svc-stripe');
568
+ expect(tasks[0].label).toBe('Set up Stripe');
569
+ expect(tasks[0].serviceRef).toEqual({
570
+ name: 'Stripe',
571
+ category: 'services',
572
+ });
573
+ });
574
+ it('generates tasks for infrastructure with envKeys', () => {
575
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
576
+ techStack: {
577
+ infrastructure: [
578
+ {
579
+ name: 'AWS S3',
580
+ url: 'https://aws.amazon.com/s3',
581
+ description: 'Object storage',
582
+ envKeys: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'],
583
+ },
584
+ ],
585
+ },
586
+ }));
587
+ const tasks = generateServiceDeployTasks(tmpDir);
588
+ expect(tasks).toHaveLength(1);
589
+ expect(tasks[0].id).toBe('deploy-svc-aws-s3');
590
+ expect(tasks[0].label).toBe('Set up AWS S3');
591
+ });
592
+ it('skips services with empty envKeys arrays', () => {
593
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
594
+ techStack: {
595
+ services: [
596
+ {
597
+ name: 'Stripe',
598
+ url: '',
599
+ description: '',
600
+ envKeys: ['STRIPE_KEY'],
601
+ },
602
+ {
603
+ name: 'SomeLib',
604
+ url: '',
605
+ description: '',
606
+ envKeys: [],
607
+ },
608
+ ],
609
+ },
610
+ }));
611
+ const tasks = generateServiceDeployTasks(tmpDir);
612
+ expect(tasks).toHaveLength(1);
613
+ expect(tasks[0].id).toBe('deploy-svc-stripe');
614
+ });
615
+ it('generates multiple tasks across categories', () => {
616
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
617
+ techStack: {
618
+ databases: [
619
+ {
620
+ name: 'PostgreSQL',
621
+ url: '',
622
+ description: '',
623
+ envKeys: ['DATABASE_URL'],
624
+ },
625
+ ],
626
+ services: [
627
+ {
628
+ name: 'Resend',
629
+ url: '',
630
+ description: '',
631
+ envKeys: ['RESEND_API_KEY'],
632
+ },
633
+ ],
634
+ },
635
+ }));
636
+ const tasks = generateServiceDeployTasks(tmpDir);
637
+ expect(tasks).toHaveLength(2);
638
+ const ids = tasks.map((t) => t.id);
639
+ expect(ids).toContain('deploy-svc-postgresql');
640
+ expect(ids).toContain('deploy-svc-resend');
641
+ });
642
+ });
643
+ // ── readRoadmap with dynamic service tasks ────────────────────────────
644
+ describe('readRoadmap with dynamic service tasks', () => {
645
+ it('includes dynamic service tasks in deploy list', () => {
646
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
647
+ techStack: {
648
+ services: [
649
+ {
650
+ name: 'Stripe',
651
+ url: '',
652
+ description: '',
653
+ envKeys: ['STRIPE_KEY'],
654
+ },
655
+ ],
656
+ },
657
+ }));
658
+ const data = readRoadmap(tmpDir);
659
+ const stripeTask = data.deploy.find((t) => t.id === 'deploy-svc-stripe');
660
+ expect(stripeTask).toBeDefined();
661
+ expect(stripeTask.label).toBe('Set up Stripe');
662
+ });
663
+ it('inserts service tasks after deploy-hosting and before deploy-env-vars', () => {
664
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
665
+ techStack: {
666
+ services: [
667
+ {
668
+ name: 'Stripe',
669
+ url: '',
670
+ description: '',
671
+ envKeys: ['STRIPE_KEY'],
672
+ },
673
+ ],
674
+ },
675
+ }));
676
+ const data = readRoadmap(tmpDir);
677
+ const ids = data.deploy.map((t) => t.id);
678
+ const hostingIdx = ids.indexOf('deploy-hosting');
679
+ const envVarsIdx = ids.indexOf('deploy-env-vars');
680
+ const stripeIdx = ids.indexOf('deploy-svc-stripe');
681
+ expect(hostingIdx).toBeGreaterThanOrEqual(0);
682
+ expect(envVarsIdx).toBeGreaterThanOrEqual(0);
683
+ expect(stripeIdx).toBeGreaterThan(hostingIdx);
684
+ expect(stripeIdx).toBeLessThan(envVarsIdx);
685
+ });
686
+ it('preserves completion status of persisted service tasks', () => {
687
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
688
+ techStack: {
689
+ services: [
690
+ {
691
+ name: 'Stripe',
692
+ url: '',
693
+ description: '',
694
+ envKeys: ['STRIPE_KEY'],
695
+ },
696
+ ],
697
+ },
698
+ }));
699
+ // Write roadmap with stripe task already completed
700
+ const defaults = getDefaultRoadmap();
701
+ defaults.deploy.push({
702
+ id: 'deploy-svc-stripe',
703
+ label: 'Set up Stripe',
704
+ completed: true,
705
+ completedAt: '2026-03-30T00:00:00Z',
706
+ autoDetect: 'svc-envkeys-stripe',
707
+ });
708
+ writeRoadmap(tmpDir, defaults);
709
+ const data = readRoadmap(tmpDir);
710
+ const stripeTask = data.deploy.find((t) => t.id === 'deploy-svc-stripe');
711
+ expect(stripeTask).toBeDefined();
712
+ expect(stripeTask.completed).toBe(true);
713
+ expect(stripeTask.completedAt).toBe('2026-03-30T00:00:00Z');
714
+ });
715
+ });
716
+ // ── Dynamic service auto-detection ────────────────────────────────────
717
+ describe('dynamic service auto-detection', () => {
718
+ it('marks service task complete when all envKeys are in environmentVariables', () => {
719
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
720
+ techStack: {
721
+ services: [
722
+ {
723
+ name: 'Stripe',
724
+ url: '',
725
+ description: '',
726
+ envKeys: ['STRIPE_SECRET_KEY', 'STRIPE_PUBLISHABLE_KEY'],
727
+ },
728
+ ],
729
+ },
730
+ environmentVariables: [
731
+ { key: 'STRIPE_SECRET_KEY', value: 'sk_test_123' },
732
+ { key: 'STRIPE_PUBLISHABLE_KEY', value: 'pk_test_456' },
733
+ ],
734
+ }));
735
+ const todos = [
736
+ {
737
+ id: 'deploy-svc-stripe',
738
+ label: 'Set up Stripe',
739
+ completed: false,
740
+ autoDetect: 'svc-envkeys-stripe',
741
+ },
742
+ ];
743
+ const result = checkAutoDetections(tmpDir, todos);
744
+ expect(result[0].completed).toBe(true);
745
+ expect(result[0].completedAt).toBeDefined();
746
+ });
747
+ it('does not mark service task complete when some envKeys are missing', () => {
748
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
749
+ techStack: {
750
+ services: [
751
+ {
752
+ name: 'Stripe',
753
+ url: '',
754
+ description: '',
755
+ envKeys: ['STRIPE_SECRET_KEY', 'STRIPE_PUBLISHABLE_KEY'],
756
+ },
757
+ ],
758
+ },
759
+ environmentVariables: [
760
+ { key: 'STRIPE_SECRET_KEY', value: 'sk_test_123' },
761
+ ],
762
+ }));
763
+ const todos = [
764
+ {
765
+ id: 'deploy-svc-stripe',
766
+ label: 'Set up Stripe',
767
+ completed: false,
768
+ autoDetect: 'svc-envkeys-stripe',
769
+ },
770
+ ];
771
+ const result = checkAutoDetections(tmpDir, todos);
772
+ expect(result[0].completed).toBe(false);
773
+ });
774
+ it('does not mark service task complete when envKey has empty value', () => {
775
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
776
+ techStack: {
777
+ services: [
778
+ {
779
+ name: 'Stripe',
780
+ url: '',
781
+ description: '',
782
+ envKeys: ['STRIPE_KEY'],
783
+ },
784
+ ],
785
+ },
786
+ environmentVariables: [{ key: 'STRIPE_KEY', value: '' }],
787
+ }));
788
+ const todos = [
789
+ {
790
+ id: 'deploy-svc-stripe',
791
+ label: 'Set up Stripe',
792
+ completed: false,
793
+ autoDetect: 'svc-envkeys-stripe',
794
+ },
795
+ ];
796
+ const result = checkAutoDetections(tmpDir, todos);
797
+ expect(result[0].completed).toBe(false);
798
+ });
799
+ it('handles "name" field in environmentVariables (legacy format)', () => {
800
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
801
+ techStack: {
802
+ services: [
803
+ {
804
+ name: 'Stripe',
805
+ url: '',
806
+ description: '',
807
+ envKeys: ['STRIPE_KEY'],
808
+ },
809
+ ],
810
+ },
811
+ environmentVariables: [{ name: 'STRIPE_KEY', value: 'sk_test_123' }],
812
+ }));
813
+ const todos = [
814
+ {
815
+ id: 'deploy-svc-stripe',
816
+ label: 'Set up Stripe',
817
+ completed: false,
818
+ autoDetect: 'svc-envkeys-stripe',
819
+ },
820
+ ];
821
+ const result = checkAutoDetections(tmpDir, todos);
822
+ expect(result[0].completed).toBe(true);
823
+ });
824
+ it('un-completes service task when envKeys are removed from config', () => {
825
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
826
+ techStack: {
827
+ services: [
828
+ {
829
+ name: 'Stripe',
830
+ url: '',
831
+ description: '',
832
+ envKeys: ['STRIPE_KEY'],
833
+ },
834
+ ],
835
+ },
836
+ environmentVariables: [],
837
+ }));
838
+ const todos = [
839
+ {
840
+ id: 'deploy-svc-stripe',
841
+ label: 'Set up Stripe',
842
+ completed: true,
843
+ completedAt: '2026-01-01',
844
+ autoDetect: 'svc-envkeys-stripe',
845
+ },
846
+ ];
847
+ const result = checkAutoDetections(tmpDir, todos);
848
+ expect(result[0].completed).toBe(false);
849
+ expect(result[0].completedAt).toBeUndefined();
850
+ });
851
+ });
852
+ // ── env-vars-configured auto-detection ────────────────────────────────
853
+ describe('env-vars-configured auto-detection', () => {
854
+ it('marks deploy-env-vars complete when all service envKeys are configured', () => {
855
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
856
+ techStack: {
857
+ databases: [
858
+ { name: 'Supabase', url: '', description: '', envKeys: ['DB_URL'] },
859
+ ],
860
+ services: [
861
+ {
862
+ name: 'Stripe',
863
+ url: '',
864
+ description: '',
865
+ envKeys: ['STRIPE_KEY'],
866
+ },
867
+ ],
868
+ },
869
+ environmentVariables: [
870
+ { key: 'DB_URL', value: 'postgresql://...' },
871
+ { key: 'STRIPE_KEY', value: 'sk_test_123' },
872
+ ],
873
+ }));
874
+ const todos = [
875
+ {
876
+ id: 'deploy-env-vars',
877
+ label: 'Configure environment variables',
878
+ completed: false,
879
+ autoDetect: 'env-vars-configured',
880
+ },
881
+ ];
882
+ const result = checkAutoDetections(tmpDir, todos);
883
+ expect(result[0].completed).toBe(true);
884
+ });
885
+ it('does not mark deploy-env-vars complete when some envKeys are missing', () => {
886
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
887
+ techStack: {
888
+ services: [
889
+ {
890
+ name: 'Stripe',
891
+ url: '',
892
+ description: '',
893
+ envKeys: ['STRIPE_SECRET', 'STRIPE_PUBLIC'],
894
+ },
895
+ ],
896
+ },
897
+ environmentVariables: [{ key: 'STRIPE_SECRET', value: 'sk_test_123' }],
898
+ }));
899
+ const todos = [
900
+ {
901
+ id: 'deploy-env-vars',
902
+ label: 'Configure environment variables',
903
+ completed: false,
904
+ autoDetect: 'env-vars-configured',
905
+ },
906
+ ];
907
+ const result = checkAutoDetections(tmpDir, todos);
908
+ expect(result[0].completed).toBe(false);
909
+ });
910
+ it('marks deploy-env-vars complete when no services have envKeys', () => {
911
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
912
+ techStack: {
913
+ languages: [{ name: 'TypeScript', url: '', description: '' }],
914
+ },
915
+ }));
916
+ const todos = [
917
+ {
918
+ id: 'deploy-env-vars',
919
+ label: 'Configure environment variables',
920
+ completed: false,
921
+ autoDetect: 'env-vars-configured',
922
+ },
923
+ ];
924
+ const result = checkAutoDetections(tmpDir, todos);
925
+ expect(result[0].completed).toBe(true);
926
+ });
927
+ });
928
+ // ── deploy-database defaults and auto-detection ─────────────────────
929
+ describe('deploy-database', () => {
930
+ it('deploy-database has autoDetect key in defaults', () => {
931
+ const defaults = getDefaultRoadmap();
932
+ const db = defaults.deploy.find((t) => t.id === 'deploy-database');
933
+ expect(db).toBeDefined();
934
+ expect(db.autoDetect).toBe('database-configured');
935
+ expect(db.label).toBe('Set up hosted database');
936
+ });
937
+ it('deploy-database appears after deploy-hosting in defaults', () => {
938
+ const defaults = getDefaultRoadmap();
939
+ const ids = defaults.deploy.map((t) => t.id);
940
+ const hostingIdx = ids.indexOf('deploy-hosting');
941
+ const dbIdx = ids.indexOf('deploy-database');
942
+ expect(dbIdx).toBe(hostingIdx + 1);
943
+ });
944
+ it('marks database-configured when Supabase provider with projectRef and all env vars', () => {
945
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
946
+ database: {
947
+ provider: 'supabase',
948
+ supabaseProjectRef: 'xyzref',
949
+ },
950
+ environmentVariables: [
951
+ {
952
+ key: 'NEXT_PUBLIC_SUPABASE_URL',
953
+ value: 'https://xyzref.supabase.co',
954
+ },
955
+ { key: 'NEXT_PUBLIC_SUPABASE_ANON_KEY', value: 'eyJ...' },
956
+ { key: 'DATABASE_URL', value: 'postgresql://...' },
957
+ ],
958
+ }));
959
+ const todos = [
960
+ {
961
+ id: 'deploy-database',
962
+ label: 'Set up hosted database',
963
+ completed: false,
964
+ autoDetect: 'database-configured',
965
+ },
966
+ ];
967
+ const result = checkAutoDetections(tmpDir, todos);
968
+ expect(result[0].completed).toBe(true);
969
+ expect(result[0].completedAt).toBeDefined();
970
+ });
971
+ it('does not mark database-configured when provider is missing', () => {
972
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({}));
973
+ const todos = [
974
+ {
975
+ id: 'deploy-database',
976
+ label: 'Set up hosted database',
977
+ completed: false,
978
+ autoDetect: 'database-configured',
979
+ },
980
+ ];
981
+ const result = checkAutoDetections(tmpDir, todos);
982
+ expect(result[0].completed).toBe(false);
983
+ });
984
+ it('does not mark database-configured for Supabase without projectRef', () => {
985
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
986
+ database: { provider: 'supabase' },
987
+ environmentVariables: [
988
+ { key: 'NEXT_PUBLIC_SUPABASE_URL', value: 'https://x.supabase.co' },
989
+ { key: 'NEXT_PUBLIC_SUPABASE_ANON_KEY', value: 'key' },
990
+ { key: 'DATABASE_URL', value: 'postgresql://...' },
991
+ ],
992
+ }));
993
+ const todos = [
994
+ {
995
+ id: 'deploy-database',
996
+ label: 'Set up hosted database',
997
+ completed: false,
998
+ autoDetect: 'database-configured',
999
+ },
1000
+ ];
1001
+ const result = checkAutoDetections(tmpDir, todos);
1002
+ expect(result[0].completed).toBe(false);
1003
+ });
1004
+ it('does not mark database-configured for Supabase with missing env vars', () => {
1005
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
1006
+ database: {
1007
+ provider: 'supabase',
1008
+ supabaseProjectRef: 'xyzref',
1009
+ },
1010
+ environmentVariables: [
1011
+ {
1012
+ key: 'NEXT_PUBLIC_SUPABASE_URL',
1013
+ value: 'https://xyzref.supabase.co',
1014
+ },
1015
+ // Missing ANON_KEY and DATABASE_URL
1016
+ ],
1017
+ }));
1018
+ const todos = [
1019
+ {
1020
+ id: 'deploy-database',
1021
+ label: 'Set up hosted database',
1022
+ completed: false,
1023
+ autoDetect: 'database-configured',
1024
+ },
1025
+ ];
1026
+ const result = checkAutoDetections(tmpDir, todos);
1027
+ expect(result[0].completed).toBe(false);
1028
+ });
1029
+ });
1030
+ // ── service task insertion with deploy-database ─────────────────────
1031
+ describe('service tasks insert after deploy-database', () => {
1032
+ it('inserts service tasks after deploy-database and before deploy-env-vars', () => {
1033
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
1034
+ techStack: {
1035
+ services: [
1036
+ {
1037
+ name: 'Stripe',
1038
+ url: '',
1039
+ description: '',
1040
+ envKeys: ['STRIPE_KEY'],
1041
+ },
1042
+ ],
1043
+ },
1044
+ }));
1045
+ const data = readRoadmap(tmpDir);
1046
+ const ids = data.deploy.map((t) => t.id);
1047
+ const dbIdx = ids.indexOf('deploy-database');
1048
+ const envVarsIdx = ids.indexOf('deploy-env-vars');
1049
+ const stripeIdx = ids.indexOf('deploy-svc-stripe');
1050
+ expect(dbIdx).toBeGreaterThanOrEqual(0);
1051
+ expect(stripeIdx).toBeGreaterThan(dbIdx);
1052
+ expect(stripeIdx).toBeLessThan(envVarsIdx);
1053
+ });
1054
+ });
1055
+ // ── Supabase exclusion from dynamic service tasks ────────────────────
1056
+ describe('generateServiceDeployTasks Supabase exclusion', () => {
1057
+ it('excludes Supabase from generated service tasks', () => {
1058
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
1059
+ techStack: {
1060
+ databases: [
1061
+ {
1062
+ name: 'Supabase',
1063
+ url: 'https://supabase.com',
1064
+ description: 'Hosted PostgreSQL',
1065
+ envKeys: [
1066
+ 'NEXT_PUBLIC_SUPABASE_URL',
1067
+ 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
1068
+ 'DATABASE_URL',
1069
+ ],
1070
+ },
1071
+ ],
1072
+ services: [
1073
+ {
1074
+ name: 'Stripe',
1075
+ url: '',
1076
+ description: '',
1077
+ envKeys: ['STRIPE_KEY'],
1078
+ },
1079
+ ],
1080
+ },
1081
+ }));
1082
+ const tasks = generateServiceDeployTasks(tmpDir);
1083
+ // Supabase should be excluded (handled by deploy-database), Stripe should remain
1084
+ expect(tasks).toHaveLength(1);
1085
+ expect(tasks[0].id).toBe('deploy-svc-stripe');
1086
+ });
1087
+ });
1088
+ // ── sanitizeRoadmapData preserves serviceRef ──────────────────────────
1089
+ describe('sanitizeRoadmapData with serviceRef', () => {
1090
+ it('preserves serviceRef field on tasks', () => {
1091
+ const result = sanitizeRoadmapData({
1092
+ plan: [],
1093
+ deploy: [
1094
+ {
1095
+ id: 'deploy-svc-stripe',
1096
+ label: 'Set up Stripe',
1097
+ completed: false,
1098
+ autoDetect: 'svc-envkeys-stripe',
1099
+ serviceRef: { name: 'Stripe', category: 'services' },
1100
+ },
1101
+ ],
1102
+ });
1103
+ const task = result.deploy.find((t) => t.id === 'deploy-svc-stripe');
1104
+ expect(task).toBeDefined();
1105
+ expect(task.serviceRef).toEqual({ name: 'Stripe', category: 'services' });
1106
+ });
1107
+ });
1108
+ //# sourceMappingURL=editorRoadmap.test.js.map