popeye-cli 1.0.1 → 1.2.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 (216) hide show
  1. package/.env.example +24 -1
  2. package/CONTRIBUTING.md +275 -0
  3. package/OPEN_SOURCE_MANIFESTO.md +172 -0
  4. package/README.md +832 -123
  5. package/dist/adapters/claude.d.ts +19 -4
  6. package/dist/adapters/claude.d.ts.map +1 -1
  7. package/dist/adapters/claude.js +908 -42
  8. package/dist/adapters/claude.js.map +1 -1
  9. package/dist/adapters/gemini.d.ts +55 -0
  10. package/dist/adapters/gemini.d.ts.map +1 -0
  11. package/dist/adapters/gemini.js +318 -0
  12. package/dist/adapters/gemini.js.map +1 -0
  13. package/dist/adapters/grok.d.ts +73 -0
  14. package/dist/adapters/grok.d.ts.map +1 -0
  15. package/dist/adapters/grok.js +430 -0
  16. package/dist/adapters/grok.js.map +1 -0
  17. package/dist/adapters/openai.d.ts +1 -1
  18. package/dist/adapters/openai.d.ts.map +1 -1
  19. package/dist/adapters/openai.js +47 -8
  20. package/dist/adapters/openai.js.map +1 -1
  21. package/dist/auth/claude.d.ts +11 -9
  22. package/dist/auth/claude.d.ts.map +1 -1
  23. package/dist/auth/claude.js +107 -71
  24. package/dist/auth/claude.js.map +1 -1
  25. package/dist/auth/gemini.d.ts +58 -0
  26. package/dist/auth/gemini.d.ts.map +1 -0
  27. package/dist/auth/gemini.js +172 -0
  28. package/dist/auth/gemini.js.map +1 -0
  29. package/dist/auth/grok.d.ts +73 -0
  30. package/dist/auth/grok.d.ts.map +1 -0
  31. package/dist/auth/grok.js +211 -0
  32. package/dist/auth/grok.js.map +1 -0
  33. package/dist/auth/index.d.ts +14 -7
  34. package/dist/auth/index.d.ts.map +1 -1
  35. package/dist/auth/index.js +41 -6
  36. package/dist/auth/index.js.map +1 -1
  37. package/dist/auth/keychain.d.ts +20 -7
  38. package/dist/auth/keychain.d.ts.map +1 -1
  39. package/dist/auth/keychain.js +85 -29
  40. package/dist/auth/keychain.js.map +1 -1
  41. package/dist/auth/openai.d.ts +2 -2
  42. package/dist/auth/openai.d.ts.map +1 -1
  43. package/dist/auth/openai.js +30 -32
  44. package/dist/auth/openai.js.map +1 -1
  45. package/dist/cli/commands/auth.d.ts +1 -1
  46. package/dist/cli/commands/auth.d.ts.map +1 -1
  47. package/dist/cli/commands/auth.js +79 -8
  48. package/dist/cli/commands/auth.js.map +1 -1
  49. package/dist/cli/commands/create.d.ts.map +1 -1
  50. package/dist/cli/commands/create.js +15 -4
  51. package/dist/cli/commands/create.js.map +1 -1
  52. package/dist/cli/interactive.d.ts.map +1 -1
  53. package/dist/cli/interactive.js +1494 -114
  54. package/dist/cli/interactive.js.map +1 -1
  55. package/dist/config/defaults.d.ts +9 -1
  56. package/dist/config/defaults.d.ts.map +1 -1
  57. package/dist/config/defaults.js +19 -2
  58. package/dist/config/defaults.js.map +1 -1
  59. package/dist/config/index.d.ts +19 -0
  60. package/dist/config/index.d.ts.map +1 -1
  61. package/dist/config/index.js +33 -1
  62. package/dist/config/index.js.map +1 -1
  63. package/dist/config/schema.d.ts +47 -0
  64. package/dist/config/schema.d.ts.map +1 -1
  65. package/dist/config/schema.js +29 -1
  66. package/dist/config/schema.js.map +1 -1
  67. package/dist/generators/fullstack.d.ts +32 -0
  68. package/dist/generators/fullstack.d.ts.map +1 -0
  69. package/dist/generators/fullstack.js +497 -0
  70. package/dist/generators/fullstack.js.map +1 -0
  71. package/dist/generators/index.d.ts +4 -3
  72. package/dist/generators/index.d.ts.map +1 -1
  73. package/dist/generators/index.js +15 -1
  74. package/dist/generators/index.js.map +1 -1
  75. package/dist/generators/python.d.ts +17 -1
  76. package/dist/generators/python.d.ts.map +1 -1
  77. package/dist/generators/python.js +34 -20
  78. package/dist/generators/python.js.map +1 -1
  79. package/dist/generators/templates/fullstack.d.ts +113 -0
  80. package/dist/generators/templates/fullstack.d.ts.map +1 -0
  81. package/dist/generators/templates/fullstack.js +1004 -0
  82. package/dist/generators/templates/fullstack.js.map +1 -0
  83. package/dist/generators/typescript.d.ts +19 -1
  84. package/dist/generators/typescript.d.ts.map +1 -1
  85. package/dist/generators/typescript.js +37 -20
  86. package/dist/generators/typescript.js.map +1 -1
  87. package/dist/state/index.d.ts +108 -0
  88. package/dist/state/index.d.ts.map +1 -1
  89. package/dist/state/index.js +551 -4
  90. package/dist/state/index.js.map +1 -1
  91. package/dist/state/registry.d.ts +52 -0
  92. package/dist/state/registry.d.ts.map +1 -0
  93. package/dist/state/registry.js +215 -0
  94. package/dist/state/registry.js.map +1 -0
  95. package/dist/types/cli.d.ts +8 -0
  96. package/dist/types/cli.d.ts.map +1 -1
  97. package/dist/types/cli.js.map +1 -1
  98. package/dist/types/consensus.d.ts +186 -4
  99. package/dist/types/consensus.d.ts.map +1 -1
  100. package/dist/types/consensus.js +35 -3
  101. package/dist/types/consensus.js.map +1 -1
  102. package/dist/types/project.d.ts +76 -0
  103. package/dist/types/project.d.ts.map +1 -1
  104. package/dist/types/project.js +1 -1
  105. package/dist/types/project.js.map +1 -1
  106. package/dist/types/workflow.d.ts +217 -16
  107. package/dist/types/workflow.d.ts.map +1 -1
  108. package/dist/types/workflow.js +40 -1
  109. package/dist/types/workflow.js.map +1 -1
  110. package/dist/workflow/auto-fix.d.ts +45 -0
  111. package/dist/workflow/auto-fix.d.ts.map +1 -0
  112. package/dist/workflow/auto-fix.js +274 -0
  113. package/dist/workflow/auto-fix.js.map +1 -0
  114. package/dist/workflow/consensus.d.ts +70 -2
  115. package/dist/workflow/consensus.d.ts.map +1 -1
  116. package/dist/workflow/consensus.js +872 -17
  117. package/dist/workflow/consensus.js.map +1 -1
  118. package/dist/workflow/execution-mode.d.ts +10 -4
  119. package/dist/workflow/execution-mode.d.ts.map +1 -1
  120. package/dist/workflow/execution-mode.js +547 -58
  121. package/dist/workflow/execution-mode.js.map +1 -1
  122. package/dist/workflow/index.d.ts +14 -2
  123. package/dist/workflow/index.d.ts.map +1 -1
  124. package/dist/workflow/index.js +69 -6
  125. package/dist/workflow/index.js.map +1 -1
  126. package/dist/workflow/milestone-workflow.d.ts +34 -0
  127. package/dist/workflow/milestone-workflow.d.ts.map +1 -0
  128. package/dist/workflow/milestone-workflow.js +414 -0
  129. package/dist/workflow/milestone-workflow.js.map +1 -0
  130. package/dist/workflow/plan-mode.d.ts +80 -3
  131. package/dist/workflow/plan-mode.d.ts.map +1 -1
  132. package/dist/workflow/plan-mode.js +767 -49
  133. package/dist/workflow/plan-mode.js.map +1 -1
  134. package/dist/workflow/plan-storage.d.ts +386 -0
  135. package/dist/workflow/plan-storage.d.ts.map +1 -0
  136. package/dist/workflow/plan-storage.js +878 -0
  137. package/dist/workflow/plan-storage.js.map +1 -0
  138. package/dist/workflow/project-verification.d.ts +37 -0
  139. package/dist/workflow/project-verification.d.ts.map +1 -0
  140. package/dist/workflow/project-verification.js +381 -0
  141. package/dist/workflow/project-verification.js.map +1 -0
  142. package/dist/workflow/task-workflow.d.ts +37 -0
  143. package/dist/workflow/task-workflow.d.ts.map +1 -0
  144. package/dist/workflow/task-workflow.js +386 -0
  145. package/dist/workflow/task-workflow.js.map +1 -0
  146. package/dist/workflow/test-runner.d.ts +9 -0
  147. package/dist/workflow/test-runner.d.ts.map +1 -1
  148. package/dist/workflow/test-runner.js +101 -5
  149. package/dist/workflow/test-runner.js.map +1 -1
  150. package/dist/workflow/ui-designer.d.ts +82 -0
  151. package/dist/workflow/ui-designer.d.ts.map +1 -0
  152. package/dist/workflow/ui-designer.js +234 -0
  153. package/dist/workflow/ui-designer.js.map +1 -0
  154. package/dist/workflow/ui-setup.d.ts +58 -0
  155. package/dist/workflow/ui-setup.d.ts.map +1 -0
  156. package/dist/workflow/ui-setup.js +685 -0
  157. package/dist/workflow/ui-setup.js.map +1 -0
  158. package/dist/workflow/ui-verification.d.ts +114 -0
  159. package/dist/workflow/ui-verification.d.ts.map +1 -0
  160. package/dist/workflow/ui-verification.js +258 -0
  161. package/dist/workflow/ui-verification.js.map +1 -0
  162. package/dist/workflow/workflow-logger.d.ts +110 -0
  163. package/dist/workflow/workflow-logger.d.ts.map +1 -0
  164. package/dist/workflow/workflow-logger.js +267 -0
  165. package/dist/workflow/workflow-logger.js.map +1 -0
  166. package/dist/workflow/workspace-manager.d.ts +342 -0
  167. package/dist/workflow/workspace-manager.d.ts.map +1 -0
  168. package/dist/workflow/workspace-manager.js +733 -0
  169. package/dist/workflow/workspace-manager.js.map +1 -0
  170. package/package.json +2 -2
  171. package/src/adapters/claude.ts +1067 -47
  172. package/src/adapters/gemini.ts +373 -0
  173. package/src/adapters/grok.ts +492 -0
  174. package/src/adapters/openai.ts +48 -9
  175. package/src/auth/claude.ts +120 -78
  176. package/src/auth/gemini.ts +207 -0
  177. package/src/auth/grok.ts +255 -0
  178. package/src/auth/index.ts +47 -9
  179. package/src/auth/keychain.ts +95 -28
  180. package/src/auth/openai.ts +29 -36
  181. package/src/cli/commands/auth.ts +89 -10
  182. package/src/cli/commands/create.ts +13 -4
  183. package/src/cli/interactive.ts +1774 -142
  184. package/src/config/defaults.ts +19 -2
  185. package/src/config/index.ts +36 -1
  186. package/src/config/schema.ts +30 -1
  187. package/src/generators/fullstack.ts +551 -0
  188. package/src/generators/index.ts +25 -1
  189. package/src/generators/python.ts +65 -20
  190. package/src/generators/templates/fullstack.ts +1047 -0
  191. package/src/generators/typescript.ts +69 -20
  192. package/src/state/index.ts +713 -4
  193. package/src/state/registry.ts +278 -0
  194. package/src/types/cli.ts +8 -0
  195. package/src/types/consensus.ts +197 -6
  196. package/src/types/project.ts +82 -1
  197. package/src/types/workflow.ts +90 -1
  198. package/src/workflow/auto-fix.ts +340 -0
  199. package/src/workflow/consensus.ts +1180 -16
  200. package/src/workflow/execution-mode.ts +673 -74
  201. package/src/workflow/index.ts +95 -6
  202. package/src/workflow/milestone-workflow.ts +576 -0
  203. package/src/workflow/plan-mode.ts +924 -50
  204. package/src/workflow/plan-storage.ts +1282 -0
  205. package/src/workflow/project-verification.ts +471 -0
  206. package/src/workflow/task-workflow.ts +528 -0
  207. package/src/workflow/test-runner.ts +120 -5
  208. package/src/workflow/ui-designer.ts +337 -0
  209. package/src/workflow/ui-setup.ts +797 -0
  210. package/src/workflow/ui-verification.ts +357 -0
  211. package/src/workflow/workflow-logger.ts +353 -0
  212. package/src/workflow/workspace-manager.ts +912 -0
  213. package/tests/config/config.test.ts +1 -1
  214. package/tests/types/consensus.test.ts +3 -3
  215. package/tests/workflow/plan-mode.test.ts +213 -0
  216. package/tests/workflow/test-runner.test.ts +5 -3
