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
@@ -48,6 +48,10 @@ import {
48
48
  generateLeadCaptureEnvExample,
49
49
  } from './templates/website-conversion.js';
50
50
  import type { WebsiteContentContext } from './website-context.js';
51
+ import { validateWebsiteContextOrThrow } from './website-context.js';
52
+ import { scanGeneratedContent } from './website-content-scanner.js';
53
+ import { printDebugTrace, isDebugEnabled } from './website-debug.js';
54
+ import type { WebsiteDebugTrace } from './website-debug.js';
51
55
 
52
56
  /**
53
57
  * Project generation result
@@ -75,6 +79,8 @@ export interface WebsiteGeneratorOptions {
75
79
  skipReadme?: boolean;
76
80
  /** Content context from user docs for populating templates */
77
81
  contentContext?: WebsiteContentContext;
82
+ /** Skip content validation (scaffold-only use) */
83
+ skipValidation?: boolean;
78
84
  }
79
85
 
80
86
  /**
@@ -111,6 +117,7 @@ export async function generateWebsiteProject(
111
117
  skipDocker = false,
112
118
  skipReadme = false,
113
119
  contentContext,
120
+ skipValidation = false,
114
121
  } = options;
115
122
 
116
123
  const projectName = spec.name || 'my-project';
@@ -140,6 +147,51 @@ export async function generateWebsiteProject(
140
147
  await ensureDir(path.join(projectDir, '.popeye'));
141
148
  }
142
149
 
150
+ // Validate content context quality gate
151
+ if (!skipValidation) {
152
+ const validationContext = contentContext || {
153
+ productName: projectName,
154
+ features: [],
155
+ rawDocs: '',
156
+ };
157
+ validateWebsiteContextOrThrow(validationContext, projectName);
158
+ }
159
+
160
+ // Debug trace
161
+ if (isDebugEnabled() && contentContext) {
162
+ const trace: WebsiteDebugTrace = {
163
+ workspaceRoot: projectDir,
164
+ docsFound: contentContext.rawDocs
165
+ ? contentContext.rawDocs.split(/^--- .+ ---$/m).filter(Boolean).map((s, i) => ({
166
+ path: `doc-${i}`,
167
+ size: s.length,
168
+ }))
169
+ : [],
170
+ brandAssets: {
171
+ logoPath: contentContext.brand?.logoPath,
172
+ logoOutputPath: contentContext.brandAssets?.logoOutputPath || 'public/brand/logo.svg',
173
+ },
174
+ productName: {
175
+ value: contentContext.productName,
176
+ source: contentContext.rawDocs ? 'docs' : 'directory',
177
+ },
178
+ primaryColor: {
179
+ value: contentContext.brand?.primaryColor,
180
+ source: contentContext.brand?.primaryColor ? 'brand-docs' : 'defaults',
181
+ },
182
+ strategyStatus: contentContext.strategy ? 'success' : 'skipped',
183
+ templateValues: {
184
+ headline: contentContext.strategy?.messaging.headline,
185
+ features: contentContext.features.length,
186
+ pricingTiers: contentContext.pricing?.length || 0,
187
+ },
188
+ sectionsRendered: [],
189
+ validationPassed: true,
190
+ validationIssues: [],
191
+ };
192
+ printDebugTrace(trace);
193
+ }
194
+
143
195
  // Generate and write files
144
196
  const files: Array<{ path: string; content: string }> = [
145
197
  // Config files
@@ -157,7 +209,11 @@ export async function generateWebsiteProject(
157
209
  },
158
210
  {
159
211
  path: path.join(projectDir, 'tailwind.config.ts'),
160
- content: generateWebsiteTailwindConfig(),
212
+ content: generateWebsiteTailwindConfig({
213
+ primaryColor: contentContext?.brand?.primaryColor,
214
+ workspaceMode,
215
+ projectName: workspaceMode ? projectName : undefined,
216
+ }),
161
217
  },
162
218
  {
163
219
  path: path.join(projectDir, 'postcss.config.js'),
@@ -313,11 +369,11 @@ export async function generateWebsiteProject(
313
369
  });
314
370
  }
315
371
 
316
- // Copy logo to public/ if brand context has one
372
+ // Copy logo to public/brand/ if brand context has one
317
373
  if (contentContext?.brand?.logoPath) {
318
374
  try {
319
375
  const logoExt = path.extname(contentContext.brand.logoPath);
320
- const destPath = path.join(projectDir, 'public', `logo${logoExt}`);
376
+ const destPath = path.join(projectDir, 'public', 'brand', `logo${logoExt}`);
321
377
  await fs.copyFile(contentContext.brand.logoPath, destPath);
322
378
  filesCreated.push(destPath);
323
379
  } catch {
@@ -347,6 +403,18 @@ export async function generateWebsiteProject(
347
403
  filesCreated.push(file.path);
348
404
  }
349
405
 
406
+ // Post-generation content scan for placeholder fingerprints
407
+ try {
408
+ const scanResult = await scanGeneratedContent(projectDir);
409
+ if (scanResult.issues.length > 0) {
410
+ for (const issue of scanResult.issues) {
411
+ console.warn(`[content-scan] ${issue.severity}: ${issue.message} in ${issue.file}`);
412
+ }
413
+ }
414
+ } catch {
415
+ // Non-blocking: scan failures should not stop generation
416
+ }
417
+
350
418
  return {
351
419
  success: true,
352
420
  projectDir,
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Workspace root detection
3
+ * Resolves the workspace root directory by walking up the directory tree,
4
+ * looking for Popeye config, monorepo indicators, or package.json with workspaces
5
+ */
6
+
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ /**
11
+ * Resolve the workspace root directory from a given working directory
12
+ *
13
+ * Heuristic priority:
14
+ * 1. Walk ancestors: first dir containing `.popeye/` -> workspace root
15
+ * 2. First dir containing `package.json` with "workspaces" field
16
+ * 3. First dir containing `pnpm-workspace.yaml` or `turbo.json`
17
+ * 4. `cwd` (fallback)
18
+ *
19
+ * @param cwd - The current working directory
20
+ * @returns The resolved workspace root path
21
+ */
22
+ export async function resolveWorkspaceRoot(cwd: string): Promise<string> {
23
+ let current = path.resolve(cwd);
24
+ const root = path.parse(current).root;
25
+
26
+ while (current !== root) {
27
+ // Check for .popeye/ directory
28
+ if (await dirExists(path.join(current, '.popeye'))) {
29
+ return current;
30
+ }
31
+
32
+ // Check for package.json with "workspaces" field
33
+ const pkgJsonPath = path.join(current, 'package.json');
34
+ if (await fileExists(pkgJsonPath)) {
35
+ try {
36
+ const content = await fs.readFile(pkgJsonPath, 'utf-8');
37
+ const pkg = JSON.parse(content);
38
+ if (pkg.workspaces) {
39
+ return current;
40
+ }
41
+ } catch {
42
+ // Invalid JSON, skip
43
+ }
44
+ }
45
+
46
+ // Check for pnpm-workspace.yaml or turbo.json
47
+ if (
48
+ (await fileExists(path.join(current, 'pnpm-workspace.yaml'))) ||
49
+ (await fileExists(path.join(current, 'turbo.json')))
50
+ ) {
51
+ return current;
52
+ }
53
+
54
+ current = path.dirname(current);
55
+ }
56
+
57
+ return cwd;
58
+ }
59
+
60
+ /**
61
+ * Build a list of directories to scan for docs and brand assets
62
+ * Includes workspace root, its parent, and relevant subdirectories
63
+ *
64
+ * @param cwd - The current working directory
65
+ * @returns Array of directories to scan (deduplicated)
66
+ */
67
+ export async function getScanDirectories(cwd: string): Promise<string[]> {
68
+ const workspaceRoot = await resolveWorkspaceRoot(cwd);
69
+ const parentDir = path.dirname(workspaceRoot);
70
+
71
+ const candidates = [workspaceRoot, parentDir];
72
+ const subdirs = ['docs', 'brand', 'assets'];
73
+
74
+ for (const base of [workspaceRoot, parentDir]) {
75
+ for (const sub of subdirs) {
76
+ candidates.push(path.join(base, sub));
77
+ }
78
+ }
79
+
80
+ // Deduplicate by resolved path and filter to existing directories
81
+ const seen = new Set<string>();
82
+ const result: string[] = [];
83
+
84
+ for (const dir of candidates) {
85
+ const resolved = path.resolve(dir);
86
+ if (seen.has(resolved)) continue;
87
+ seen.add(resolved);
88
+
89
+ if (await dirExists(resolved)) {
90
+ result.push(resolved);
91
+ }
92
+ }
93
+
94
+ return result;
95
+ }
96
+
97
+ async function fileExists(filePath: string): Promise<boolean> {
98
+ try {
99
+ const stat = await fs.stat(filePath);
100
+ return stat.isFile();
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ async function dirExists(dirPath: string): Promise<boolean> {
107
+ try {
108
+ const stat = await fs.stat(dirPath);
109
+ return stat.isDirectory();
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
@@ -352,6 +352,20 @@ export async function storeUserDocs(
352
352
  return updateState(projectDir, { userDocs });
353
353
  }
354
354
 
355
+ /**
356
+ * Store discovered source document paths in project state
357
+ *
358
+ * @param projectDir - The project root directory
359
+ * @param sourceDocPaths - Array of absolute paths to doc files
360
+ * @returns The updated state
361
+ */
362
+ export async function storeSourceDocPaths(
363
+ projectDir: string,
364
+ sourceDocPaths: string[]
365
+ ): Promise<ProjectState> {
366
+ return updateState(projectDir, { sourceDocPaths });
367
+ }
368
+
355
369
  /**
356
370
  * Store brand context in project state
357
371
  *
@@ -201,6 +201,10 @@ export interface ProjectState {
201
201
  };
202
202
  /** Path to website strategy JSON file (relative to .popeye/) */
203
203
  websiteStrategy?: string;
204
+ /** Error message from website strategy generation (for visibility) */
205
+ strategyError?: string;
206
+ /** Absolute paths to discovered source documentation files */
207
+ sourceDocPaths?: string[];
204
208
  }
205
209
 
206
210
  /**
@@ -244,6 +248,8 @@ export const ProjectStateSchema = z.object({
244
248
  primaryColor: z.string().optional(),
245
249
  }).optional(),
246
250
  websiteStrategy: z.string().optional(),
251
+ strategyError: z.string().optional(),
252
+ sourceDocPaths: z.array(z.string()).optional(),
247
253
  });
248
254
 
249
255
  /**
@@ -21,6 +21,10 @@ import {
21
21
  generateRootDockerCompose,
22
22
  } from '../generators/templates/fullstack.js';
23
23
  import { loadState, saveState } from '../state/persistence.js';
24
+ import { buildWebsiteContext, resolveBrandAssets, validateWebsiteContext } from '../generators/website-context.js';
25
+ import type { WebsiteContentContext } from '../generators/website-context.js';
26
+ import { resolveWorkspaceRoot } from '../generators/workspace-root.js';
27
+ import { loadWebsiteStrategy } from '../workflow/website-strategy.js';
24
28
  import type { UpgradeResult } from './index.js';
25
29
 
26
30
  /**
@@ -42,6 +46,57 @@ async function pathExists(filePath: string): Promise<boolean> {
42
46
  }
43
47
  }
44
48
 
49
+ /**
50
+ * Build website content context from user docs, brand assets, and strategy
51
+ *
52
+ * Replicates the context-building pattern from website-updater.ts so that
53
+ * upgrade-generated websites get real content instead of TODO placeholders.
54
+ *
55
+ * @param projectDir - Project directory (workspace root)
56
+ * @param projectName - Project name
57
+ * @returns Content context and optional warning
58
+ */
59
+ export async function buildUpgradeContentContext(
60
+ projectDir: string,
61
+ projectName: string,
62
+ ): Promise<{ context?: WebsiteContentContext; warning?: string }> {
63
+ try {
64
+ // Build context from user docs (scans projectDir + parent via getScanDirectories)
65
+ const context = await buildWebsiteContext(projectDir, projectName);
66
+
67
+ // Apply brand context from state if available
68
+ const state = await loadState(projectDir);
69
+ if (state?.brandContext?.primaryColor) {
70
+ context.brand = { ...context.brand, primaryColor: state.brandContext.primaryColor };
71
+ }
72
+ if (state?.brandContext?.logoPath) {
73
+ context.brand = { ...context.brand, logoPath: state.brandContext.logoPath };
74
+ }
75
+
76
+ // Resolve brand assets using workspace root
77
+ const workspaceRoot = await resolveWorkspaceRoot(projectDir);
78
+ context.brandAssets = await resolveBrandAssets(workspaceRoot, context.brand);
79
+
80
+ // Load website strategy if available
81
+ const strategyData = await loadWebsiteStrategy(projectDir);
82
+ if (strategyData) {
83
+ context.strategy = strategyData.strategy;
84
+ }
85
+
86
+ // Soft validation: include quality warnings in the return value
87
+ const validation = validateWebsiteContext(context, projectName);
88
+ const validationWarnings = [...validation.issues, ...validation.warnings]
89
+ .map((w) => `[quality-gate] ${w}`);
90
+
91
+ return {
92
+ context,
93
+ warning: validationWarnings.length > 0 ? validationWarnings.join('; ') : undefined,
94
+ };
95
+ } catch (e) {
96
+ return { warning: e instanceof Error ? e.message : 'Unknown error building website context' };
97
+ }
98
+ }
99
+
45
100
  /**
46
101
  * Update state.json language field
47
102
  *
@@ -297,11 +352,21 @@ export async function upgradeFullstackToAll(
297
352
  openaiModel: 'gpt-4o',
298
353
  };
299
354
 
355
+ // Build content context from user docs, brand assets, and strategy
356
+ const { context: contentContext, warning } = await buildUpgradeContentContext(
357
+ projectDir,
358
+ projectName,
359
+ );
360
+ if (warning) {
361
+ console.warn(`[upgrade] Website context warning: ${warning}`);
362
+ }
363
+
300
364
  const result = await generateWebsiteProject(spec, projectDir, {
301
365
  baseDir: websiteDir,
302
366
  workspaceMode: true,
303
367
  skipDocker: true,
304
368
  skipReadme: true,
369
+ contentContext,
305
370
  });
306
371
 
307
372
  if (!result.success) {