popeye-cli 1.5.0 → 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 (161) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +50 -8
  3. package/dist/cli/commands/create.d.ts.map +1 -1
  4. package/dist/cli/commands/create.js +54 -4
  5. package/dist/cli/commands/create.js.map +1 -1
  6. package/dist/cli/interactive.d.ts +29 -0
  7. package/dist/cli/interactive.d.ts.map +1 -1
  8. package/dist/cli/interactive.js +90 -7
  9. package/dist/cli/interactive.js.map +1 -1
  10. package/dist/generators/all.d.ts +4 -1
  11. package/dist/generators/all.d.ts.map +1 -1
  12. package/dist/generators/all.js +36 -316
  13. package/dist/generators/all.js.map +1 -1
  14. package/dist/generators/doc-parser.d.ts +18 -3
  15. package/dist/generators/doc-parser.d.ts.map +1 -1
  16. package/dist/generators/doc-parser.js +81 -10
  17. package/dist/generators/doc-parser.js.map +1 -1
  18. package/dist/generators/frontend-design-analyzer.d.ts +30 -0
  19. package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
  20. package/dist/generators/frontend-design-analyzer.js +208 -0
  21. package/dist/generators/frontend-design-analyzer.js.map +1 -0
  22. package/dist/generators/shared-packages.d.ts +45 -0
  23. package/dist/generators/shared-packages.d.ts.map +1 -0
  24. package/dist/generators/shared-packages.js +456 -0
  25. package/dist/generators/shared-packages.js.map +1 -0
  26. package/dist/generators/templates/index.d.ts +4 -0
  27. package/dist/generators/templates/index.d.ts.map +1 -1
  28. package/dist/generators/templates/index.js +4 -0
  29. package/dist/generators/templates/index.js.map +1 -1
  30. package/dist/generators/templates/website-components.d.ts.map +1 -1
  31. package/dist/generators/templates/website-components.js +36 -11
  32. package/dist/generators/templates/website-components.js.map +1 -1
  33. package/dist/generators/templates/website-config.d.ts +15 -1
  34. package/dist/generators/templates/website-config.d.ts.map +1 -1
  35. package/dist/generators/templates/website-config.js +155 -13
  36. package/dist/generators/templates/website-config.js.map +1 -1
  37. package/dist/generators/templates/website-landing.d.ts +24 -0
  38. package/dist/generators/templates/website-landing.d.ts.map +1 -0
  39. package/dist/generators/templates/website-landing.js +276 -0
  40. package/dist/generators/templates/website-landing.js.map +1 -0
  41. package/dist/generators/templates/website-layout.d.ts +42 -0
  42. package/dist/generators/templates/website-layout.d.ts.map +1 -0
  43. package/dist/generators/templates/website-layout.js +408 -0
  44. package/dist/generators/templates/website-layout.js.map +1 -0
  45. package/dist/generators/templates/website-pricing.d.ts +11 -0
  46. package/dist/generators/templates/website-pricing.d.ts.map +1 -0
  47. package/dist/generators/templates/website-pricing.js +313 -0
  48. package/dist/generators/templates/website-pricing.js.map +1 -0
  49. package/dist/generators/templates/website-sections.d.ts +102 -0
  50. package/dist/generators/templates/website-sections.d.ts.map +1 -0
  51. package/dist/generators/templates/website-sections.js +444 -0
  52. package/dist/generators/templates/website-sections.js.map +1 -0
  53. package/dist/generators/templates/website.d.ts +10 -50
  54. package/dist/generators/templates/website.d.ts.map +1 -1
  55. package/dist/generators/templates/website.js +12 -788
  56. package/dist/generators/templates/website.js.map +1 -1
  57. package/dist/generators/website-content-scanner.d.ts +37 -0
  58. package/dist/generators/website-content-scanner.d.ts.map +1 -0
  59. package/dist/generators/website-content-scanner.js +165 -0
  60. package/dist/generators/website-content-scanner.js.map +1 -0
  61. package/dist/generators/website-context.d.ts +38 -2
  62. package/dist/generators/website-context.d.ts.map +1 -1
  63. package/dist/generators/website-context.js +179 -19
  64. package/dist/generators/website-context.js.map +1 -1
  65. package/dist/generators/website-debug.d.ts +68 -0
  66. package/dist/generators/website-debug.d.ts.map +1 -0
  67. package/dist/generators/website-debug.js +93 -0
  68. package/dist/generators/website-debug.js.map +1 -0
  69. package/dist/generators/website.d.ts +2 -0
  70. package/dist/generators/website.d.ts.map +1 -1
  71. package/dist/generators/website.js +66 -4
  72. package/dist/generators/website.js.map +1 -1
  73. package/dist/generators/workspace-root.d.ts +27 -0
  74. package/dist/generators/workspace-root.d.ts.map +1 -0
  75. package/dist/generators/workspace-root.js +100 -0
  76. package/dist/generators/workspace-root.js.map +1 -0
  77. package/dist/state/index.d.ts +8 -0
  78. package/dist/state/index.d.ts.map +1 -1
  79. package/dist/state/index.js +10 -0
  80. package/dist/state/index.js.map +1 -1
  81. package/dist/types/workflow.d.ts +6 -0
  82. package/dist/types/workflow.d.ts.map +1 -1
  83. package/dist/types/workflow.js +2 -0
  84. package/dist/types/workflow.js.map +1 -1
  85. package/dist/upgrade/handlers.d.ts +15 -0
  86. package/dist/upgrade/handlers.d.ts.map +1 -1
  87. package/dist/upgrade/handlers.js +52 -0
  88. package/dist/upgrade/handlers.js.map +1 -1
  89. package/dist/workflow/auto-fix-bundler.d.ts +37 -0
  90. package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
  91. package/dist/workflow/auto-fix-bundler.js +320 -0
  92. package/dist/workflow/auto-fix-bundler.js.map +1 -0
  93. package/dist/workflow/auto-fix.d.ts.map +1 -1
  94. package/dist/workflow/auto-fix.js +10 -3
  95. package/dist/workflow/auto-fix.js.map +1 -1
  96. package/dist/workflow/index.d.ts +1 -0
  97. package/dist/workflow/index.d.ts.map +1 -1
  98. package/dist/workflow/index.js +12 -0
  99. package/dist/workflow/index.js.map +1 -1
  100. package/dist/workflow/overview.d.ts.map +1 -1
  101. package/dist/workflow/overview.js +4 -0
  102. package/dist/workflow/overview.js.map +1 -1
  103. package/dist/workflow/plan-mode.d.ts +4 -3
  104. package/dist/workflow/plan-mode.d.ts.map +1 -1
  105. package/dist/workflow/plan-mode.js +69 -5
  106. package/dist/workflow/plan-mode.js.map +1 -1
  107. package/dist/workflow/website-strategy.d.ts +9 -0
  108. package/dist/workflow/website-strategy.d.ts.map +1 -1
  109. package/dist/workflow/website-strategy.js +73 -1
  110. package/dist/workflow/website-strategy.js.map +1 -1
  111. package/dist/workflow/website-updater.d.ts.map +1 -1
  112. package/dist/workflow/website-updater.js +15 -4
  113. package/dist/workflow/website-updater.js.map +1 -1
  114. package/package.json +1 -1
  115. package/src/cli/commands/create.ts +58 -4
  116. package/src/cli/interactive.ts +96 -7
  117. package/src/generators/all.ts +44 -332
  118. package/src/generators/doc-parser.ts +87 -10
  119. package/src/generators/frontend-design-analyzer.ts +261 -0
  120. package/src/generators/shared-packages.ts +500 -0
  121. package/src/generators/templates/index.ts +4 -0
  122. package/src/generators/templates/website-components.ts +36 -11
  123. package/src/generators/templates/website-config.ts +166 -13
  124. package/src/generators/templates/website-landing.ts +331 -0
  125. package/src/generators/templates/website-layout.ts +443 -0
  126. package/src/generators/templates/website-pricing.ts +330 -0
  127. package/src/generators/templates/website-sections.ts +541 -0
  128. package/src/generators/templates/website.ts +38 -851
  129. package/src/generators/website-content-scanner.ts +208 -0
  130. package/src/generators/website-context.ts +248 -20
  131. package/src/generators/website-debug.ts +130 -0
  132. package/src/generators/website.ts +71 -3
  133. package/src/generators/workspace-root.ts +113 -0
  134. package/src/state/index.ts +14 -0
  135. package/src/types/workflow.ts +6 -0
  136. package/src/upgrade/handlers.ts +65 -0
  137. package/src/workflow/auto-fix-bundler.ts +392 -0
  138. package/src/workflow/auto-fix.ts +11 -3
  139. package/src/workflow/index.ts +12 -0
  140. package/src/workflow/overview.ts +6 -0
  141. package/src/workflow/plan-mode.ts +81 -7
  142. package/src/workflow/website-strategy.ts +75 -1
  143. package/src/workflow/website-updater.ts +17 -6
  144. package/tests/cli/project-naming.test.ts +136 -0
  145. package/tests/generators/doc-parser.test.ts +121 -0
  146. package/tests/generators/frontend-design-analyzer.test.ts +90 -0
  147. package/tests/generators/quality-gate.test.ts +183 -0
  148. package/tests/generators/shared-packages.test.ts +83 -0
  149. package/tests/generators/website-components.test.ts +1 -1
  150. package/tests/generators/website-config.test.ts +84 -0
  151. package/tests/generators/website-content-scanner.test.ts +181 -0
  152. package/tests/generators/website-context.test.ts +109 -0
  153. package/tests/generators/website-debug.test.ts +77 -0
  154. package/tests/generators/website-landing.test.ts +188 -0
  155. package/tests/generators/website-pricing.test.ts +98 -0
  156. package/tests/generators/website-sections.test.ts +245 -0
  157. package/tests/generators/workspace-root.test.ts +105 -0
  158. package/tests/upgrade/handlers.test.ts +162 -0
  159. package/tests/workflow/auto-fix-bundler.test.ts +242 -0
  160. package/tests/workflow/plan-mode.test.ts +111 -1
  161. package/tests/workflow/website-strategy.test.ts +55 -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
