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.
- package/CHANGELOG.md +54 -0
- package/README.md +264 -63
- package/dist/adapters/gemini.d.ts +1 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +9 -4
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +1 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +9 -4
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +35 -9
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +54 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts +29 -0
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +132 -7
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +8 -2
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +37 -316
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +64 -0
- package/dist/generators/doc-parser.d.ts.map +1 -0
- package/dist/generators/doc-parser.js +407 -0
- package/dist/generators/doc-parser.js.map +1 -0
- package/dist/generators/frontend-design-analyzer.d.ts +30 -0
- package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
- package/dist/generators/frontend-design-analyzer.js +208 -0
- package/dist/generators/frontend-design-analyzer.js.map +1 -0
- package/dist/generators/shared-packages.d.ts +45 -0
- package/dist/generators/shared-packages.d.ts.map +1 -0
- package/dist/generators/shared-packages.js +456 -0
- package/dist/generators/shared-packages.js.map +1 -0
- package/dist/generators/templates/index.d.ts +8 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +8 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website-components.d.ts +33 -0
- package/dist/generators/templates/website-components.d.ts.map +1 -0
- package/dist/generators/templates/website-components.js +303 -0
- package/dist/generators/templates/website-components.js.map +1 -0
- package/dist/generators/templates/website-config.d.ts +55 -0
- package/dist/generators/templates/website-config.d.ts.map +1 -0
- package/dist/generators/templates/website-config.js +425 -0
- package/dist/generators/templates/website-config.js.map +1 -0
- package/dist/generators/templates/website-conversion.d.ts +27 -0
- package/dist/generators/templates/website-conversion.d.ts.map +1 -0
- package/dist/generators/templates/website-conversion.js +326 -0
- package/dist/generators/templates/website-conversion.js.map +1 -0
- package/dist/generators/templates/website-landing.d.ts +24 -0
- package/dist/generators/templates/website-landing.d.ts.map +1 -0
- package/dist/generators/templates/website-landing.js +276 -0
- package/dist/generators/templates/website-landing.js.map +1 -0
- package/dist/generators/templates/website-layout.d.ts +42 -0
- package/dist/generators/templates/website-layout.d.ts.map +1 -0
- package/dist/generators/templates/website-layout.js +408 -0
- package/dist/generators/templates/website-layout.js.map +1 -0
- package/dist/generators/templates/website-pricing.d.ts +11 -0
- package/dist/generators/templates/website-pricing.d.ts.map +1 -0
- package/dist/generators/templates/website-pricing.js +313 -0
- package/dist/generators/templates/website-pricing.js.map +1 -0
- package/dist/generators/templates/website-sections.d.ts +102 -0
- package/dist/generators/templates/website-sections.d.ts.map +1 -0
- package/dist/generators/templates/website-sections.js +444 -0
- package/dist/generators/templates/website-sections.js.map +1 -0
- package/dist/generators/templates/website-seo.d.ts +76 -0
- package/dist/generators/templates/website-seo.d.ts.map +1 -0
- package/dist/generators/templates/website-seo.js +326 -0
- package/dist/generators/templates/website-seo.js.map +1 -0
- package/dist/generators/templates/website.d.ts +10 -83
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +12 -875
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-scanner.d.ts +37 -0
- package/dist/generators/website-content-scanner.d.ts.map +1 -0
- package/dist/generators/website-content-scanner.js +165 -0
- package/dist/generators/website-content-scanner.js.map +1 -0
- package/dist/generators/website-context.d.ts +119 -0
- package/dist/generators/website-context.d.ts.map +1 -0
- package/dist/generators/website-context.js +350 -0
- package/dist/generators/website-context.js.map +1 -0
- package/dist/generators/website-debug.d.ts +68 -0
- package/dist/generators/website-debug.d.ts.map +1 -0
- package/dist/generators/website-debug.js +93 -0
- package/dist/generators/website-debug.js.map +1 -0
- package/dist/generators/website.d.ts +5 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +136 -11
- package/dist/generators/website.js.map +1 -1
- package/dist/generators/workspace-root.d.ts +27 -0
- package/dist/generators/workspace-root.d.ts.map +1 -0
- package/dist/generators/workspace-root.js +100 -0
- package/dist/generators/workspace-root.js.map +1 -0
- package/dist/state/index.d.ts +35 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +40 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +3 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +1 -0
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +263 -0
- package/dist/types/website-strategy.d.ts.map +1 -0
- package/dist/types/website-strategy.js +105 -0
- package/dist/types/website-strategy.js.map +1 -0
- package/dist/types/workflow.d.ts +21 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +8 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts +15 -0
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +52 -0
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/auto-fix-bundler.d.ts +37 -0
- package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
- package/dist/workflow/auto-fix-bundler.js +320 -0
- package/dist/workflow/auto-fix-bundler.js.map +1 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +10 -3
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +2 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +18 -0
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +4 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +37 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts +89 -0
- package/dist/workflow/overview.d.ts.map +1 -0
- package/dist/workflow/overview.js +358 -0
- package/dist/workflow/overview.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +6 -4
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +148 -6
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +79 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -0
- package/dist/workflow/website-strategy.js +310 -0
- package/dist/workflow/website-strategy.js.map +1 -0
- package/dist/workflow/website-updater.d.ts +17 -0
- package/dist/workflow/website-updater.d.ts.map +1 -0
- package/dist/workflow/website-updater.js +116 -0
- package/dist/workflow/website-updater.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +1 -1
- package/dist/workflow/workflow-logger.d.ts.map +1 -1
- package/dist/workflow/workflow-logger.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +10 -4
- package/src/adapters/grok.ts +10 -4
- package/src/adapters/openai.ts +38 -6
- package/src/cli/commands/create.ts +58 -4
- package/src/cli/interactive.ts +143 -7
- package/src/generators/all.ts +49 -332
- package/src/generators/doc-parser.ts +449 -0
- package/src/generators/frontend-design-analyzer.ts +261 -0
- package/src/generators/shared-packages.ts +500 -0
- package/src/generators/templates/index.ts +8 -0
- package/src/generators/templates/website-components.ts +330 -0
- package/src/generators/templates/website-config.ts +444 -0
- package/src/generators/templates/website-conversion.ts +341 -0
- package/src/generators/templates/website-landing.ts +331 -0
- package/src/generators/templates/website-layout.ts +443 -0
- package/src/generators/templates/website-pricing.ts +330 -0
- package/src/generators/templates/website-sections.ts +541 -0
- package/src/generators/templates/website-seo.ts +370 -0
- package/src/generators/templates/website.ts +38 -905
- package/src/generators/website-content-scanner.ts +208 -0
- package/src/generators/website-context.ts +493 -0
- package/src/generators/website-debug.ts +130 -0
- package/src/generators/website.ts +178 -20
- package/src/generators/workspace-root.ts +113 -0
- package/src/state/index.ts +56 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/website-strategy.ts +243 -0
- package/src/types/workflow.ts +21 -0
- package/src/upgrade/handlers.ts +65 -0
- package/src/workflow/auto-fix-bundler.ts +392 -0
- package/src/workflow/auto-fix.ts +11 -3
- package/src/workflow/consensus.ts +2 -0
- package/src/workflow/execution-mode.ts +21 -0
- package/src/workflow/index.ts +37 -0
- package/src/workflow/overview.ts +475 -0
- package/src/workflow/plan-mode.ts +193 -8
- package/src/workflow/website-strategy.ts +379 -0
- package/src/workflow/website-updater.ts +142 -0
- package/src/workflow/workflow-logger.ts +1 -0
- package/tests/adapters/persona-switching.test.ts +63 -0
- package/tests/cli/project-naming.test.ts +136 -0
- package/tests/generators/doc-parser.test.ts +121 -0
- package/tests/generators/frontend-design-analyzer.test.ts +90 -0
- package/tests/generators/quality-gate.test.ts +183 -0
- package/tests/generators/shared-packages.test.ts +83 -0
- package/tests/generators/website-components.test.ts +159 -0
- package/tests/generators/website-config.test.ts +84 -0
- package/tests/generators/website-content-scanner.test.ts +181 -0
- package/tests/generators/website-context.test.ts +331 -0
- package/tests/generators/website-debug.test.ts +77 -0
- package/tests/generators/website-landing.test.ts +188 -0
- package/tests/generators/website-pricing.test.ts +98 -0
- package/tests/generators/website-sections.test.ts +245 -0
- package/tests/generators/website-seo-quality.test.ts +246 -0
- package/tests/generators/workspace-root.test.ts +105 -0
- package/tests/upgrade/handlers.test.ts +162 -0
- package/tests/workflow/auto-fix-bundler.test.ts +242 -0
- package/tests/workflow/overview.test.ts +392 -0
- package/tests/workflow/plan-mode.test.ts +111 -1
- package/tests/workflow/website-strategy.test.ts +246 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for project naming logic
|
|
3
|
+
* Verifies CWD-aware naming, doc-derived names, and fallback behavior
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import {
|
|
11
|
+
generateProjectName,
|
|
12
|
+
generateProjectNameFromIdea,
|
|
13
|
+
extractNameFromDocs,
|
|
14
|
+
} from '../../src/cli/interactive.js';
|
|
15
|
+
|
|
16
|
+
let tmpDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-naming-'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('generateProjectNameFromIdea', () => {
|
|
27
|
+
it('should extract explicit project name patterns', () => {
|
|
28
|
+
// "named" pattern extracts the word after it
|
|
29
|
+
expect(generateProjectNameFromIdea("Create an app named 'Gateco'")).toBe('gateco');
|
|
30
|
+
// "called" pattern extracts the word after it
|
|
31
|
+
expect(generateProjectNameFromIdea('Build something called TodoApp')).toBe('todo-app');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should extract CamelCase project names', () => {
|
|
35
|
+
// Without "for" keyword that triggers the explicit pattern, CamelCase is detected
|
|
36
|
+
expect(generateProjectNameFromIdea('Build TodoMaster with style')).toBe('todo-master');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should filter out action words from prompt text', () => {
|
|
40
|
+
// "read all files" should NOT produce "read-all-files" since those are stop words
|
|
41
|
+
const result = generateProjectNameFromIdea('read all files');
|
|
42
|
+
expect(result).not.toBe('read-all-files');
|
|
43
|
+
// Meaningful word extraction filters all three, but the fallback takes first 2 raw words
|
|
44
|
+
expect(result).toBe('read-all');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should extract meaningful words when no explicit name found', () => {
|
|
48
|
+
const result = generateProjectNameFromIdea('secure enterprise authentication system');
|
|
49
|
+
expect(result).toBe('secure-enterprise-authentication');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return my-project as last resort', () => {
|
|
53
|
+
expect(generateProjectNameFromIdea('')).toBe('my-project');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('extractNameFromDocs', () => {
|
|
58
|
+
it('should extract product name from markdown heading', async () => {
|
|
59
|
+
const docPath = path.join(tmpDir, 'Gateco-spec.md');
|
|
60
|
+
await fs.writeFile(docPath, '# Gateco\n\nSome description here.');
|
|
61
|
+
|
|
62
|
+
const name = await extractNameFromDocs(tmpDir);
|
|
63
|
+
expect(name).toBe('Gateco');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should skip README files', async () => {
|
|
67
|
+
await fs.writeFile(path.join(tmpDir, 'README.md'), '# MyProject\n\nDescription.');
|
|
68
|
+
|
|
69
|
+
const name = await extractNameFromDocs(tmpDir);
|
|
70
|
+
expect(name).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return null when no docs found', async () => {
|
|
74
|
+
const name = await extractNameFromDocs(tmpDir);
|
|
75
|
+
expect(name).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return null for non-existent directory', async () => {
|
|
79
|
+
const name = await extractNameFromDocs('/nonexistent/path/123456');
|
|
80
|
+
expect(name).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should ignore generic headings', async () => {
|
|
84
|
+
// "Home" is too short, but length >= 3 so it would pass... let's test with a generic dir name
|
|
85
|
+
await fs.writeFile(path.join(tmpDir, 'spec.md'), '# Src\n\nDescription.');
|
|
86
|
+
|
|
87
|
+
const name = await extractNameFromDocs(tmpDir);
|
|
88
|
+
// "Src" is in GENERIC_DIR_NAMES
|
|
89
|
+
expect(name).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('generateProjectName (CWD-aware)', () => {
|
|
94
|
+
it('should prefer doc-derived name over CWD basename', async () => {
|
|
95
|
+
const projectDir = path.join(tmpDir, 'SomeDir');
|
|
96
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
97
|
+
await fs.writeFile(
|
|
98
|
+
path.join(projectDir, 'product-spec.md'),
|
|
99
|
+
'# Gateco\n\nA security layer for AI.'
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const name = await generateProjectName('read all files', projectDir);
|
|
103
|
+
expect(name).toBe('gateco');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should use CWD basename when no docs found', async () => {
|
|
107
|
+
const projectDir = path.join(tmpDir, 'MyAwesomeApp');
|
|
108
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
const name = await generateProjectName('read all files', projectDir);
|
|
111
|
+
expect(name).toBe('my-awesome-app');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should skip generic CWD names and fall back to idea', async () => {
|
|
115
|
+
const projectDir = path.join(tmpDir, 'Projects');
|
|
116
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
// "Projects" is a generic dir name, so falls back to idea extraction
|
|
119
|
+
// The idea contains "named SuperApp" pattern -> extracts "SuperApp"
|
|
120
|
+
const name = await generateProjectName('Build an app named SuperApp', projectDir);
|
|
121
|
+
expect(name).toBe('super-app');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should fall back to idea extraction when no CWD provided', async () => {
|
|
125
|
+
const name = await generateProjectName('Build an app called Gateco');
|
|
126
|
+
expect(name).toBe('gateco');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should handle CamelCase CWD basenames', async () => {
|
|
130
|
+
const projectDir = path.join(tmpDir, 'MyGateco');
|
|
131
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
const name = await generateProjectName('some idea', projectDir);
|
|
134
|
+
expect(name).toBe('my-gateco');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for doc-parser module
|
|
3
|
+
* Verifies feature extraction, dev-task filtering, docs-first priority
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
extractFeatures,
|
|
9
|
+
extractProductName,
|
|
10
|
+
extractTagline,
|
|
11
|
+
extractDescription,
|
|
12
|
+
extractPrimaryColor,
|
|
13
|
+
extractPricing,
|
|
14
|
+
isSuspiciousProductName,
|
|
15
|
+
} from '../../src/generators/doc-parser.js';
|
|
16
|
+
|
|
17
|
+
describe('extractFeatures', () => {
|
|
18
|
+
it('extracts features from docs bullet list', () => {
|
|
19
|
+
const docs = `# Product\n## Features\n- **Fast Search** - Lightning fast search\n- **Auth** - Built-in authentication`;
|
|
20
|
+
const features = extractFeatures(docs);
|
|
21
|
+
expect(features.length).toBeGreaterThanOrEqual(2);
|
|
22
|
+
expect(features[0].title).toBe('Fast Search');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('extracts from docs before specification', () => {
|
|
26
|
+
const docs = `# Product\n## Features\n- **Real Feature** - From docs`;
|
|
27
|
+
const spec = `## Features\n- Implement login page\n- Fix CSS bug`;
|
|
28
|
+
const features = extractFeatures(docs, spec);
|
|
29
|
+
// Should use docs features, not spec dev tasks
|
|
30
|
+
expect(features[0].title).toBe('Real Feature');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('falls back to specification when docs have no features', () => {
|
|
34
|
+
const docs = `# Product\n## About\nJust a description.`;
|
|
35
|
+
const spec = `## Core Design Principles\n1. **Vector DB agnostic**\n2. **Embedding agnostic**`;
|
|
36
|
+
const features = extractFeatures(docs, spec);
|
|
37
|
+
expect(features.length).toBeGreaterThanOrEqual(2);
|
|
38
|
+
expect(features[0].title).toContain('Vector DB');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('filters out dev-task verbs', () => {
|
|
42
|
+
const docs = `# Tasks\n## Features\n- Implement user auth\n- Fix broken login\n- **Real Feature** - Does real stuff\n- Refactor database layer`;
|
|
43
|
+
const features = extractFeatures(docs);
|
|
44
|
+
// Dev tasks should be filtered out
|
|
45
|
+
expect(features.some(f => f.title.startsWith('Implement'))).toBe(false);
|
|
46
|
+
expect(features.some(f => f.title.startsWith('Fix'))).toBe(false);
|
|
47
|
+
expect(features.some(f => f.title.startsWith('Refactor'))).toBe(false);
|
|
48
|
+
// Real feature should remain
|
|
49
|
+
expect(features.some(f => f.title === 'Real Feature')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns empty when no feature sections exist', () => {
|
|
53
|
+
const docs = `# README\nJust a readme file.`;
|
|
54
|
+
const features = extractFeatures(docs);
|
|
55
|
+
expect(features).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('limits features to 6', () => {
|
|
59
|
+
const items = Array.from({ length: 10 }, (_, i) => `- **Feature ${i + 1}** - Description ${i + 1}`).join('\n');
|
|
60
|
+
const docs = `## Features\n${items}`;
|
|
61
|
+
const features = extractFeatures(docs);
|
|
62
|
+
expect(features.length).toBeLessThanOrEqual(6);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('extractProductName', () => {
|
|
67
|
+
it('extracts from "# Name -- tagline" heading', () => {
|
|
68
|
+
const docs = '# Gateco -- Permission-Aware Retrieval';
|
|
69
|
+
expect(extractProductName(docs)).toBe('Gateco');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('picks shortest name when multiple headings', () => {
|
|
73
|
+
const docs = '# Gateco -- main product\n# Gateco UI Color System -- colors';
|
|
74
|
+
expect(extractProductName(docs)).toBe('Gateco');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns undefined when no match', () => {
|
|
78
|
+
expect(extractProductName('Just some text')).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('extractPrimaryColor', () => {
|
|
83
|
+
it('extracts accent-primary token', () => {
|
|
84
|
+
const docs = '| `accent-primary` | `#2563EB` | Primary CTA |';
|
|
85
|
+
expect(extractPrimaryColor(docs)).toBe('#2563EB');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('skips dark background colors', () => {
|
|
89
|
+
const docs = '| bg | #0F172A |\n| accent-primary | #2563EB |';
|
|
90
|
+
expect(extractPrimaryColor(docs)).toBe('#2563EB');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('isSuspiciousProductName', () => {
|
|
95
|
+
it('flags directory-like names', () => {
|
|
96
|
+
expect(isSuspiciousProductName('my-cool-project')).toBe(true);
|
|
97
|
+
expect(isSuspiciousProductName('my-app')).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('allows real product names', () => {
|
|
101
|
+
expect(isSuspiciousProductName('Gateco')).toBe(false);
|
|
102
|
+
expect(isSuspiciousProductName('SuperApp')).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('extractPricing', () => {
|
|
107
|
+
it('extracts pricing tiers from markdown table', () => {
|
|
108
|
+
const docs = `## Pricing\n| Plan | Price |\n|---|---|\n| Free | Free |\n| Pro | $99/month minimum |\n| Enterprise | Custom pricing |`;
|
|
109
|
+
const tiers = extractPricing(docs);
|
|
110
|
+
expect(tiers).toBeDefined();
|
|
111
|
+
expect(tiers!.length).toBe(3);
|
|
112
|
+
expect(tiers![0].name).toContain('Free');
|
|
113
|
+
expect(tiers![1].price).toBe('$99');
|
|
114
|
+
expect(tiers![2].price).toBe('Custom');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns undefined when no pricing found', () => {
|
|
118
|
+
const docs = '# Product\nJust a product.';
|
|
119
|
+
expect(extractPricing(docs)).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for frontend design language analyzer
|
|
3
|
+
* Verifies CSS variable extraction, component library detection, and tailwind parsing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { analyzeFrontendDesign } from '../../src/generators/frontend-design-analyzer.js';
|
|
11
|
+
|
|
12
|
+
describe('analyzeFrontendDesign', () => {
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-fe-design-'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns null when no frontend app directory exists', async () => {
|
|
24
|
+
const result = await analyzeFrontendDesign(tmpDir);
|
|
25
|
+
|
|
26
|
+
expect(result).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('extracts CSS custom properties from index.css', async () => {
|
|
30
|
+
const frontendDir = path.join(tmpDir, 'apps', 'frontend', 'src');
|
|
31
|
+
await fs.mkdir(frontendDir, { recursive: true });
|
|
32
|
+
await fs.writeFile(
|
|
33
|
+
path.join(frontendDir, 'index.css'),
|
|
34
|
+
`:root {
|
|
35
|
+
--primary: 222.2 47.4% 11.2%;
|
|
36
|
+
--radius: 0.5rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.dark {
|
|
40
|
+
--primary: 210 40% 98%;
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
);
|
|
44
|
+
// Need package.json for component lib detection
|
|
45
|
+
await fs.writeFile(
|
|
46
|
+
path.join(tmpDir, 'apps', 'frontend', 'package.json'),
|
|
47
|
+
JSON.stringify({ dependencies: { '@shadcn/ui': '^1.0.0' } })
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const result = await analyzeFrontendDesign(tmpDir);
|
|
51
|
+
|
|
52
|
+
expect(result).not.toBeNull();
|
|
53
|
+
expect(result!.primaryColor).toBeDefined();
|
|
54
|
+
expect(result!.borderRadius).toBe('0.5rem');
|
|
55
|
+
expect(result!.darkMode).toBe(true);
|
|
56
|
+
expect(result!.source).toBe('css-variables');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('detects shadcn component library from package.json', async () => {
|
|
60
|
+
const frontendDir = path.join(tmpDir, 'apps', 'frontend');
|
|
61
|
+
await fs.mkdir(frontendDir, { recursive: true });
|
|
62
|
+
await fs.writeFile(
|
|
63
|
+
path.join(frontendDir, 'package.json'),
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
dependencies: { '@shadcn/ui': '^1.0.0', react: '^18.0.0' },
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const result = await analyzeFrontendDesign(tmpDir);
|
|
70
|
+
|
|
71
|
+
expect(result).not.toBeNull();
|
|
72
|
+
expect(result!.componentLibrary).toBe('shadcn');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('detects MUI component library', async () => {
|
|
76
|
+
const frontendDir = path.join(tmpDir, 'apps', 'frontend');
|
|
77
|
+
await fs.mkdir(frontendDir, { recursive: true });
|
|
78
|
+
await fs.writeFile(
|
|
79
|
+
path.join(frontendDir, 'package.json'),
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
dependencies: { '@mui/material': '^5.0.0', react: '^18.0.0' },
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const result = await analyzeFrontendDesign(tmpDir);
|
|
86
|
+
|
|
87
|
+
expect(result).not.toBeNull();
|
|
88
|
+
expect(result!.componentLibrary).toBe('mui');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for website content quality gate
|
|
3
|
+
* Verifies that validateWebsiteContextOrThrow blocks generic websites
|
|
4
|
+
* and validateWebsiteContext returns structured soft validation results
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
validateWebsiteContextOrThrow,
|
|
10
|
+
validateWebsiteContext,
|
|
11
|
+
} from '../../src/generators/website-context.js';
|
|
12
|
+
import type { WebsiteContentContext } from '../../src/generators/website-context.js';
|
|
13
|
+
|
|
14
|
+
function makeContext(overrides: Partial<WebsiteContentContext> = {}): WebsiteContentContext {
|
|
15
|
+
return {
|
|
16
|
+
productName: 'Gateco',
|
|
17
|
+
features: [
|
|
18
|
+
{ title: 'Access Control', description: 'Fine-grained permissions' },
|
|
19
|
+
{ title: 'Vector DB Agnostic', description: 'Works with any vector database' },
|
|
20
|
+
],
|
|
21
|
+
rawDocs: '--- spec.md ---\n# Gateco\nLong enough content to pass validation threshold.\n' + 'x'.repeat(100),
|
|
22
|
+
strategy: {
|
|
23
|
+
icp: { primaryPersona: 'devs', painPoints: [], goals: [], objections: [] },
|
|
24
|
+
positioning: { category: 'Security', differentiators: [], valueProposition: 'Secure AI', proofPoints: [] },
|
|
25
|
+
messaging: { headline: 'h', subheadline: 's', elevatorPitch: 'e', longDescription: 'l' },
|
|
26
|
+
seoStrategy: { primaryKeywords: [], secondaryKeywords: [], longTailKeywords: [], titleTemplates: {}, metaDescriptions: {} },
|
|
27
|
+
siteArchitecture: { pages: [], navigation: [], footerSections: [] },
|
|
28
|
+
conversionStrategy: { primaryCta: { text: 'Go', href: '/' }, secondaryCta: { text: 'More', href: '/' }, trustSignals: [], socialProof: [], leadCapture: 'none' },
|
|
29
|
+
competitiveContext: { category: 'sec', competitors: [], differentiators: [] },
|
|
30
|
+
},
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('validateWebsiteContextOrThrow', () => {
|
|
36
|
+
it('passes validation with valid product context', () => {
|
|
37
|
+
const context = makeContext();
|
|
38
|
+
|
|
39
|
+
expect(() => validateWebsiteContextOrThrow(context, 'gateco')).not.toThrow();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('throws on suspicious product name (directory-like)', () => {
|
|
43
|
+
const context = makeContext({ productName: 'read-all-files' });
|
|
44
|
+
|
|
45
|
+
expect(() => validateWebsiteContextOrThrow(context, 'read-all-files')).toThrow(
|
|
46
|
+
/looks like a directory name/
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('throws when no docs found', () => {
|
|
51
|
+
const context = makeContext({ rawDocs: '' });
|
|
52
|
+
|
|
53
|
+
expect(() => validateWebsiteContextOrThrow(context, 'test')).toThrow(
|
|
54
|
+
/No project documentation found/
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('throws when no features extracted', () => {
|
|
59
|
+
const context = makeContext({ features: [] });
|
|
60
|
+
|
|
61
|
+
expect(() => validateWebsiteContextOrThrow(context, 'test')).toThrow(
|
|
62
|
+
/No product features extracted/
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('includes POPEYE_DEBUG_WEBSITE hint in error message', () => {
|
|
67
|
+
const context = makeContext({ rawDocs: '' });
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
validateWebsiteContextOrThrow(context, 'test');
|
|
71
|
+
expect.fail('Should have thrown');
|
|
72
|
+
} catch (error) {
|
|
73
|
+
expect((error as Error).message).toContain('POPEYE_DEBUG_WEBSITE=1');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('still throws when passed is false from validateWebsiteContext', () => {
|
|
78
|
+
// Context with multiple blocking issues
|
|
79
|
+
const context = makeContext({
|
|
80
|
+
productName: 'my-app',
|
|
81
|
+
rawDocs: '',
|
|
82
|
+
features: [],
|
|
83
|
+
strategy: undefined,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(() => validateWebsiteContextOrThrow(context, 'my-app')).toThrow(
|
|
87
|
+
/Website generation blocked/
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('validateWebsiteContext (soft mode)', () => {
|
|
93
|
+
it('returns passed=true with no issues for valid context', () => {
|
|
94
|
+
const context = makeContext({ tagline: 'Secure your AI' });
|
|
95
|
+
const result = validateWebsiteContext(context, 'gateco');
|
|
96
|
+
|
|
97
|
+
expect(result.passed).toBe(true);
|
|
98
|
+
expect(result.issues).toHaveLength(0);
|
|
99
|
+
expect(result.contentScore).toBeGreaterThanOrEqual(90);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns warnings without marking passed=false', () => {
|
|
103
|
+
// Valid context but missing tagline triggers a warning, not an issue
|
|
104
|
+
const context = makeContext({ tagline: undefined });
|
|
105
|
+
const result = validateWebsiteContext(context, 'gateco');
|
|
106
|
+
|
|
107
|
+
expect(result.passed).toBe(true);
|
|
108
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
109
|
+
expect(result.warnings.some((w) => /tagline/i.test(w))).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('detects default pricing fingerprint as a warning', () => {
|
|
113
|
+
const context = makeContext({
|
|
114
|
+
pricing: [
|
|
115
|
+
{ name: 'Free', price: '$0', description: 'Basic', features: ['1 user'], cta: 'Start' },
|
|
116
|
+
{ name: 'Pro', price: '$29', description: 'Team', features: ['10 users'], cta: 'Go', featured: true },
|
|
117
|
+
{ name: 'Enterprise', price: 'Custom', description: 'Scale', features: ['Unlimited'], cta: 'Contact' },
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
const result = validateWebsiteContext(context, 'gateco');
|
|
121
|
+
|
|
122
|
+
expect(result.passed).toBe(true);
|
|
123
|
+
expect(result.warnings.some((w) => /default values/i.test(w))).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('does not flag custom pricing as default', () => {
|
|
127
|
+
const context = makeContext({
|
|
128
|
+
pricing: [
|
|
129
|
+
{ name: 'Hobby', price: '$9', description: 'For side projects', features: ['5 APIs'], cta: 'Start' },
|
|
130
|
+
{ name: 'Team', price: '$49', description: 'For teams', features: ['50 APIs'], cta: 'Go' },
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
const result = validateWebsiteContext(context, 'gateco');
|
|
134
|
+
|
|
135
|
+
expect(result.warnings.some((w) => /default values/i.test(w))).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('decreases contentScore with more defaults', () => {
|
|
139
|
+
const minimal = makeContext({
|
|
140
|
+
tagline: undefined,
|
|
141
|
+
description: undefined,
|
|
142
|
+
brand: undefined,
|
|
143
|
+
});
|
|
144
|
+
const rich = makeContext({
|
|
145
|
+
tagline: 'Secure your AI',
|
|
146
|
+
description: 'The best AI security platform',
|
|
147
|
+
brand: { primaryColor: '#3B82F6' },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const minResult = validateWebsiteContext(minimal, 'gateco');
|
|
151
|
+
const richResult = validateWebsiteContext(rich, 'gateco');
|
|
152
|
+
|
|
153
|
+
expect(richResult.contentScore).toBeGreaterThan(minResult.contentScore);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('returns blocking issues for missing docs', () => {
|
|
157
|
+
const context = makeContext({ rawDocs: '' });
|
|
158
|
+
const result = validateWebsiteContext(context, 'test');
|
|
159
|
+
|
|
160
|
+
expect(result.passed).toBe(false);
|
|
161
|
+
expect(result.issues.some((i) => /documentation/i.test(i))).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('warns about missing description', () => {
|
|
165
|
+
const context = makeContext({ description: undefined });
|
|
166
|
+
const result = validateWebsiteContext(context, 'gateco');
|
|
167
|
+
|
|
168
|
+
expect(result.warnings.some((w) => /description/i.test(w))).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('clamps score to 0 when everything is wrong', () => {
|
|
172
|
+
const context: WebsiteContentContext = {
|
|
173
|
+
productName: 'my-app',
|
|
174
|
+
features: [],
|
|
175
|
+
rawDocs: '',
|
|
176
|
+
strategy: undefined,
|
|
177
|
+
};
|
|
178
|
+
const result = validateWebsiteContext(context, 'my-app');
|
|
179
|
+
|
|
180
|
+
expect(result.contentScore).toBe(0);
|
|
181
|
+
expect(result.passed).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for shared-packages module
|
|
3
|
+
* Verifies color scale generation and brand color passthrough
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
generateColorScale,
|
|
9
|
+
generateDesignTokensPackage,
|
|
10
|
+
} from '../../src/generators/shared-packages.js';
|
|
11
|
+
|
|
12
|
+
describe('generateColorScale', () => {
|
|
13
|
+
it('generates 10-stop color scale from valid hex', () => {
|
|
14
|
+
const scale = generateColorScale('#2563EB');
|
|
15
|
+
|
|
16
|
+
expect(Object.keys(scale)).toEqual(['50', '100', '200', '300', '400', '500', '600', '700', '800', '900']);
|
|
17
|
+
// Lightest stop should be light, darkest should be dark
|
|
18
|
+
expect(scale['50']).toMatch(/^#[0-9a-f]{6}$/);
|
|
19
|
+
expect(scale['900']).toMatch(/^#[0-9a-f]{6}$/);
|
|
20
|
+
// The stops should vary (not all the same)
|
|
21
|
+
const uniqueColors = new Set(Object.values(scale));
|
|
22
|
+
expect(uniqueColors.size).toBeGreaterThan(5);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('falls back to default sky-blue on invalid hex', () => {
|
|
26
|
+
const scale = generateColorScale('not-a-color');
|
|
27
|
+
|
|
28
|
+
// Should return the default sky-blue palette
|
|
29
|
+
expect(scale['500']).toBe('#0ea5e9');
|
|
30
|
+
expect(scale['600']).toBe('#0284c7');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('handles hex with # prefix correctly', () => {
|
|
34
|
+
const withHash = generateColorScale('#FF0000');
|
|
35
|
+
const withoutHash = generateColorScale('#FF0000');
|
|
36
|
+
|
|
37
|
+
expect(withHash).toEqual(withoutHash);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('generateDesignTokensPackage', () => {
|
|
42
|
+
it('uses brand color when provided', () => {
|
|
43
|
+
const result = generateDesignTokensPackage('test-project', {
|
|
44
|
+
primaryColor: '#2563EB',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Find the colors.ts file
|
|
48
|
+
const colorsFile = result.files.find((f) => f.path === 'src/colors.ts');
|
|
49
|
+
expect(colorsFile).toBeDefined();
|
|
50
|
+
// Should NOT contain the default sky-blue
|
|
51
|
+
expect(colorsFile!.content).not.toContain('#0ea5e9');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('uses default sky-blue when no brand color provided', () => {
|
|
55
|
+
const result = generateDesignTokensPackage('test-project');
|
|
56
|
+
|
|
57
|
+
const colorsFile = result.files.find((f) => f.path === 'src/colors.ts');
|
|
58
|
+
expect(colorsFile).toBeDefined();
|
|
59
|
+
// Should contain default sky-blue values
|
|
60
|
+
expect(colorsFile!.content).toContain('#0ea5e9');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('generates correct package structure', () => {
|
|
64
|
+
const result = generateDesignTokensPackage('my-project');
|
|
65
|
+
|
|
66
|
+
const paths = result.files.map((f) => f.path);
|
|
67
|
+
expect(paths).toContain('package.json');
|
|
68
|
+
expect(paths).toContain('tsconfig.json');
|
|
69
|
+
expect(paths).toContain('src/index.ts');
|
|
70
|
+
expect(paths).toContain('src/colors.ts');
|
|
71
|
+
expect(paths).toContain('src/typography.ts');
|
|
72
|
+
expect(paths).toContain('src/tailwind-preset.ts');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('package.json contains correct name with scope', () => {
|
|
76
|
+
const result = generateDesignTokensPackage('gateco');
|
|
77
|
+
|
|
78
|
+
const pkgFile = result.files.find((f) => f.path === 'package.json');
|
|
79
|
+
expect(pkgFile).toBeDefined();
|
|
80
|
+
const pkg = JSON.parse(pkgFile!.content);
|
|
81
|
+
expect(pkg.name).toBe('@gateco/design-tokens');
|
|
82
|
+
});
|
|
83
|
+
});
|