popeye-cli 1.4.7 → 1.6.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 (214) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +264 -63
  3. package/dist/adapters/gemini.d.ts +1 -0
  4. package/dist/adapters/gemini.d.ts.map +1 -1
  5. package/dist/adapters/gemini.js +9 -4
  6. package/dist/adapters/gemini.js.map +1 -1
  7. package/dist/adapters/grok.d.ts +1 -0
  8. package/dist/adapters/grok.d.ts.map +1 -1
  9. package/dist/adapters/grok.js +9 -4
  10. package/dist/adapters/grok.js.map +1 -1
  11. package/dist/adapters/openai.d.ts +1 -1
  12. package/dist/adapters/openai.d.ts.map +1 -1
  13. package/dist/adapters/openai.js +35 -9
  14. package/dist/adapters/openai.js.map +1 -1
  15. package/dist/cli/commands/create.d.ts.map +1 -1
  16. package/dist/cli/commands/create.js +54 -4
  17. package/dist/cli/commands/create.js.map +1 -1
  18. package/dist/cli/interactive.d.ts +29 -0
  19. package/dist/cli/interactive.d.ts.map +1 -1
  20. package/dist/cli/interactive.js +132 -7
  21. package/dist/cli/interactive.js.map +1 -1
  22. package/dist/generators/all.d.ts +8 -2
  23. package/dist/generators/all.d.ts.map +1 -1
  24. package/dist/generators/all.js +37 -316
  25. package/dist/generators/all.js.map +1 -1
  26. package/dist/generators/doc-parser.d.ts +64 -0
  27. package/dist/generators/doc-parser.d.ts.map +1 -0
  28. package/dist/generators/doc-parser.js +407 -0
  29. package/dist/generators/doc-parser.js.map +1 -0
  30. package/dist/generators/frontend-design-analyzer.d.ts +30 -0
  31. package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
  32. package/dist/generators/frontend-design-analyzer.js +208 -0
  33. package/dist/generators/frontend-design-analyzer.js.map +1 -0
  34. package/dist/generators/shared-packages.d.ts +45 -0
  35. package/dist/generators/shared-packages.d.ts.map +1 -0
  36. package/dist/generators/shared-packages.js +456 -0
  37. package/dist/generators/shared-packages.js.map +1 -0
  38. package/dist/generators/templates/index.d.ts +8 -0
  39. package/dist/generators/templates/index.d.ts.map +1 -1
  40. package/dist/generators/templates/index.js +8 -0
  41. package/dist/generators/templates/index.js.map +1 -1
  42. package/dist/generators/templates/website-components.d.ts +33 -0
  43. package/dist/generators/templates/website-components.d.ts.map +1 -0
  44. package/dist/generators/templates/website-components.js +303 -0
  45. package/dist/generators/templates/website-components.js.map +1 -0
  46. package/dist/generators/templates/website-config.d.ts +55 -0
  47. package/dist/generators/templates/website-config.d.ts.map +1 -0
  48. package/dist/generators/templates/website-config.js +425 -0
  49. package/dist/generators/templates/website-config.js.map +1 -0
  50. package/dist/generators/templates/website-conversion.d.ts +27 -0
  51. package/dist/generators/templates/website-conversion.d.ts.map +1 -0
  52. package/dist/generators/templates/website-conversion.js +326 -0
  53. package/dist/generators/templates/website-conversion.js.map +1 -0
  54. package/dist/generators/templates/website-landing.d.ts +24 -0
  55. package/dist/generators/templates/website-landing.d.ts.map +1 -0
  56. package/dist/generators/templates/website-landing.js +276 -0
  57. package/dist/generators/templates/website-landing.js.map +1 -0
  58. package/dist/generators/templates/website-layout.d.ts +42 -0
  59. package/dist/generators/templates/website-layout.d.ts.map +1 -0
  60. package/dist/generators/templates/website-layout.js +408 -0
  61. package/dist/generators/templates/website-layout.js.map +1 -0
  62. package/dist/generators/templates/website-pricing.d.ts +11 -0
  63. package/dist/generators/templates/website-pricing.d.ts.map +1 -0
  64. package/dist/generators/templates/website-pricing.js +313 -0
  65. package/dist/generators/templates/website-pricing.js.map +1 -0
  66. package/dist/generators/templates/website-sections.d.ts +102 -0
  67. package/dist/generators/templates/website-sections.d.ts.map +1 -0
  68. package/dist/generators/templates/website-sections.js +444 -0
  69. package/dist/generators/templates/website-sections.js.map +1 -0
  70. package/dist/generators/templates/website-seo.d.ts +76 -0
  71. package/dist/generators/templates/website-seo.d.ts.map +1 -0
  72. package/dist/generators/templates/website-seo.js +326 -0
  73. package/dist/generators/templates/website-seo.js.map +1 -0
  74. package/dist/generators/templates/website.d.ts +10 -83
  75. package/dist/generators/templates/website.d.ts.map +1 -1
  76. package/dist/generators/templates/website.js +12 -875
  77. package/dist/generators/templates/website.js.map +1 -1
  78. package/dist/generators/website-content-scanner.d.ts +37 -0
  79. package/dist/generators/website-content-scanner.d.ts.map +1 -0
  80. package/dist/generators/website-content-scanner.js +165 -0
  81. package/dist/generators/website-content-scanner.js.map +1 -0
  82. package/dist/generators/website-context.d.ts +119 -0
  83. package/dist/generators/website-context.d.ts.map +1 -0
  84. package/dist/generators/website-context.js +350 -0
  85. package/dist/generators/website-context.js.map +1 -0
  86. package/dist/generators/website-debug.d.ts +68 -0
  87. package/dist/generators/website-debug.d.ts.map +1 -0
  88. package/dist/generators/website-debug.js +93 -0
  89. package/dist/generators/website-debug.js.map +1 -0
  90. package/dist/generators/website.d.ts +5 -0
  91. package/dist/generators/website.d.ts.map +1 -1
  92. package/dist/generators/website.js +136 -11
  93. package/dist/generators/website.js.map +1 -1
  94. package/dist/generators/workspace-root.d.ts +27 -0
  95. package/dist/generators/workspace-root.d.ts.map +1 -0
  96. package/dist/generators/workspace-root.js +100 -0
  97. package/dist/generators/workspace-root.js.map +1 -0
  98. package/dist/state/index.d.ts +35 -0
  99. package/dist/state/index.d.ts.map +1 -1
  100. package/dist/state/index.js +40 -0
  101. package/dist/state/index.js.map +1 -1
  102. package/dist/types/consensus.d.ts +3 -0
  103. package/dist/types/consensus.d.ts.map +1 -1
  104. package/dist/types/consensus.js +1 -0
  105. package/dist/types/consensus.js.map +1 -1
  106. package/dist/types/website-strategy.d.ts +263 -0
  107. package/dist/types/website-strategy.d.ts.map +1 -0
  108. package/dist/types/website-strategy.js +105 -0
  109. package/dist/types/website-strategy.js.map +1 -0
  110. package/dist/types/workflow.d.ts +21 -0
  111. package/dist/types/workflow.d.ts.map +1 -1
  112. package/dist/types/workflow.js +8 -0
  113. package/dist/types/workflow.js.map +1 -1
  114. package/dist/upgrade/handlers.d.ts +15 -0
  115. package/dist/upgrade/handlers.d.ts.map +1 -1
  116. package/dist/upgrade/handlers.js +52 -0
  117. package/dist/upgrade/handlers.js.map +1 -1
  118. package/dist/workflow/auto-fix-bundler.d.ts +37 -0
  119. package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
  120. package/dist/workflow/auto-fix-bundler.js +320 -0
  121. package/dist/workflow/auto-fix-bundler.js.map +1 -0
  122. package/dist/workflow/auto-fix.d.ts.map +1 -1
  123. package/dist/workflow/auto-fix.js +10 -3
  124. package/dist/workflow/auto-fix.js.map +1 -1
  125. package/dist/workflow/consensus.d.ts.map +1 -1
  126. package/dist/workflow/consensus.js +2 -0
  127. package/dist/workflow/consensus.js.map +1 -1
  128. package/dist/workflow/execution-mode.d.ts.map +1 -1
  129. package/dist/workflow/execution-mode.js +18 -0
  130. package/dist/workflow/execution-mode.js.map +1 -1
  131. package/dist/workflow/index.d.ts +4 -0
  132. package/dist/workflow/index.d.ts.map +1 -1
  133. package/dist/workflow/index.js +37 -0
  134. package/dist/workflow/index.js.map +1 -1
  135. package/dist/workflow/overview.d.ts +89 -0
  136. package/dist/workflow/overview.d.ts.map +1 -0
  137. package/dist/workflow/overview.js +358 -0
  138. package/dist/workflow/overview.js.map +1 -0
  139. package/dist/workflow/plan-mode.d.ts +6 -4
  140. package/dist/workflow/plan-mode.d.ts.map +1 -1
  141. package/dist/workflow/plan-mode.js +148 -6
  142. package/dist/workflow/plan-mode.js.map +1 -1
  143. package/dist/workflow/website-strategy.d.ts +79 -0
  144. package/dist/workflow/website-strategy.d.ts.map +1 -0
  145. package/dist/workflow/website-strategy.js +310 -0
  146. package/dist/workflow/website-strategy.js.map +1 -0
  147. package/dist/workflow/website-updater.d.ts +17 -0
  148. package/dist/workflow/website-updater.d.ts.map +1 -0
  149. package/dist/workflow/website-updater.js +116 -0
  150. package/dist/workflow/website-updater.js.map +1 -0
  151. package/dist/workflow/workflow-logger.d.ts +1 -1
  152. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  153. package/dist/workflow/workflow-logger.js.map +1 -1
  154. package/package.json +1 -1
  155. package/src/adapters/gemini.ts +10 -4
  156. package/src/adapters/grok.ts +10 -4
  157. package/src/adapters/openai.ts +38 -6
  158. package/src/cli/commands/create.ts +58 -4
  159. package/src/cli/interactive.ts +143 -7
  160. package/src/generators/all.ts +49 -332
  161. package/src/generators/doc-parser.ts +449 -0
  162. package/src/generators/frontend-design-analyzer.ts +261 -0
  163. package/src/generators/shared-packages.ts +500 -0
  164. package/src/generators/templates/index.ts +8 -0
  165. package/src/generators/templates/website-components.ts +330 -0
  166. package/src/generators/templates/website-config.ts +444 -0
  167. package/src/generators/templates/website-conversion.ts +341 -0
  168. package/src/generators/templates/website-landing.ts +331 -0
  169. package/src/generators/templates/website-layout.ts +443 -0
  170. package/src/generators/templates/website-pricing.ts +330 -0
  171. package/src/generators/templates/website-sections.ts +541 -0
  172. package/src/generators/templates/website-seo.ts +370 -0
  173. package/src/generators/templates/website.ts +38 -905
  174. package/src/generators/website-content-scanner.ts +208 -0
  175. package/src/generators/website-context.ts +493 -0
  176. package/src/generators/website-debug.ts +130 -0
  177. package/src/generators/website.ts +178 -20
  178. package/src/generators/workspace-root.ts +113 -0
  179. package/src/state/index.ts +56 -0
  180. package/src/types/consensus.ts +3 -0
  181. package/src/types/website-strategy.ts +243 -0
  182. package/src/types/workflow.ts +21 -0
  183. package/src/upgrade/handlers.ts +65 -0
  184. package/src/workflow/auto-fix-bundler.ts +392 -0
  185. package/src/workflow/auto-fix.ts +11 -3
  186. package/src/workflow/consensus.ts +2 -0
  187. package/src/workflow/execution-mode.ts +21 -0
  188. package/src/workflow/index.ts +37 -0
  189. package/src/workflow/overview.ts +475 -0
  190. package/src/workflow/plan-mode.ts +193 -8
  191. package/src/workflow/website-strategy.ts +379 -0
  192. package/src/workflow/website-updater.ts +142 -0
  193. package/src/workflow/workflow-logger.ts +1 -0
  194. package/tests/adapters/persona-switching.test.ts +63 -0
  195. package/tests/cli/project-naming.test.ts +136 -0
  196. package/tests/generators/doc-parser.test.ts +121 -0
  197. package/tests/generators/frontend-design-analyzer.test.ts +90 -0
  198. package/tests/generators/quality-gate.test.ts +183 -0
  199. package/tests/generators/shared-packages.test.ts +83 -0
  200. package/tests/generators/website-components.test.ts +159 -0
  201. package/tests/generators/website-config.test.ts +84 -0
  202. package/tests/generators/website-content-scanner.test.ts +181 -0
  203. package/tests/generators/website-context.test.ts +331 -0
  204. package/tests/generators/website-debug.test.ts +77 -0
  205. package/tests/generators/website-landing.test.ts +188 -0
  206. package/tests/generators/website-pricing.test.ts +98 -0
  207. package/tests/generators/website-sections.test.ts +245 -0
  208. package/tests/generators/website-seo-quality.test.ts +246 -0
  209. package/tests/generators/workspace-root.test.ts +105 -0
  210. package/tests/upgrade/handlers.test.ts +162 -0
  211. package/tests/workflow/auto-fix-bundler.test.ts +242 -0
  212. package/tests/workflow/overview.test.ts +392 -0
  213. package/tests/workflow/plan-mode.test.ts +111 -1
  214. package/tests/workflow/website-strategy.test.ts +246 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Tests for upgrade handler content context builder
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import { buildUpgradeContentContext } from '../../src/upgrade/handlers.js';
10
+
11
+ // Mock external dependencies to isolate unit behavior
12
+ vi.mock('../../src/generators/website-context.js', () => ({
13
+ buildWebsiteContext: vi.fn(),
14
+ resolveBrandAssets: vi.fn(),
15
+ validateWebsiteContext: vi.fn(),
16
+ }));
17
+
18
+ vi.mock('../../src/generators/workspace-root.js', () => ({
19
+ resolveWorkspaceRoot: vi.fn(),
20
+ }));
21
+
22
+ vi.mock('../../src/workflow/website-strategy.js', () => ({
23
+ loadWebsiteStrategy: vi.fn(),
24
+ }));
25
+
26
+ vi.mock('../../src/state/persistence.js', () => ({
27
+ loadState: vi.fn(),
28
+ saveState: vi.fn(),
29
+ }));
30
+
31
+ import { buildWebsiteContext, resolveBrandAssets, validateWebsiteContext } from '../../src/generators/website-context.js';
32
+ import { resolveWorkspaceRoot } from '../../src/generators/workspace-root.js';
33
+ import { loadWebsiteStrategy } from '../../src/workflow/website-strategy.js';
34
+ import { loadState } from '../../src/state/persistence.js';
35
+
36
+ const mockBuildWebsiteContext = vi.mocked(buildWebsiteContext);
37
+ const mockResolveBrandAssets = vi.mocked(resolveBrandAssets);
38
+ const mockValidateWebsiteContext = vi.mocked(validateWebsiteContext);
39
+ const mockResolveWorkspaceRoot = vi.mocked(resolveWorkspaceRoot);
40
+ const mockLoadWebsiteStrategy = vi.mocked(loadWebsiteStrategy);
41
+ const mockLoadState = vi.mocked(loadState);
42
+
43
+ let tmpDir: string;
44
+
45
+ beforeEach(async () => {
46
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-handlers-test-'));
47
+ vi.clearAllMocks();
48
+
49
+ // Default mock returns
50
+ mockBuildWebsiteContext.mockResolvedValue({
51
+ productName: 'TestProject',
52
+ features: [{ title: 'Feature 1', description: 'A great feature' }],
53
+ rawDocs: '# Test docs',
54
+ });
55
+ mockResolveBrandAssets.mockResolvedValue({
56
+ logoSource: null,
57
+ faviconSource: null,
58
+ targets: [],
59
+ });
60
+ mockResolveWorkspaceRoot.mockResolvedValue(tmpDir);
61
+ mockLoadWebsiteStrategy.mockResolvedValue(null);
62
+ mockLoadState.mockResolvedValue(null);
63
+ mockValidateWebsiteContext.mockReturnValue({
64
+ passed: true,
65
+ issues: [],
66
+ warnings: [],
67
+ contentScore: 100,
68
+ });
69
+ });
70
+
71
+ afterEach(async () => {
72
+ await fs.rm(tmpDir, { recursive: true, force: true });
73
+ });
74
+
75
+ describe('buildUpgradeContentContext', () => {
76
+ it('should build context from user docs and return features', async () => {
77
+ const { context, warning } = await buildUpgradeContentContext(tmpDir, 'TestProject');
78
+
79
+ expect(warning).toBeUndefined();
80
+ expect(context).toBeDefined();
81
+ expect(context!.productName).toBe('TestProject');
82
+ expect(context!.features).toHaveLength(1);
83
+ expect(context!.features[0].title).toBe('Feature 1');
84
+
85
+ // Verify buildWebsiteContext was called with correct args
86
+ expect(mockBuildWebsiteContext).toHaveBeenCalledWith(tmpDir, 'TestProject');
87
+ });
88
+
89
+ it('should apply brand context from state when available', async () => {
90
+ mockLoadState.mockResolvedValue({
91
+ name: 'TestProject',
92
+ language: 'all',
93
+ brandContext: {
94
+ primaryColor: '#2563EB',
95
+ logoPath: '/path/to/logo.png',
96
+ },
97
+ } as any);
98
+
99
+ const { context } = await buildUpgradeContentContext(tmpDir, 'TestProject');
100
+
101
+ expect(context!.brand).toBeDefined();
102
+ expect(context!.brand!.primaryColor).toBe('#2563EB');
103
+ expect(context!.brand!.logoPath).toBe('/path/to/logo.png');
104
+ });
105
+
106
+ it('should load website strategy when available', async () => {
107
+ const mockStrategy = {
108
+ icp: { title: 'Developer', painPoints: ['slow builds'] },
109
+ messaging: { headline: 'Build faster', subheadline: 'Ship more' },
110
+ };
111
+ mockLoadWebsiteStrategy.mockResolvedValue({
112
+ strategy: mockStrategy as any,
113
+ metadata: { inputHash: 'abc', generatedAt: '2024-01-01', version: '1.0' },
114
+ });
115
+
116
+ const { context } = await buildUpgradeContentContext(tmpDir, 'TestProject');
117
+
118
+ expect(context!.strategy).toBeDefined();
119
+ expect(context!.strategy).toBe(mockStrategy);
120
+ expect(mockLoadWebsiteStrategy).toHaveBeenCalledWith(tmpDir);
121
+ });
122
+
123
+ it('should resolve brand assets using workspace root', async () => {
124
+ const wsRoot = '/resolved/workspace/root';
125
+ mockResolveWorkspaceRoot.mockResolvedValue(wsRoot);
126
+
127
+ const { context } = await buildUpgradeContentContext(tmpDir, 'TestProject');
128
+
129
+ expect(context).toBeDefined();
130
+ expect(mockResolveWorkspaceRoot).toHaveBeenCalledWith(tmpDir);
131
+ expect(mockResolveBrandAssets).toHaveBeenCalledWith(wsRoot, context!.brand);
132
+ });
133
+
134
+ it('should return warning on error without crashing', async () => {
135
+ mockBuildWebsiteContext.mockRejectedValue(new Error('Docs directory not found'));
136
+
137
+ const { context, warning } = await buildUpgradeContentContext(tmpDir, 'TestProject');
138
+
139
+ expect(context).toBeUndefined();
140
+ expect(warning).toBe('Docs directory not found');
141
+ });
142
+
143
+ it('should handle non-Error throws gracefully', async () => {
144
+ mockBuildWebsiteContext.mockRejectedValue('unexpected string error');
145
+
146
+ const { context, warning } = await buildUpgradeContentContext(tmpDir, 'TestProject');
147
+
148
+ expect(context).toBeUndefined();
149
+ expect(warning).toBe('Unknown error building website context');
150
+ });
151
+
152
+ it('should work when no state or strategy exists', async () => {
153
+ // Default mocks already return null for state and strategy
154
+ const { context, warning } = await buildUpgradeContentContext(tmpDir, 'TestProject');
155
+
156
+ expect(warning).toBeUndefined();
157
+ expect(context).toBeDefined();
158
+ expect(context!.strategy).toBeUndefined();
159
+ // Brand should still be whatever buildWebsiteContext returns
160
+ expect(context!.productName).toBe('TestProject');
161
+ });
162
+ });
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Tests for auto-fix-bundler: CSS/PostCSS/Tailwind/webpack error parsing and config discovery
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import {
10
+ parseBundlerErrors,
11
+ findRelatedConfigs,
12
+ parseMultiFileResponse,
13
+ } from '../../src/workflow/auto-fix-bundler.js';
14
+
15
+ let tempDir: string;
16
+
17
+ beforeEach(async () => {
18
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-bundler-'));
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await fs.rm(tempDir, { recursive: true, force: true });
23
+ });
24
+
25
+ describe('parseBundlerErrors', () => {
26
+ it('should parse Tailwind CSS "class does not exist" errors', () => {
27
+ const output = `./src/app/globals.css:1:1
28
+ Syntax error: /Users/test/project/apps/website/src/app/globals.css The \`bg-background\` class does not exist. If \`bg-background\` is a custom class, make sure it is defined within a \`@layer\` directive.
29
+
30
+ > 1 | @tailwind base;
31
+ | ^
32
+ 2 | @tailwind components;
33
+ 3 | @tailwind utilities;`;
34
+
35
+ const errors = parseBundlerErrors(output);
36
+
37
+ expect(errors.length).toBeGreaterThanOrEqual(1);
38
+ // Should find either the css pattern or the file:line:col pattern
39
+ const cssError = errors.find(e => e.type === 'css' || e.type === 'syntax');
40
+ expect(cssError).toBeDefined();
41
+ expect(cssError!.message).toContain('bg-background');
42
+ });
43
+
44
+ it('should parse CSS syntax error with absolute path', () => {
45
+ const output = `Syntax error: /Users/test/apps/website/src/globals.css The \`text-foreground\` class does not exist. If \`text-foreground\` is a custom class, make sure it is defined within a \`@layer\` directive.
46
+
47
+ Some other lines here.`;
48
+
49
+ const errors = parseBundlerErrors(output);
50
+
51
+ expect(errors.length).toBe(1);
52
+ expect(errors[0].type).toBe('css');
53
+ expect(errors[0].file).toContain('globals.css');
54
+ expect(errors[0].message).toContain('text-foreground');
55
+ });
56
+
57
+ it('should parse module not found errors', () => {
58
+ const output = `Module not found: Can't resolve '@acme/design-tokens/tailwind' in '/Users/test/apps/website'
59
+
60
+ https://nextjs.org/docs/messages/module-not-found
61
+
62
+ Import trace for requested module:
63
+ ./src/app/globals.css`;
64
+
65
+ const errors = parseBundlerErrors(output);
66
+
67
+ expect(errors.length).toBeGreaterThanOrEqual(1);
68
+ const moduleError = errors.find(e => e.type === 'module-not-found');
69
+ expect(moduleError).toBeDefined();
70
+ expect(moduleError!.message).toContain('@acme/design-tokens/tailwind');
71
+ });
72
+
73
+ it('should parse file:line:col reference for non-TS files', () => {
74
+ const output = `./src/app/globals.css:1:1
75
+ Syntax error: Something went wrong with the CSS processing.
76
+
77
+ > 1 | @tailwind base;
78
+ | ^`;
79
+
80
+ const errors = parseBundlerErrors(output);
81
+
82
+ expect(errors.length).toBeGreaterThanOrEqual(1);
83
+ const refError = errors.find(e => e.type === 'syntax');
84
+ expect(refError).toBeDefined();
85
+ expect(refError!.file).toContain('globals.css');
86
+ expect(refError!.line).toBe(1);
87
+ expect(refError!.column).toBe(1);
88
+ });
89
+
90
+ it('should parse webpack build failure with import traces', () => {
91
+ const output = `Build failed because of webpack errors
92
+
93
+ Import trace for requested module:
94
+ ./src/app/globals.css
95
+ ./src/components/Layout.tsx`;
96
+
97
+ const errors = parseBundlerErrors(output);
98
+
99
+ expect(errors.length).toBeGreaterThanOrEqual(1);
100
+ expect(errors.some(e => e.file.includes('globals.css'))).toBe(true);
101
+ });
102
+
103
+ it('should strip ANSI color codes before parsing', () => {
104
+ const output = `\x1b[31mSyntax error: /path/to/file.css The \`bg-primary\` class does not exist.\x1b[0m`;
105
+
106
+ const errors = parseBundlerErrors(output);
107
+
108
+ expect(errors.length).toBe(1);
109
+ expect(errors[0].message).toContain('bg-primary');
110
+ });
111
+
112
+ it('should de-duplicate errors from the same file', () => {
113
+ const output = `Syntax error: /path/to/globals.css The \`bg-background\` class does not exist.
114
+
115
+ Syntax error: /path/to/globals.css The \`bg-background\` class does not exist.`;
116
+
117
+ const errors = parseBundlerErrors(output);
118
+
119
+ // Should de-duplicate by file
120
+ expect(errors.length).toBe(1);
121
+ });
122
+
123
+ it('should return empty array for TypeScript-only errors', () => {
124
+ const output = `src/index.ts(10,5): error TS2304: Cannot find name 'foo'
125
+ src/utils.ts(25,12): error TS2339: Property 'bar' does not exist`;
126
+
127
+ const errors = parseBundlerErrors(output);
128
+
129
+ expect(errors.length).toBe(0);
130
+ });
131
+
132
+ it('should return empty array for generic npm errors', () => {
133
+ const output = `npm ERR! code ELIFECYCLE
134
+ npm ERR! errno 1
135
+ npm ERR! Exit status 1`;
136
+
137
+ const errors = parseBundlerErrors(output);
138
+
139
+ expect(errors.length).toBe(0);
140
+ });
141
+
142
+ it('should return empty array for empty output', () => {
143
+ expect(parseBundlerErrors('')).toEqual([]);
144
+ });
145
+ });
146
+
147
+ describe('findRelatedConfigs', () => {
148
+ it('should find tailwind and postcss configs in app directory', async () => {
149
+ const appDir = path.join(tempDir, 'apps', 'website');
150
+ await fs.mkdir(appDir, { recursive: true });
151
+ await fs.writeFile(path.join(appDir, 'tailwind.config.ts'), 'export default {}');
152
+ await fs.writeFile(path.join(appDir, 'postcss.config.js'), 'module.exports = {}');
153
+
154
+ const configs = await findRelatedConfigs(tempDir, 'apps/website/src/globals.css');
155
+
156
+ expect(configs.length).toBeGreaterThanOrEqual(2);
157
+ expect(configs.some(c => c.path.includes('tailwind.config.ts'))).toBe(true);
158
+ expect(configs.some(c => c.path.includes('postcss.config.js'))).toBe(true);
159
+ });
160
+
161
+ it('should search project root as well', async () => {
162
+ await fs.writeFile(path.join(tempDir, 'package.json'), '{"name": "test"}');
163
+
164
+ const configs = await findRelatedConfigs(tempDir, 'src/globals.css');
165
+
166
+ expect(configs.some(c => c.path.includes('package.json'))).toBe(true);
167
+ });
168
+
169
+ it('should return empty array when no configs exist', async () => {
170
+ const configs = await findRelatedConfigs(tempDir, 'src/globals.css');
171
+
172
+ expect(configs).toEqual([]);
173
+ });
174
+
175
+ it('should cap config file content at 4000 chars', async () => {
176
+ const largeContent = 'x'.repeat(10000);
177
+ await fs.writeFile(path.join(tempDir, 'package.json'), largeContent);
178
+
179
+ const configs = await findRelatedConfigs(tempDir, 'src/globals.css');
180
+
181
+ const pkg = configs.find(c => c.path.includes('package.json'));
182
+ expect(pkg).toBeDefined();
183
+ expect(pkg!.content.length).toBeLessThanOrEqual(4000);
184
+ });
185
+ });
186
+
187
+ describe('parseMultiFileResponse', () => {
188
+ it('should parse single file response', () => {
189
+ const response = `FILE: /path/to/tailwind.config.ts
190
+ \`\`\`typescript
191
+ import type { Config } from 'tailwindcss';
192
+ export default { content: [] } satisfies Config;
193
+ \`\`\``;
194
+
195
+ const results = parseMultiFileResponse(response);
196
+
197
+ expect(results.length).toBe(1);
198
+ expect(results[0].targetPath).toBe('/path/to/tailwind.config.ts');
199
+ expect(results[0].content).toContain('tailwindcss');
200
+ });
201
+
202
+ it('should parse multiple file responses', () => {
203
+ const response = `I'll fix both files:
204
+
205
+ FILE: /path/to/tailwind.config.ts
206
+ \`\`\`
207
+ export default { colors: { background: 'hsl(var(--background))' } };
208
+ \`\`\`
209
+
210
+ FILE: /path/to/globals.css
211
+ \`\`\`
212
+ @tailwind base;
213
+ @tailwind components;
214
+ @tailwind utilities;
215
+ \`\`\``;
216
+
217
+ const results = parseMultiFileResponse(response);
218
+
219
+ expect(results.length).toBe(2);
220
+ expect(results[0].targetPath).toContain('tailwind.config.ts');
221
+ expect(results[1].targetPath).toContain('globals.css');
222
+ });
223
+
224
+ it('should skip files with very short content', () => {
225
+ const response = `FILE: /path/to/file.ts
226
+ \`\`\`
227
+ tiny
228
+ \`\`\``;
229
+
230
+ const results = parseMultiFileResponse(response);
231
+
232
+ expect(results.length).toBe(0);
233
+ });
234
+
235
+ it('should return empty array for unparseable response', () => {
236
+ const response = `I'm not sure how to fix this error.`;
237
+
238
+ const results = parseMultiFileResponse(response);
239
+
240
+ expect(results.length).toBe(0);
241
+ });
242
+ });