@@ -0,0 +1,733 @@
1
+ /**
2
+ * Workspace manager module
3
+ * Handles loading, saving, and querying workspace configuration for fullstack projects
4
+ * Also provides app-specific context for AI reviews
5
+ */
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ /**
9
+ * Workspace manager class
10
+ */
11
+ export class WorkspaceManager {
12
+ projectDir;
13
+ config = null;
14
+ constructor(projectDir) {
15
+ this.projectDir = projectDir;
16
+ }
17
+ /**
18
+ * Get the path to workspace.json
19
+ */
20
+ getWorkspacePath() {
21
+ return path.join(this.projectDir, '.popeye', 'workspace.json');
22
+ }
23
+ /**
24
+ * Check if this is a workspace project
25
+ */
26
+ async isWorkspaceProject() {
27
+ try {
28
+ await fs.access(this.getWorkspacePath());
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ /**
36
+ * Load workspace configuration
37
+ */
38
+ async load() {
39
+ try {
40
+ const content = await fs.readFile(this.getWorkspacePath(), 'utf-8');
41
+ this.config = JSON.parse(content);
42
+ return this.config;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * Save workspace configuration
50
+ */
51
+ async save(config) {
52
+ const workspacePath = this.getWorkspacePath();
53
+ const dir = path.dirname(workspacePath);
54
+ // Ensure directory exists
55
+ await fs.mkdir(dir, { recursive: true });
56
+ // Write config
57
+ await fs.writeFile(workspacePath, JSON.stringify(config, null, 2), 'utf-8');
58
+ this.config = config;
59
+ }
60
+ /**
61
+ * Get the loaded configuration
62
+ */
63
+ getConfig() {
64
+ return this.config;
65
+ }
66
+ /**
67
+ * Get a specific app configuration
68
+ */
69
+ getApp(appName) {
70
+ return this.config?.apps[appName];
71
+ }
72
+ /**
73
+ * Get the absolute path to an app
74
+ */
75
+ getAppPath(appName) {
76
+ const app = this.getApp(appName);
77
+ if (!app)
78
+ return null;
79
+ return path.join(this.projectDir, app.path);
80
+ }
81
+ /**
82
+ * Get all app names
83
+ */
84
+ getAppNames() {
85
+ if (!this.config)
86
+ return [];
87
+ const names = [];
88
+ if (this.config.apps.frontend)
89
+ names.push('frontend');
90
+ if (this.config.apps.backend)
91
+ names.push('backend');
92
+ return names;
93
+ }
94
+ /**
95
+ * Get test command for a specific app
96
+ */
97
+ getTestCommand(appName) {
98
+ const app = this.getApp(appName);
99
+ return app?.commands.test ?? null;
100
+ }
101
+ /**
102
+ * Get all test commands (returns object with app names as keys)
103
+ */
104
+ getAllTestCommands() {
105
+ if (!this.config)
106
+ return {};
107
+ const commands = {};
108
+ for (const appName of this.getAppNames()) {
109
+ const app = this.getApp(appName);
110
+ if (app) {
111
+ commands[appName] = {
112
+ path: path.join(this.projectDir, app.path),
113
+ command: app.commands.test,
114
+ };
115
+ }
116
+ }
117
+ return commands;
118
+ }
119
+ /**
120
+ * Get the combined test-all command
121
+ */
122
+ getTestAllCommand() {
123
+ return this.config?.commands.testAll ?? null;
124
+ }
125
+ /**
126
+ * Get lint command for a specific app
127
+ */
128
+ getLintCommand(appName) {
129
+ const app = this.getApp(appName);
130
+ return app?.commands.lint ?? null;
131
+ }
132
+ /**
133
+ * Get all lint commands
134
+ */
135
+ getAllLintCommands() {
136
+ if (!this.config)
137
+ return {};
138
+ const commands = {};
139
+ for (const appName of this.getAppNames()) {
140
+ const app = this.getApp(appName);
141
+ if (app) {
142
+ commands[appName] = {
143
+ path: path.join(this.projectDir, app.path),
144
+ command: app.commands.lint,
145
+ };
146
+ }
147
+ }
148
+ return commands;
149
+ }
150
+ /**
151
+ * Get the combined lint-all command
152
+ */
153
+ getLintAllCommand() {
154
+ return this.config?.commands.lintAll ?? null;
155
+ }
156
+ /**
157
+ * Get build command for a specific app
158
+ */
159
+ getBuildCommand(appName) {
160
+ const app = this.getApp(appName);
161
+ return app?.commands.build ?? null;
162
+ }
163
+ /**
164
+ * Get all build commands
165
+ */
166
+ getAllBuildCommands() {
167
+ if (!this.config)
168
+ return {};
169
+ const commands = {};
170
+ for (const appName of this.getAppNames()) {
171
+ const app = this.getApp(appName);
172
+ if (app) {
173
+ commands[appName] = {
174
+ path: path.join(this.projectDir, app.path),
175
+ command: app.commands.build,
176
+ };
177
+ }
178
+ }
179
+ return commands;
180
+ }
181
+ /**
182
+ * Get the combined build-all command
183
+ */
184
+ getBuildAllCommand() {
185
+ return this.config?.commands.buildAll ?? null;
186
+ }
187
+ /**
188
+ * Get dev command for a specific app
189
+ */
190
+ getDevCommand(appName) {
191
+ const app = this.getApp(appName);
192
+ return app?.commands.dev ?? null;
193
+ }
194
+ /**
195
+ * Get all dev commands
196
+ */
197
+ getAllDevCommands() {
198
+ if (!this.config)
199
+ return {};
200
+ const commands = {};
201
+ for (const appName of this.getAppNames()) {
202
+ const app = this.getApp(appName);
203
+ if (app) {
204
+ commands[appName] = {
205
+ path: path.join(this.projectDir, app.path),
206
+ command: app.commands.dev,
207
+ };
208
+ }
209
+ }
210
+ return commands;
211
+ }
212
+ /**
213
+ * Get the combined dev-all command (usually docker-compose up)
214
+ */
215
+ getDevAllCommand() {
216
+ return this.config?.commands.devAll ?? null;
217
+ }
218
+ /**
219
+ * Get docker-compose path
220
+ */
221
+ getDockerComposePath() {
222
+ if (!this.config)
223
+ return null;
224
+ return path.join(this.projectDir, this.config.docker.composePath);
225
+ }
226
+ /**
227
+ * Get context roots for an app (files to include in AI context)
228
+ */
229
+ getContextRoots(appName) {
230
+ const app = this.getApp(appName);
231
+ if (!app || !app.contextRoots)
232
+ return [];
233
+ return app.contextRoots.map((root) => path.join(this.projectDir, app.path, root));
234
+ }
235
+ /**
236
+ * Get UI spec path (for frontend)
237
+ */
238
+ getUiSpecPath() {
239
+ const frontend = this.getApp('frontend');
240
+ if (!frontend || !frontend.uiSpec)
241
+ return null;
242
+ return path.join(this.projectDir, frontend.uiSpec);
243
+ }
244
+ /**
245
+ * Get contracts path (OpenAPI spec)
246
+ */
247
+ getContractsPath() {
248
+ if (!this.config?.shared?.contracts)
249
+ return null;
250
+ return path.join(this.projectDir, this.config.shared.contracts);
251
+ }
252
+ /**
253
+ * Get app language
254
+ */
255
+ getAppLanguage(appName) {
256
+ const app = this.getApp(appName);
257
+ return app?.language ?? null;
258
+ }
259
+ /**
260
+ * Determine which app should handle a file based on path
261
+ */
262
+ getAppForFile(filePath) {
263
+ const relativePath = path.relative(this.projectDir, filePath);
264
+ for (const appName of this.getAppNames()) {
265
+ const app = this.getApp(appName);
266
+ if (app && relativePath.startsWith(app.path)) {
267
+ return appName;
268
+ }
269
+ }
270
+ return null;
271
+ }
272
+ /**
273
+ * Get review context for a specific app
274
+ * Reads key files from contextRoots to provide to AI reviewers
275
+ */
276
+ async getAppReviewContext(appName, options = {}) {
277
+ const { maxFiles = 20, maxFileSize = 50000, includeTests = true } = options;
278
+ const app = this.getApp(appName);
279
+ if (!app)
280
+ return null;
281
+ const appPath = this.getAppPath(appName);
282
+ if (!appPath)
283
+ return null;
284
+ const context = {
285
+ appName,
286
+ language: app.language,
287
+ path: app.path,
288
+ sourceFiles: [],
289
+ };
290
+ // Read files from context roots
291
+ const contextRoots = this.getContextRoots(appName);
292
+ let filesRead = 0;
293
+ for (const root of contextRoots) {
294
+ if (filesRead >= maxFiles)
295
+ break;
296
+ try {
297
+ const files = await this.readDirectoryRecursive(root, maxFileSize);
298
+ for (const file of files) {
299
+ if (filesRead >= maxFiles)
300
+ break;
301
+ context.sourceFiles.push(file);
302
+ filesRead++;
303
+ }
304
+ }
305
+ catch {
306
+ // Directory doesn't exist, skip
307
+ }
308
+ }
309
+ // Read UI spec for frontend
310
+ if (appName === 'frontend') {
311
+ const uiSpecPath = this.getUiSpecPath();
312
+ if (uiSpecPath) {
313
+ try {
314
+ context.uiSpec = await fs.readFile(uiSpecPath, 'utf-8');
315
+ }
316
+ catch {
317
+ // UI spec doesn't exist
318
+ }
319
+ }
320
+ }
321
+ // Read API contracts
322
+ const contractsPath = this.getContractsPath();
323
+ if (contractsPath) {
324
+ try {
325
+ context.apiContracts = await fs.readFile(contractsPath, 'utf-8');
326
+ }
327
+ catch {
328
+ // Contracts don't exist
329
+ }
330
+ }
331
+ // Read test files
332
+ if (includeTests) {
333
+ const testDir = path.join(appPath, appName === 'frontend' ? 'src' : 'tests');
334
+ try {
335
+ const testFiles = await this.findTestFiles(testDir, app.language);
336
+ context.testFiles = testFiles.slice(0, 5); // Limit test files
337
+ }
338
+ catch {
339
+ // Test directory doesn't exist
340
+ }
341
+ }
342
+ // Read dependencies file
343
+ try {
344
+ const depsFile = app.language === 'typescript'
345
+ ? path.join(appPath, 'package.json')
346
+ : path.join(appPath, 'pyproject.toml');
347
+ context.dependencies = await fs.readFile(depsFile, 'utf-8');
348
+ }
349
+ catch {
350
+ // Dependencies file doesn't exist
351
+ }
352
+ return context;
353
+ }
354
+ /**
355
+ * Get combined review context for fullstack project
356
+ */
357
+ async getFullstackReviewContext(projectName, projectIdea, options = {}) {
358
+ const { maxFilesPerApp = 15, includeTests = true } = options;
359
+ const context = {
360
+ projectName,
361
+ projectIdea,
362
+ };
363
+ // Get frontend context
364
+ const frontend = await this.getAppReviewContext('frontend', {
365
+ maxFiles: maxFilesPerApp,
366
+ includeTests,
367
+ });
368
+ if (frontend) {
369
+ context.frontend = frontend;
370
+ }
371
+ // Get backend context
372
+ const backend = await this.getAppReviewContext('backend', {
373
+ maxFiles: maxFilesPerApp,
374
+ includeTests,
375
+ });
376
+ if (backend) {
377
+ context.backend = backend;
378
+ }
379
+ // Get shared contracts
380
+ const contractsPath = this.getContractsPath();
381
+ if (contractsPath) {
382
+ try {
383
+ context.contracts = await fs.readFile(contractsPath, 'utf-8');
384
+ }
385
+ catch {
386
+ // Contracts don't exist
387
+ }
388
+ }
389
+ return context;
390
+ }
391
+ /**
392
+ * Format app context for AI review prompt
393
+ */
394
+ formatContextForReview(context) {
395
+ const lines = [];
396
+ lines.push(`## ${context.appName.toUpperCase()} (${context.language})`);
397
+ lines.push(`Path: ${context.path}`);
398
+ lines.push('');
399
+ if (context.dependencies) {
400
+ lines.push('### Dependencies');
401
+ lines.push('```');
402
+ lines.push(context.dependencies.slice(0, 2000)); // Limit size
403
+ lines.push('```');
404
+ lines.push('');
405
+ }
406
+ if (context.uiSpec) {
407
+ lines.push('### UI Specification');
408
+ lines.push('```json');
409
+ lines.push(context.uiSpec.slice(0, 3000));
410
+ lines.push('```');
411
+ lines.push('');
412
+ }
413
+ if (context.apiContracts) {
414
+ lines.push('### API Contracts (OpenAPI)');
415
+ lines.push('```yaml');
416
+ lines.push(context.apiContracts.slice(0, 3000));
417
+ lines.push('```');
418
+ lines.push('');
419
+ }
420
+ if (context.sourceFiles.length > 0) {
421
+ lines.push('### Key Source Files');
422
+ for (const file of context.sourceFiles) {
423
+ const relativePath = path.relative(this.projectDir, file.path);
424
+ lines.push(`#### ${relativePath}`);
425
+ lines.push('```');
426
+ lines.push(file.content.slice(0, 5000)); // Limit per file
427
+ lines.push('```');
428
+ lines.push('');
429
+ }
430
+ }
431
+ return lines.join('\n');
432
+ }
433
+ /**
434
+ * Format fullstack context for review prompt
435
+ */
436
+ formatFullstackContextForReview(context) {
437
+ const lines = [];
438
+ lines.push(`# Project: ${context.projectName}`);
439
+ if (context.projectIdea) {
440
+ lines.push(`**Idea:** ${context.projectIdea}`);
441
+ }
442
+ lines.push('');
443
+ if (context.contracts) {
444
+ lines.push('## Shared API Contracts');
445
+ lines.push('```yaml');
446
+ lines.push(context.contracts.slice(0, 3000));
447
+ lines.push('```');
448
+ lines.push('');
449
+ }
450
+ if (context.frontend) {
451
+ lines.push(this.formatContextForReview(context.frontend));
452
+ }
453
+ if (context.backend) {
454
+ lines.push(this.formatContextForReview(context.backend));
455
+ }
456
+ return lines.join('\n');
457
+ }
458
+ /**
459
+ * Determine review app target based on plan content
460
+ * Analyzes plan text to determine if it's frontend, backend, or unified
461
+ */
462
+ categorizeByPlanContent(planContent) {
463
+ const lowerContent = planContent.toLowerCase();
464
+ // Frontend indicators
465
+ const frontendKeywords = [
466
+ 'react', 'component', 'jsx', 'tsx', 'css', 'tailwind', 'ui',
467
+ 'button', 'form', 'page', 'layout', 'style', 'vite', 'frontend',
468
+ 'client', 'browser', 'dom', 'render', 'hook', 'state',
469
+ ];
470
+ // Backend indicators
471
+ const backendKeywords = [
472
+ 'api', 'endpoint', 'route', 'database', 'model', 'schema',
473
+ 'fastapi', 'flask', 'django', 'express', 'server', 'backend',
474
+ 'authentication', 'middleware', 'orm', 'sql', 'query', 'crud',
475
+ ];
476
+ const frontendScore = frontendKeywords.filter(kw => lowerContent.includes(kw)).length;
477
+ const backendScore = backendKeywords.filter(kw => lowerContent.includes(kw)).length;
478
+ // Threshold for classification
479
+ if (frontendScore > backendScore * 2 && frontendScore >= 3) {
480
+ return 'frontend';
481
+ }
482
+ if (backendScore > frontendScore * 2 && backendScore >= 3) {
483
+ return 'backend';
484
+ }
485
+ // Mixed or unclear - unified
486
+ return 'unified';
487
+ }
488
+ /**
489
+ * Read directory recursively and return file contents
490
+ */
491
+ async readDirectoryRecursive(dir, maxFileSize) {
492
+ const files = [];
493
+ try {
494
+ const entries = await fs.readdir(dir, { withFileTypes: true });
495
+ for (const entry of entries) {
496
+ const fullPath = path.join(dir, entry.name);
497
+ // Skip node_modules, __pycache__, etc.
498
+ if (entry.name.startsWith('.') ||
499
+ entry.name === 'node_modules' ||
500
+ entry.name === '__pycache__' ||
501
+ entry.name === 'dist' ||
502
+ entry.name === 'build') {
503
+ continue;
504
+ }
505
+ if (entry.isDirectory()) {
506
+ const subFiles = await this.readDirectoryRecursive(fullPath, maxFileSize);
507
+ files.push(...subFiles);
508
+ }
509
+ else if (entry.isFile()) {
510
+ // Only read code files
511
+ const ext = path.extname(entry.name);
512
+ if (['.ts', '.tsx', '.js', '.jsx', '.py', '.json', '.yaml', '.yml'].includes(ext)) {
513
+ try {
514
+ const stat = await fs.stat(fullPath);
515
+ if (stat.size <= maxFileSize) {
516
+ const content = await fs.readFile(fullPath, 'utf-8');
517
+ files.push({ path: fullPath, content });
518
+ }
519
+ }
520
+ catch {
521
+ // Skip unreadable files
522
+ }
523
+ }
524
+ }
525
+ }
526
+ }
527
+ catch {
528
+ // Directory doesn't exist or isn't readable
529
+ }
530
+ return files;
531
+ }
532
+ /**
533
+ * Find test files in a directory
534
+ */
535
+ async findTestFiles(dir, language) {
536
+ const files = [];
537
+ const testPatterns = language === 'typescript'
538
+ ? ['.test.ts', '.test.tsx', '.spec.ts', '.spec.tsx']
539
+ : ['test_', '_test.py'];
540
+ try {
541
+ const allFiles = await this.readDirectoryRecursive(dir, 30000);
542
+ for (const file of allFiles) {
543
+ const fileName = path.basename(file.path);
544
+ const isTestFile = testPatterns.some(pattern => language === 'typescript'
545
+ ? fileName.endsWith(pattern)
546
+ : fileName.startsWith(pattern) || fileName.endsWith(pattern));
547
+ if (isTestFile) {
548
+ files.push(file);
549
+ }
550
+ }
551
+ }
552
+ catch {
553
+ // Directory doesn't exist
554
+ }
555
+ return files;
556
+ }
557
+ /**
558
+ * Get feedback document paths for workspace
559
+ */
560
+ getFeedbackPaths() {
561
+ const plansDir = path.join(this.projectDir, 'docs', 'plans');
562
+ return {
563
+ master: {
564
+ unified: path.join(plansDir, 'master', 'unified', 'feedback.md'),
565
+ frontend: path.join(plansDir, 'master', 'frontend', 'feedback.md'),
566
+ backend: path.join(plansDir, 'master', 'backend', 'feedback.md'),
567
+ },
568
+ getMilestonePaths: (milestoneId) => ({
569
+ unified: path.join(plansDir, `milestone-${milestoneId}`, 'unified', 'feedback.md'),
570
+ frontend: path.join(plansDir, `milestone-${milestoneId}`, 'frontend', 'feedback.md'),
571
+ backend: path.join(plansDir, `milestone-${milestoneId}`, 'backend', 'feedback.md'),
572
+ }),
573
+ getTaskPaths: (milestoneId, taskId) => ({
574
+ unified: path.join(plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`, 'unified', 'feedback.md'),
575
+ frontend: path.join(plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`, 'frontend', 'feedback.md'),
576
+ backend: path.join(plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`, 'backend', 'feedback.md'),
577
+ }),
578
+ };
579
+ }
580
+ }
581
+ /**
582
+ * Load workspace configuration from a project directory
583
+ *
584
+ * @param projectDir - Project directory
585
+ * @returns WorkspaceConfig or null if not a workspace project
586
+ */
587
+ export async function loadWorkspace(projectDir) {
588
+ const manager = new WorkspaceManager(projectDir);
589
+ return manager.load();
590
+ }
591
+ /**
592
+ * Save workspace configuration to a project directory
593
+ *
594
+ * @param projectDir - Project directory
595
+ * @param config - Workspace configuration
596
+ */
597
+ export async function saveWorkspace(projectDir, config) {
598
+ const manager = new WorkspaceManager(projectDir);
599
+ return manager.save(config);
600
+ }
601
+ /**
602
+ * Check if a directory is a workspace project
603
+ *
604
+ * @param projectDir - Project directory
605
+ * @returns True if workspace project
606
+ */
607
+ export async function isWorkspaceProject(projectDir) {
608
+ const manager = new WorkspaceManager(projectDir);
609
+ return manager.isWorkspaceProject();
610
+ }
611
+ /**
612
+ * Get app context for AI code generation
613
+ *
614
+ * @param projectDir - Project directory
615
+ * @param appName - App name
616
+ * @returns Object with app info and context files
617
+ */
618
+ export async function getAppContext(projectDir, appName) {
619
+ const manager = new WorkspaceManager(projectDir);
620
+ const config = await manager.load();
621
+ if (!config)
622
+ return null;
623
+ const app = manager.getApp(appName);
624
+ return {
625
+ app,
626
+ language: manager.getAppLanguage(appName),
627
+ contextRoots: manager.getContextRoots(appName),
628
+ path: manager.getAppPath(appName),
629
+ };
630
+ }
631
+ /**
632
+ * Get test commands for workspace
633
+ *
634
+ * @param projectDir - Project directory
635
+ * @returns Test commands per app and combined command
636
+ */
637
+ export async function getTestCommands(projectDir) {
638
+ const manager = new WorkspaceManager(projectDir);
639
+ const config = await manager.load();
640
+ if (!config)
641
+ return null;
642
+ return {
643
+ perApp: manager.getAllTestCommands(),
644
+ combined: manager.getTestAllCommand(),
645
+ };
646
+ }
647
+ /**
648
+ * Get build commands for workspace
649
+ *
650
+ * @param projectDir - Project directory
651
+ * @returns Build commands per app and combined command
652
+ */
653
+ export async function getBuildCommands(projectDir) {
654
+ const manager = new WorkspaceManager(projectDir);
655
+ const config = await manager.load();
656
+ if (!config)
657
+ return null;
658
+ return {
659
+ perApp: manager.getAllBuildCommands(),
660
+ combined: manager.getBuildAllCommand(),
661
+ };
662
+ }
663
+ /**
664
+ * Get app-specific review context
665
+ *
666
+ * @param projectDir - Project directory
667
+ * @param appName - App name (frontend or backend)
668
+ * @returns Review context with source files and metadata
669
+ */
670
+ export async function getAppReviewContext(projectDir, appName) {
671
+ const manager = new WorkspaceManager(projectDir);
672
+ const config = await manager.load();
673
+ if (!config)
674
+ return null;
675
+ return manager.getAppReviewContext(appName);
676
+ }
677
+ /**
678
+ * Get fullstack review context for AI reviews
679
+ *
680
+ * @param projectDir - Project directory
681
+ * @param projectName - Project name
682
+ * @param projectIdea - Original project idea
683
+ * @returns Combined review context for both apps
684
+ */
685
+ export async function getFullstackReviewContext(projectDir, projectName, projectIdea) {
686
+ const manager = new WorkspaceManager(projectDir);
687
+ const config = await manager.load();
688
+ if (!config)
689
+ return null;
690
+ return manager.getFullstackReviewContext(projectName, projectIdea);
691
+ }
692
+ /**
693
+ * Format context for AI review prompt
694
+ *
695
+ * @param projectDir - Project directory
696
+ * @param projectName - Project name
697
+ * @param projectIdea - Original project idea
698
+ * @returns Formatted string for AI review
699
+ */
700
+ export async function formatContextForAIReview(projectDir, projectName, projectIdea) {
701
+ const manager = new WorkspaceManager(projectDir);
702
+ const config = await manager.load();
703
+ if (!config)
704
+ return null;
705
+ const context = await manager.getFullstackReviewContext(projectName, projectIdea);
706
+ return manager.formatFullstackContextForReview(context);
707
+ }
708
+ /**
709
+ * Categorize a task/plan as frontend, backend, or unified
710
+ *
711
+ * @param projectDir - Project directory
712
+ * @param planContent - Plan or task content to analyze
713
+ * @returns App target category
714
+ */
715
+ export async function categorizePlanContent(projectDir, planContent) {
716
+ const manager = new WorkspaceManager(projectDir);
717
+ await manager.load();
718
+ return manager.categorizeByPlanContent(planContent);
719
+ }
720
+ /**
721
+ * Get feedback paths for a workspace project
722
+ *
723
+ * @param projectDir - Project directory
724
+ * @returns Object with feedback path getters
725
+ */
726
+ export async function getWorkspaceFeedbackPaths(projectDir) {
727
+ const manager = new WorkspaceManager(projectDir);
728
+ const config = await manager.load();
729
+ if (!config)
730
+ return null;
731
+ return manager.getFeedbackPaths();
732
+ }
733
+ //# sourceMappingURL=workspace-manager.js.map