+ });
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect } from 'vitest';
6
- import { parsePlanMilestones } from '../../src/workflow/plan-mode.js';
6
+ import { parsePlanMilestones, parseTaskTag, tagToAppTarget } from '../../src/workflow/plan-mode.js';
7
7
 
8
8
  describe('parsePlanMilestones', () => {
9
9
  describe('with explicit task markers', () => {
@@ -195,6 +195,76 @@ Description: Build the main API endpoints
195
195
  });
196
196
  });
197
197
 
198
+ describe('app tag handling', () => {
199
+ it('should extract tasks with [WEB] tags', () => {
200
+ const plan = `
201
+ ## Milestone 1: Website Branding
202
+
203
+ ### Task 1.1 [WEB]: Update root layout with Gateco branding
204
+ - **Description**: Replace default branding with Gateco colors and logo
205
+
206
+ ### Task 1.2 [WEB]: Create hero section component
207
+ - **Description**: Build the landing page hero with CTA
208
+ `;
209
+
210
+ const milestones = parsePlanMilestones(plan);
211
+ const allTasks = milestones.flatMap(m => m.tasks);
212
+
213
+ expect(allTasks.length).toBeGreaterThanOrEqual(2);
214
+ const taskNames = allTasks.map(t => t.name.toLowerCase());
215
+ expect(taskNames.some(n => n.includes('update root layout'))).toBe(true);
216
+ expect(taskNames.some(n => n.includes('create hero section'))).toBe(true);
217
+ });
218
+
219
+ it('should extract tasks with [INT] tags', () => {
220
+ const plan = `
221
+ ## Milestone 3: Integration
222
+
223
+ ### Task 3.1 [INT]: Wire frontend auth to backend API
224
+ - **Description**: Connect the frontend login form to the backend auth endpoint
225
+
226
+ ### Task 3.2 [INT]: Set up end-to-end test suite
227
+ - **Description**: Create E2E tests covering the full auth flow
228
+ `;
229
+
230
+ const milestones = parsePlanMilestones(plan);
231
+ const allTasks = milestones.flatMap(m => m.tasks);
232
+
233
+ expect(allTasks.length).toBeGreaterThanOrEqual(2);
234
+ const taskNames = allTasks.map(t => t.name.toLowerCase());
235
+ expect(taskNames.some(n => n.includes('wire frontend auth'))).toBe(true);
236
+ expect(taskNames.some(n => n.includes('set up end-to-end'))).toBe(true);
237
+ });
238
+
239
+ it('should extract tasks with mixed app tags across milestones', () => {
240
+ const plan = `
241
+ ## Milestone 1: Setup
242
+
243
+ ### Task 1.1 [FE]: Create React component library
244
+ - **Description**: Scaffold shared components
245
+
246
+ ### Task 1.2 [BE]: Implement REST API endpoints
247
+ - **Description**: Build core API
248
+
249
+ ### Task 1.3 [WEB]: Build marketing landing page
250
+ - **Description**: Create the public-facing website
251
+
252
+ ### Task 1.4 [INT]: Configure CI/CD pipeline
253
+ - **Description**: Set up automated deployment
254
+ `;
255
+
256
+ const milestones = parsePlanMilestones(plan);
257
+ const allTasks = milestones.flatMap(m => m.tasks);
258
+
259
+ expect(allTasks.length).toBeGreaterThanOrEqual(4);
260
+ const taskNames = allTasks.map(t => t.name.toLowerCase());
261
+ expect(taskNames.some(n => n.includes('create react component'))).toBe(true);
262
+ expect(taskNames.some(n => n.includes('implement rest api'))).toBe(true);
263
+ expect(taskNames.some(n => n.includes('build marketing landing'))).toBe(true);
264
+ expect(taskNames.some(n => n.includes('configure ci/cd'))).toBe(true);
265
+ });
266
+ });
267
+
198
268
  describe('fallback behavior', () => {
199
269
  it('should create a default milestone when no tasks found', () => {
200
270
  const plan = `
@@ -211,3 +281,43 @@ The project will do various things but no specific implementation steps are list
211
281
  });
212
282
  });
213
283
  });
284
+
285
+ describe('parseTaskTag', () => {
286
+ it('should return WEB for [WEB] tagged tasks', () => {
287
+ expect(parseTaskTag('[WEB]: Update root layout')).toBe('WEB');
288
+ expect(parseTaskTag('[web]: Update root layout')).toBe('WEB');
289
+ });
290
+
291
+ it('should return FE, BE, INT for their respective tags', () => {
292
+ expect(parseTaskTag('[FE]: Create component')).toBe('FE');
293
+ expect(parseTaskTag('[BE]: Build API')).toBe('BE');
294
+ expect(parseTaskTag('[INT]: Wire frontend to backend')).toBe('INT');
295
+ });
296
+
297
+ it('should return undefined for untagged tasks', () => {
298
+ expect(parseTaskTag('Create component')).toBeUndefined();
299
+ expect(parseTaskTag('Build API endpoints')).toBeUndefined();
300
+ });
301
+
302
+ it('should return undefined for unknown tags', () => {
303
+ expect(parseTaskTag('[UNKNOWN]: Some task')).toBeUndefined();
304
+ });
305
+ });
306
+
307
+ describe('tagToAppTarget', () => {
308
+ it('should map WEB to website', () => {
309
+ expect(tagToAppTarget('WEB')).toBe('website');
310
+ });
311
+
312
+ it('should map FE to frontend', () => {
313
+ expect(tagToAppTarget('FE')).toBe('frontend');
314
+ });
315
+
316
+ it('should map BE to backend', () => {
317
+ expect(tagToAppTarget('BE')).toBe('backend');
318
+ });
319
+
320
+ it('should map INT to unified', () => {
321
+ expect(tagToAppTarget('INT')).toBe('unified');
322
+ });
323
+ });
@@ -11,6 +11,7 @@ import {
11
11
  loadWebsiteStrategy,
12
12
  formatStrategyForPlanContext,
13
13
  isStrategyStale,
14
+ packProductContext,
14
15
  } from '../../src/workflow/website-strategy.js';
15
16
  import type {
16
17
  WebsiteStrategyDocument,
@@ -189,3 +190,57 @@ describe('isStrategyStale', () => {
189
190
  expect(loaded!.strategy.messaging.headline).toBe('Ship Code 10x Faster');
190
191
  });
191
192
  });
193
+
194
+ describe('packProductContext', () => {
195
+ it('preserves high-priority docs (spec, pricing) within budget', () => {
196
+ const context = [
197
+ '--- random-notes.md ---',
198
+ 'Some random notes about the project.',
199
+ '',
200
+ '--- product-spec.md ---',
201
+ '# Product Specification\nThis is the product specification.',
202
+ '',
203
+ '--- pricing.md ---',
204
+ '# Pricing\nFree, Pro, Enterprise tiers.',
205
+ '',
206
+ '--- color-scheme.md ---',
207
+ '# Colors\nPrimary: #2563EB',
208
+ ].join('\n');
209
+
210
+ const packed = packProductContext(context, 500);
211
+
212
+ // Spec should come first (priority 1)
213
+ const specIndex = packed.indexOf('product-spec.md');
214
+ const pricingIndex = packed.indexOf('pricing.md');
215
+ const randomIndex = packed.indexOf('random-notes.md');
216
+
217
+ expect(specIndex).toBeGreaterThanOrEqual(0);
218
+ expect(pricingIndex).toBeGreaterThanOrEqual(0);
219
+ // Spec should appear before random notes (or random notes may be cut)
220
+ if (randomIndex >= 0) {
221
+ expect(specIndex).toBeLessThan(randomIndex);
222
+ }
223
+ });
224
+
225
+ it('handles context without headers gracefully', () => {
226
+ const context = 'Just raw text without any headers';
227
+
228
+ const packed = packProductContext(context, 100);
229
+
230
+ expect(packed).toBe(context);
231
+ });
232
+
233
+ it('respects budget limit', () => {
234
+ const largeContext = [
235
+ '--- spec.md ---',
236
+ 'x'.repeat(5000),
237
+ '',
238
+ '--- pricing.md ---',
239
+ 'y'.repeat(5000),
240
+ ].join('\n');
241
+
242
+ const packed = packProductContext(largeContext, 1000);
243
+
244
+ expect(packed.length).toBeLessThanOrEqual(1100); // Some tolerance for headers
245
+ });
246
+ });