voidforge-build 23.11.0 → 23.11.2
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/dist/.claude/commands/git.md +36 -3
- package/dist/CHANGELOG.md +46 -0
- package/dist/VERSION.md +3 -1
- package/dist/docs/methods/RELEASE_MANAGER.md +26 -0
- package/dist/scripts/voidforge.d.ts +4 -2
- package/dist/scripts/voidforge.js +106 -17
- package/package.json +1 -1
- package/dist/wizard/lib/anomaly-detection.d.ts +0 -59
- package/dist/wizard/lib/anomaly-detection.js +0 -122
- package/dist/wizard/lib/asset-scanner.d.ts +0 -23
- package/dist/wizard/lib/asset-scanner.js +0 -107
- package/dist/wizard/lib/build-analytics.d.ts +0 -39
- package/dist/wizard/lib/build-analytics.js +0 -91
- package/dist/wizard/lib/codegen/erd-gen.d.ts +0 -16
- package/dist/wizard/lib/codegen/erd-gen.js +0 -98
- package/dist/wizard/lib/codegen/openapi-gen.d.ts +0 -15
- package/dist/wizard/lib/codegen/openapi-gen.js +0 -79
- package/dist/wizard/lib/codegen/prisma-types.d.ts +0 -15
- package/dist/wizard/lib/codegen/prisma-types.js +0 -44
- package/dist/wizard/lib/codegen/seed-gen.d.ts +0 -16
- package/dist/wizard/lib/codegen/seed-gen.js +0 -128
- package/dist/wizard/lib/correlation-engine.d.ts +0 -59
- package/dist/wizard/lib/correlation-engine.js +0 -152
- package/dist/wizard/lib/desktop-notify.d.ts +0 -27
- package/dist/wizard/lib/desktop-notify.js +0 -98
- package/dist/wizard/lib/image-gen.d.ts +0 -56
- package/dist/wizard/lib/image-gen.js +0 -159
- package/dist/wizard/lib/natural-language-deploy.d.ts +0 -30
- package/dist/wizard/lib/natural-language-deploy.js +0 -186
- package/dist/wizard/lib/route-optimizer.d.ts +0 -28
- package/dist/wizard/lib/route-optimizer.js +0 -93
- package/dist/wizard/lib/service-install.d.ts +0 -18
- package/dist/wizard/lib/service-install.js +0 -182
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PRD asset scanner — identifies image/visual requirements from PRD prose.
|
|
3
|
-
* Used by Celebrimbor's /imagine command to find what needs generating.
|
|
4
|
-
* Pure text analysis — no API calls, no side effects.
|
|
5
|
-
*/
|
|
6
|
-
/** Patterns that indicate a visual asset requirement in PRD prose. */
|
|
7
|
-
const ASSET_PATTERNS = [
|
|
8
|
-
{ pattern: /illustrat(?:ion|ed|e)/i, category: 'illustration' },
|
|
9
|
-
{ pattern: /portrait/i, category: 'portrait' },
|
|
10
|
-
{ pattern: /silhouette/i, category: 'portrait' },
|
|
11
|
-
{ pattern: /avatar/i, category: 'portrait' },
|
|
12
|
-
{ pattern: /(?:custom\s+)?\bicon\b/i, category: 'icon' },
|
|
13
|
-
{ pattern: /og[:\s-]image/i, category: 'og-image' },
|
|
14
|
-
{ pattern: /social\s+(?:sharing\s+)?image/i, category: 'og-image' },
|
|
15
|
-
{ pattern: /hero\s+(?:image|banner|art)/i, category: 'hero' },
|
|
16
|
-
{ pattern: /splash\s+(?:page|screen)/i, category: 'hero' },
|
|
17
|
-
{ pattern: /background\s+image/i, category: 'background' },
|
|
18
|
-
{ pattern: /cover\s+image/i, category: 'background' },
|
|
19
|
-
{ pattern: /\blogo\b/i, category: 'logo' },
|
|
20
|
-
{ pattern: /\bfavicon\b/i, category: 'icon' },
|
|
21
|
-
{ pattern: /comic\s+strip/i, category: 'illustration' },
|
|
22
|
-
{ pattern: /comic\s+panel/i, category: 'illustration' },
|
|
23
|
-
{ pattern: /screenshot/i, category: 'screenshot' },
|
|
24
|
-
{ pattern: /mockup/i, category: 'screenshot' },
|
|
25
|
-
];
|
|
26
|
-
/** Default dimensions per asset category. */
|
|
27
|
-
const CATEGORY_DIMENSIONS = {
|
|
28
|
-
'portrait': { width: 1024, height: 1024 },
|
|
29
|
-
'illustration': { width: 1024, height: 1024 },
|
|
30
|
-
'og-image': { width: 1200, height: 630 },
|
|
31
|
-
'hero': { width: 1792, height: 1024 },
|
|
32
|
-
'background': { width: 1792, height: 1024 },
|
|
33
|
-
'logo': { width: 512, height: 512 },
|
|
34
|
-
'icon': { width: 512, height: 512 },
|
|
35
|
-
'screenshot': { width: 1280, height: 720 },
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* Scan a PRD document for visual asset requirements.
|
|
39
|
-
* Returns a list of assets that need generating.
|
|
40
|
-
*/
|
|
41
|
-
export function scanPrdForAssets(prdContent) {
|
|
42
|
-
const assets = [];
|
|
43
|
-
const lines = prdContent.split('\n');
|
|
44
|
-
let currentSection = '';
|
|
45
|
-
for (let i = 0; i < lines.length; i++) {
|
|
46
|
-
const line = lines[i];
|
|
47
|
-
// Track section headers
|
|
48
|
-
const headerMatch = line.match(/^#{1,4}\s+(.+)/);
|
|
49
|
-
if (headerMatch) {
|
|
50
|
-
currentSection = headerMatch[1].trim();
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
// Check each line against asset patterns
|
|
54
|
-
for (const { pattern, category } of ASSET_PATTERNS) {
|
|
55
|
-
if (pattern.test(line)) {
|
|
56
|
-
// Extract surrounding context (current line + next line for description)
|
|
57
|
-
const contextLines = lines.slice(Math.max(0, i - 1), Math.min(lines.length, i + 3));
|
|
58
|
-
const context = contextLines.join(' ').trim();
|
|
59
|
-
const dims = CATEGORY_DIMENSIONS[category] || { width: 1024, height: 1024 };
|
|
60
|
-
assets.push({
|
|
61
|
-
description: line.trim(),
|
|
62
|
-
category,
|
|
63
|
-
context,
|
|
64
|
-
width: dims.width,
|
|
65
|
-
height: dims.height,
|
|
66
|
-
section: currentSection,
|
|
67
|
-
});
|
|
68
|
-
break; // One match per line is enough
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// Deduplicate by description similarity
|
|
73
|
-
const seen = new Set();
|
|
74
|
-
return assets.filter(a => {
|
|
75
|
-
const key = a.description.toLowerCase().slice(0, 60);
|
|
76
|
-
if (seen.has(key))
|
|
77
|
-
return false;
|
|
78
|
-
seen.add(key);
|
|
79
|
-
return true;
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Extract brand/style keywords from the PRD for style prefix generation.
|
|
84
|
-
* Looks for Section 14 (Brand) or any section mentioning "brand", "style", "aesthetic".
|
|
85
|
-
*/
|
|
86
|
-
export function extractBrandStyle(prdContent) {
|
|
87
|
-
const keywords = [];
|
|
88
|
-
const lines = prdContent.split('\n');
|
|
89
|
-
let inBrandSection = false;
|
|
90
|
-
for (const line of lines) {
|
|
91
|
-
const headerMatch = line.match(/^#{1,4}\s+(.+)/);
|
|
92
|
-
if (headerMatch) {
|
|
93
|
-
const title = headerMatch[1].toLowerCase();
|
|
94
|
-
inBrandSection = title.includes('brand') || title.includes('style') || title.includes('aesthetic') || title.includes('design') || title.includes('personality');
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (inBrandSection && line.trim()) {
|
|
98
|
-
// Extract adjectives and style keywords
|
|
99
|
-
const styleWords = line.match(/\b(minimal|bold|playful|professional|elegant|modern|retro|vintage|comic|pulp|neon|dark|light|cinematic|warm|cool|vibrant|muted|halftone|watercolor|photorealistic|illustration|flat|gradient|geometric|organic)\b/gi);
|
|
100
|
-
if (styleWords) {
|
|
101
|
-
keywords.push(...styleWords.map(w => w.toLowerCase()));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// Deduplicate
|
|
106
|
-
return [...new Set(keywords)];
|
|
107
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build analytics — tracks metrics across projects for trend analysis.
|
|
3
|
-
* Stored at ~/.voidforge/analytics.json. No external dependencies.
|
|
4
|
-
*
|
|
5
|
-
* Wong guards the knowledge. The Sanctum grows.
|
|
6
|
-
*/
|
|
7
|
-
export interface PhaseMetric {
|
|
8
|
-
phase: string;
|
|
9
|
-
findingsCount: number;
|
|
10
|
-
fixesApplied: number;
|
|
11
|
-
/** Duration in seconds (optional — only if measurable) */
|
|
12
|
-
durationSeconds?: number;
|
|
13
|
-
}
|
|
14
|
-
export interface BuildRecord {
|
|
15
|
-
projectName: string;
|
|
16
|
-
framework: string;
|
|
17
|
-
database: string;
|
|
18
|
-
deployTarget: string;
|
|
19
|
-
timestamp: string;
|
|
20
|
-
version: string;
|
|
21
|
-
phases: PhaseMetric[];
|
|
22
|
-
totalFindings: number;
|
|
23
|
-
totalFixes: number;
|
|
24
|
-
testCount?: number;
|
|
25
|
-
lessonsExtracted: number;
|
|
26
|
-
}
|
|
27
|
-
export interface AnalyticsStore {
|
|
28
|
-
builds: BuildRecord[];
|
|
29
|
-
}
|
|
30
|
-
/** Record a completed build. */
|
|
31
|
-
export declare function recordBuild(record: BuildRecord): Promise<void>;
|
|
32
|
-
/** Surface trends across past builds. Returns human-readable insights. */
|
|
33
|
-
export declare function surfaceTrends(currentFramework?: string): Promise<string[]>;
|
|
34
|
-
/** Get a summary of all recorded builds. */
|
|
35
|
-
export declare function getBuildHistory(): Promise<{
|
|
36
|
-
count: number;
|
|
37
|
-
frameworks: string[];
|
|
38
|
-
latestBuild: string | null;
|
|
39
|
-
}>;
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build analytics — tracks metrics across projects for trend analysis.
|
|
3
|
-
* Stored at ~/.voidforge/analytics.json. No external dependencies.
|
|
4
|
-
*
|
|
5
|
-
* Wong guards the knowledge. The Sanctum grows.
|
|
6
|
-
*/
|
|
7
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { homedir } from 'node:os';
|
|
10
|
-
const ANALYTICS_DIR = join(homedir(), '.voidforge');
|
|
11
|
-
const ANALYTICS_FILE = join(ANALYTICS_DIR, 'analytics.json');
|
|
12
|
-
async function ensureDir() {
|
|
13
|
-
try {
|
|
14
|
-
await mkdir(ANALYTICS_DIR, { recursive: true });
|
|
15
|
-
}
|
|
16
|
-
catch { /* exists */ }
|
|
17
|
-
}
|
|
18
|
-
async function loadStore() {
|
|
19
|
-
try {
|
|
20
|
-
const raw = await readFile(ANALYTICS_FILE, 'utf-8');
|
|
21
|
-
return JSON.parse(raw);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return { builds: [] };
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
async function saveStore(store) {
|
|
28
|
-
await ensureDir();
|
|
29
|
-
await writeFile(ANALYTICS_FILE, JSON.stringify(store, null, 2), 'utf-8');
|
|
30
|
-
}
|
|
31
|
-
/** Record a completed build. */
|
|
32
|
-
export async function recordBuild(record) {
|
|
33
|
-
const store = await loadStore();
|
|
34
|
-
store.builds.push(record);
|
|
35
|
-
// Keep last 100 builds to prevent unbounded growth
|
|
36
|
-
if (store.builds.length > 100) {
|
|
37
|
-
store.builds = store.builds.slice(-100);
|
|
38
|
-
}
|
|
39
|
-
await saveStore(store);
|
|
40
|
-
}
|
|
41
|
-
/** Surface trends across past builds. Returns human-readable insights. */
|
|
42
|
-
export async function surfaceTrends(currentFramework) {
|
|
43
|
-
const store = await loadStore();
|
|
44
|
-
const builds = store.builds;
|
|
45
|
-
if (builds.length < 2)
|
|
46
|
-
return [];
|
|
47
|
-
const insights = [];
|
|
48
|
-
// Finding hotspots — which phases consistently produce the most findings?
|
|
49
|
-
const phaseFindings = {};
|
|
50
|
-
for (const build of builds) {
|
|
51
|
-
for (const phase of build.phases) {
|
|
52
|
-
if (!phaseFindings[phase.phase])
|
|
53
|
-
phaseFindings[phase.phase] = [];
|
|
54
|
-
phaseFindings[phase.phase].push(phase.findingsCount);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
for (const [phase, counts] of Object.entries(phaseFindings)) {
|
|
58
|
-
const avg = counts.reduce((a, b) => a + b, 0) / counts.length;
|
|
59
|
-
if (avg > 5 && counts.length >= 2) {
|
|
60
|
-
insights.push(`Phase "${phase}" averages ${avg.toFixed(1)} findings across ${counts.length} builds — consider proactive checks in earlier phases.`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Framework-specific patterns
|
|
64
|
-
if (currentFramework) {
|
|
65
|
-
const frameworkBuilds = builds.filter(b => b.framework === currentFramework);
|
|
66
|
-
if (frameworkBuilds.length >= 2) {
|
|
67
|
-
const avgFindings = frameworkBuilds.reduce((a, b) => a + b.totalFindings, 0) / frameworkBuilds.length;
|
|
68
|
-
insights.push(`Your ${currentFramework} projects average ${avgFindings.toFixed(0)} findings per build (${frameworkBuilds.length} builds).`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Fix-to-finding ratio trend
|
|
72
|
-
const ratios = builds.map(b => b.totalFindings > 0 ? b.totalFixes / b.totalFindings : 1);
|
|
73
|
-
const recentRatios = ratios.slice(-5);
|
|
74
|
-
const avgRatio = recentRatios.reduce((a, b) => a + b, 0) / recentRatios.length;
|
|
75
|
-
if (avgRatio < 0.8) {
|
|
76
|
-
insights.push(`Fix-to-finding ratio is ${(avgRatio * 100).toFixed(0)}% — some findings are being deferred. Consider addressing all findings in each build.`);
|
|
77
|
-
}
|
|
78
|
-
// Lessons trend
|
|
79
|
-
const totalLessons = builds.reduce((a, b) => a + b.lessonsExtracted, 0);
|
|
80
|
-
if (totalLessons > 0) {
|
|
81
|
-
insights.push(`${totalLessons} lessons extracted across ${builds.length} builds. The forge is learning.`);
|
|
82
|
-
}
|
|
83
|
-
return insights;
|
|
84
|
-
}
|
|
85
|
-
/** Get a summary of all recorded builds. */
|
|
86
|
-
export async function getBuildHistory() {
|
|
87
|
-
const store = await loadStore();
|
|
88
|
-
const frameworks = [...new Set(store.builds.map(b => b.framework))];
|
|
89
|
-
const latest = store.builds.length > 0 ? store.builds[store.builds.length - 1].timestamp : null;
|
|
90
|
-
return { count: store.builds.length, frameworks, latestBuild: latest };
|
|
91
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Database ERD generation from Prisma schema (ADR-025).
|
|
3
|
-
* Parses prisma/schema.prisma and produces a Mermaid entity-relationship diagram.
|
|
4
|
-
* Conditional — only runs if prisma/schema.prisma exists.
|
|
5
|
-
*/
|
|
6
|
-
import type { ProvisionEmitter } from '../provisioners/types.js';
|
|
7
|
-
export interface ERDResult {
|
|
8
|
-
success: boolean;
|
|
9
|
-
file: string;
|
|
10
|
-
modelCount: number;
|
|
11
|
-
error?: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Generate a Mermaid ERD from the Prisma schema.
|
|
15
|
-
*/
|
|
16
|
-
export declare function generateERD(projectDir: string, emit: ProvisionEmitter): Promise<ERDResult>;
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Database ERD generation from Prisma schema (ADR-025).
|
|
3
|
-
* Parses prisma/schema.prisma and produces a Mermaid entity-relationship diagram.
|
|
4
|
-
* Conditional — only runs if prisma/schema.prisma exists.
|
|
5
|
-
*/
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
9
|
-
/**
|
|
10
|
-
* Minimal Prisma schema parser — extracts model names and fields.
|
|
11
|
-
* Not a full parser — handles the common cases for ERD generation.
|
|
12
|
-
*/
|
|
13
|
-
function parsePrismaSchema(content) {
|
|
14
|
-
const models = [];
|
|
15
|
-
const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
16
|
-
let match;
|
|
17
|
-
while ((match = modelRegex.exec(content)) !== null) {
|
|
18
|
-
const name = match[1];
|
|
19
|
-
const body = match[2];
|
|
20
|
-
const fields = [];
|
|
21
|
-
for (const line of body.split('\n')) {
|
|
22
|
-
const trimmed = line.trim();
|
|
23
|
-
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@'))
|
|
24
|
-
continue;
|
|
25
|
-
const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\[\])?\s*(\?)?\s*/);
|
|
26
|
-
if (fieldMatch) {
|
|
27
|
-
const fieldName = fieldMatch[1];
|
|
28
|
-
const fieldType = fieldMatch[2];
|
|
29
|
-
const isArray = !!fieldMatch[3];
|
|
30
|
-
const isOptional = !!fieldMatch[4];
|
|
31
|
-
// Skip Prisma directives like @id, @default, etc. — those are on the same line
|
|
32
|
-
const builtinTypes = ['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Json', 'BigInt', 'Decimal', 'Bytes'];
|
|
33
|
-
const isRelation = !builtinTypes.includes(fieldType);
|
|
34
|
-
fields.push({ name: fieldName, type: fieldType, isRelation, isOptional, isArray });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
models.push({ name, fields });
|
|
38
|
-
}
|
|
39
|
-
return models;
|
|
40
|
-
}
|
|
41
|
-
function generateMermaidERD(models) {
|
|
42
|
-
const lines = [
|
|
43
|
-
'```mermaid',
|
|
44
|
-
'erDiagram',
|
|
45
|
-
];
|
|
46
|
-
// Generate entity definitions
|
|
47
|
-
for (const model of models) {
|
|
48
|
-
lines.push(` ${model.name} {`);
|
|
49
|
-
for (const field of model.fields) {
|
|
50
|
-
if (!field.isRelation) {
|
|
51
|
-
const optional = field.isOptional ? '?' : '';
|
|
52
|
-
lines.push(` ${field.type}${optional} ${field.name}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
lines.push(' }');
|
|
56
|
-
}
|
|
57
|
-
// Generate relationships
|
|
58
|
-
for (const model of models) {
|
|
59
|
-
for (const field of model.fields) {
|
|
60
|
-
if (field.isRelation) {
|
|
61
|
-
const cardinality = field.isArray ? '}o--||' : '}o--o|';
|
|
62
|
-
lines.push(` ${model.name} ${cardinality} ${field.type} : "${field.name}"`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
lines.push('```');
|
|
67
|
-
return lines.join('\n');
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Generate a Mermaid ERD from the Prisma schema.
|
|
71
|
-
*/
|
|
72
|
-
export async function generateERD(projectDir, emit) {
|
|
73
|
-
const schemaPath = join(projectDir, 'prisma', 'schema.prisma');
|
|
74
|
-
if (!existsSync(schemaPath)) {
|
|
75
|
-
emit({ step: 'erd', status: 'skipped', message: 'No prisma/schema.prisma found — ERD generation skipped' });
|
|
76
|
-
return { success: true, file: '', modelCount: 0 };
|
|
77
|
-
}
|
|
78
|
-
emit({ step: 'erd', status: 'started', message: 'Generating database ERD from Prisma schema' });
|
|
79
|
-
try {
|
|
80
|
-
const schema = await readFile(schemaPath, 'utf-8');
|
|
81
|
-
const models = parsePrismaSchema(schema);
|
|
82
|
-
if (models.length === 0) {
|
|
83
|
-
emit({ step: 'erd', status: 'skipped', message: 'No models found in Prisma schema' });
|
|
84
|
-
return { success: true, file: '', modelCount: 0 };
|
|
85
|
-
}
|
|
86
|
-
const mermaid = generateMermaidERD(models);
|
|
87
|
-
const content = `# Database Schema\n\nAuto-generated from \`prisma/schema.prisma\` by VoidForge (ADR-025).\n\n${mermaid}\n`;
|
|
88
|
-
const docsDir = join(projectDir, 'docs');
|
|
89
|
-
await mkdir(docsDir, { recursive: true });
|
|
90
|
-
await writeFile(join(docsDir, 'schema.md'), content, 'utf-8');
|
|
91
|
-
emit({ step: 'erd', status: 'done', message: `Generated docs/schema.md — ${models.length} models mapped` });
|
|
92
|
-
return { success: true, file: 'docs/schema.md', modelCount: models.length };
|
|
93
|
-
}
|
|
94
|
-
catch (err) {
|
|
95
|
-
emit({ step: 'erd', status: 'error', message: 'Failed to generate ERD', detail: err.message });
|
|
96
|
-
return { success: false, file: '', modelCount: 0, error: err.message };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAPI/Swagger spec generation (ADR-025).
|
|
3
|
-
* Generates a starter OpenAPI spec from framework conventions.
|
|
4
|
-
* Framework-aware: Express, Next.js API routes.
|
|
5
|
-
*/
|
|
6
|
-
import type { ProvisionEmitter } from '../provisioners/types.js';
|
|
7
|
-
export interface OpenAPIResult {
|
|
8
|
-
success: boolean;
|
|
9
|
-
file: string;
|
|
10
|
-
error?: string;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Generate an OpenAPI spec file for the project.
|
|
14
|
-
*/
|
|
15
|
-
export declare function generateOpenAPIDoc(projectDir: string, projectName: string, framework: string, emit: ProvisionEmitter): Promise<OpenAPIResult>;
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAPI/Swagger spec generation (ADR-025).
|
|
3
|
-
* Generates a starter OpenAPI spec from framework conventions.
|
|
4
|
-
* Framework-aware: Express, Next.js API routes.
|
|
5
|
-
*/
|
|
6
|
-
import { writeFile, mkdir } from 'node:fs/promises';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
function generateOpenAPISpec(projectName, framework) {
|
|
9
|
-
const isNextjs = framework === 'next.js';
|
|
10
|
-
// Strip YAML-unsafe characters — only allow alphanumeric, spaces, hyphens, underscores
|
|
11
|
-
const safeName = projectName.replace(/[^a-zA-Z0-9 _-]/g, '').slice(0, 100) || 'My Project';
|
|
12
|
-
return `# OpenAPI Specification
|
|
13
|
-
# Generated by VoidForge (ADR-025)
|
|
14
|
-
# Edit this file to document your API endpoints
|
|
15
|
-
# Serve with: npx swagger-ui-express (Express) or next-swagger-doc (Next.js)
|
|
16
|
-
|
|
17
|
-
openapi: "3.1.0"
|
|
18
|
-
info:
|
|
19
|
-
title: "${safeName} API"
|
|
20
|
-
version: "1.0.0"
|
|
21
|
-
description: "API documentation for ${safeName}"
|
|
22
|
-
|
|
23
|
-
servers:
|
|
24
|
-
- url: http://localhost:${isNextjs ? '3000' : '3001'}
|
|
25
|
-
description: Development server
|
|
26
|
-
|
|
27
|
-
paths:
|
|
28
|
-
/api/health:
|
|
29
|
-
get:
|
|
30
|
-
summary: Health check
|
|
31
|
-
responses:
|
|
32
|
-
"200":
|
|
33
|
-
description: Service is healthy
|
|
34
|
-
content:
|
|
35
|
-
application/json:
|
|
36
|
-
schema:
|
|
37
|
-
type: object
|
|
38
|
-
properties:
|
|
39
|
-
status:
|
|
40
|
-
type: string
|
|
41
|
-
example: "ok"
|
|
42
|
-
timestamp:
|
|
43
|
-
type: string
|
|
44
|
-
format: date-time
|
|
45
|
-
|
|
46
|
-
# Add your API endpoints below
|
|
47
|
-
# Example:
|
|
48
|
-
# /api/users:
|
|
49
|
-
# get:
|
|
50
|
-
# summary: List users
|
|
51
|
-
# parameters:
|
|
52
|
-
# - in: query
|
|
53
|
-
# name: page
|
|
54
|
-
# schema:
|
|
55
|
-
# type: integer
|
|
56
|
-
# default: 1
|
|
57
|
-
# responses:
|
|
58
|
-
# "200":
|
|
59
|
-
# description: List of users
|
|
60
|
-
`;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Generate an OpenAPI spec file for the project.
|
|
64
|
-
*/
|
|
65
|
-
export async function generateOpenAPIDoc(projectDir, projectName, framework, emit) {
|
|
66
|
-
emit({ step: 'openapi', status: 'started', message: 'Generating OpenAPI spec' });
|
|
67
|
-
try {
|
|
68
|
-
const docsDir = join(projectDir, 'docs');
|
|
69
|
-
await mkdir(docsDir, { recursive: true });
|
|
70
|
-
const spec = generateOpenAPISpec(projectName, framework);
|
|
71
|
-
await writeFile(join(docsDir, 'api.yaml'), spec, 'utf-8');
|
|
72
|
-
emit({ step: 'openapi', status: 'done', message: 'Generated docs/api.yaml — edit to document your API endpoints' });
|
|
73
|
-
return { success: true, file: 'docs/api.yaml' };
|
|
74
|
-
}
|
|
75
|
-
catch (err) {
|
|
76
|
-
emit({ step: 'openapi', status: 'error', message: 'Failed to generate OpenAPI spec', detail: err.message });
|
|
77
|
-
return { success: false, file: '', error: err.message };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type generation from Prisma schema (ADR-025).
|
|
3
|
-
* After Prisma schema changes, runs prisma generate and creates a barrel export.
|
|
4
|
-
* Conditional — only runs if prisma/schema.prisma exists.
|
|
5
|
-
*/
|
|
6
|
-
import type { ProvisionEmitter } from '../provisioners/types.js';
|
|
7
|
-
export interface PrismaTypesResult {
|
|
8
|
-
success: boolean;
|
|
9
|
-
files: string[];
|
|
10
|
-
error?: string;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Run prisma generate and create a barrel export for types.
|
|
14
|
-
*/
|
|
15
|
-
export declare function generatePrismaTypes(projectDir: string, emit: ProvisionEmitter): Promise<PrismaTypesResult>;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type generation from Prisma schema (ADR-025).
|
|
3
|
-
* After Prisma schema changes, runs prisma generate and creates a barrel export.
|
|
4
|
-
* Conditional — only runs if prisma/schema.prisma exists.
|
|
5
|
-
*/
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { writeFile, mkdir } from 'node:fs/promises';
|
|
9
|
-
import { execCommand } from '../exec.js';
|
|
10
|
-
/**
|
|
11
|
-
* Run prisma generate and create a barrel export for types.
|
|
12
|
-
*/
|
|
13
|
-
export async function generatePrismaTypes(projectDir, emit) {
|
|
14
|
-
const schemaPath = join(projectDir, 'prisma', 'schema.prisma');
|
|
15
|
-
if (!existsSync(schemaPath)) {
|
|
16
|
-
emit({ step: 'prisma-types', status: 'skipped', message: 'No prisma/schema.prisma found — type generation skipped' });
|
|
17
|
-
return { success: true, files: [] };
|
|
18
|
-
}
|
|
19
|
-
emit({ step: 'prisma-types', status: 'started', message: 'Generating Prisma types' });
|
|
20
|
-
try {
|
|
21
|
-
// Run prisma generate
|
|
22
|
-
await execCommand('npx', ['prisma', 'generate'], {
|
|
23
|
-
cwd: projectDir,
|
|
24
|
-
timeout: 60_000,
|
|
25
|
-
});
|
|
26
|
-
// Create barrel export
|
|
27
|
-
const typesDir = join(projectDir, 'types');
|
|
28
|
-
await mkdir(typesDir, { recursive: true });
|
|
29
|
-
const barrelContent = `// Auto-generated barrel export for Prisma types
|
|
30
|
-
// Re-run: npx prisma generate
|
|
31
|
-
// Generated by VoidForge (ADR-025)
|
|
32
|
-
|
|
33
|
-
export * from '@prisma/client';
|
|
34
|
-
export type { Prisma } from '@prisma/client';
|
|
35
|
-
`;
|
|
36
|
-
await writeFile(join(typesDir, 'index.ts'), barrelContent, 'utf-8');
|
|
37
|
-
emit({ step: 'prisma-types', status: 'done', message: 'Prisma types generated — import from types/index.ts' });
|
|
38
|
-
return { success: true, files: ['types/index.ts'] };
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
emit({ step: 'prisma-types', status: 'error', message: 'Failed to generate Prisma types', detail: err.message });
|
|
42
|
-
return { success: false, files: [], error: err.message };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Database seeding script generation (ADR-025).
|
|
3
|
-
* Generates seed.ts with factory functions for all Prisma models.
|
|
4
|
-
* Conditional — only runs if prisma/schema.prisma exists.
|
|
5
|
-
*/
|
|
6
|
-
import type { ProvisionEmitter } from '../provisioners/types.js';
|
|
7
|
-
export interface SeedResult {
|
|
8
|
-
success: boolean;
|
|
9
|
-
file: string;
|
|
10
|
-
modelCount: number;
|
|
11
|
-
error?: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Generate a database seed script from the Prisma schema.
|
|
15
|
-
*/
|
|
16
|
-
export declare function generateSeedScript(projectDir: string, emit: ProvisionEmitter): Promise<SeedResult>;
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Database seeding script generation (ADR-025).
|
|
3
|
-
* Generates seed.ts with factory functions for all Prisma models.
|
|
4
|
-
* Conditional — only runs if prisma/schema.prisma exists.
|
|
5
|
-
*/
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
9
|
-
/** Map Prisma types to TypeScript factory values. */
|
|
10
|
-
const FACTORY_VALUES = {
|
|
11
|
-
'String': "'Test value'",
|
|
12
|
-
'Int': '1',
|
|
13
|
-
'Float': '1.0',
|
|
14
|
-
'Boolean': 'true',
|
|
15
|
-
'DateTime': 'new Date()',
|
|
16
|
-
'Json': '{}',
|
|
17
|
-
'BigInt': 'BigInt(1)',
|
|
18
|
-
'Decimal': '1.0',
|
|
19
|
-
};
|
|
20
|
-
function parseSeedModels(content) {
|
|
21
|
-
const models = [];
|
|
22
|
-
const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
23
|
-
let match;
|
|
24
|
-
while ((match = modelRegex.exec(content)) !== null) {
|
|
25
|
-
const name = match[1];
|
|
26
|
-
const body = match[2];
|
|
27
|
-
const fields = [];
|
|
28
|
-
const builtinTypes = ['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Json', 'BigInt', 'Decimal', 'Bytes'];
|
|
29
|
-
for (const line of body.split('\n')) {
|
|
30
|
-
const trimmed = line.trim();
|
|
31
|
-
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@'))
|
|
32
|
-
continue;
|
|
33
|
-
const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\[\])?\s*(\?)?\s*(.*)/);
|
|
34
|
-
if (fieldMatch) {
|
|
35
|
-
const fieldName = fieldMatch[1];
|
|
36
|
-
const fieldType = fieldMatch[2];
|
|
37
|
-
const isOptional = !!fieldMatch[4];
|
|
38
|
-
const rest = fieldMatch[5] || '';
|
|
39
|
-
const hasDefault = rest.includes('@default');
|
|
40
|
-
const isId = rest.includes('@id');
|
|
41
|
-
const isRelation = !builtinTypes.includes(fieldType);
|
|
42
|
-
if (!isRelation && !fieldMatch[3]) { // Skip arrays (relations)
|
|
43
|
-
fields.push({ name: fieldName, type: fieldType, isOptional, hasDefault, isId, isRelation });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
models.push({ name, fields });
|
|
48
|
-
}
|
|
49
|
-
return models;
|
|
50
|
-
}
|
|
51
|
-
function buildSeedContent(models) {
|
|
52
|
-
const factories = [];
|
|
53
|
-
const seedCalls = [];
|
|
54
|
-
for (const model of models) {
|
|
55
|
-
const lowerName = model.name.charAt(0).toLowerCase() + model.name.slice(1);
|
|
56
|
-
const factoryFields = model.fields
|
|
57
|
-
.filter(f => !f.isId && !f.hasDefault && !f.isOptional)
|
|
58
|
-
.map(f => ` ${f.name}: ${FACTORY_VALUES[f.type] || "'TODO'"},`)
|
|
59
|
-
.join('\n');
|
|
60
|
-
factories.push(`function create${model.name}(overrides: Partial<Prisma.${model.name}CreateInput> = {}): Prisma.${model.name}CreateInput {
|
|
61
|
-
return {
|
|
62
|
-
${factoryFields}
|
|
63
|
-
...overrides,
|
|
64
|
-
};
|
|
65
|
-
}`);
|
|
66
|
-
seedCalls.push(` // ${model.name}
|
|
67
|
-
const ${lowerName} = await prisma.${lowerName}.create({ data: create${model.name}() });
|
|
68
|
-
console.log('Created ${model.name}:', ${lowerName}.id ?? '(no id)');`);
|
|
69
|
-
}
|
|
70
|
-
return `// Database seed script — generated by VoidForge (ADR-025)
|
|
71
|
-
// Run: npx tsx prisma/seed.ts
|
|
72
|
-
// Or: npm run seed (add "seed": "npx tsx prisma/seed.ts" to package.json scripts)
|
|
73
|
-
|
|
74
|
-
import { PrismaClient, Prisma } from '@prisma/client';
|
|
75
|
-
|
|
76
|
-
const prisma = new PrismaClient();
|
|
77
|
-
|
|
78
|
-
// ── Factory Functions ────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
${factories.join('\n\n')}
|
|
81
|
-
|
|
82
|
-
// ── Seed ─────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
async function seed() {
|
|
85
|
-
console.log('Seeding database...');
|
|
86
|
-
|
|
87
|
-
${seedCalls.join('\n\n')}
|
|
88
|
-
|
|
89
|
-
console.log('Seeding complete.');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
seed()
|
|
93
|
-
.catch((e) => {
|
|
94
|
-
console.error('Seed failed:', e);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
})
|
|
97
|
-
.finally(() => prisma.$disconnect());
|
|
98
|
-
`;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Generate a database seed script from the Prisma schema.
|
|
102
|
-
*/
|
|
103
|
-
export async function generateSeedScript(projectDir, emit) {
|
|
104
|
-
const schemaPath = join(projectDir, 'prisma', 'schema.prisma');
|
|
105
|
-
if (!existsSync(schemaPath)) {
|
|
106
|
-
emit({ step: 'seed', status: 'skipped', message: 'No prisma/schema.prisma found — seed generation skipped' });
|
|
107
|
-
return { success: true, file: '', modelCount: 0 };
|
|
108
|
-
}
|
|
109
|
-
emit({ step: 'seed', status: 'started', message: 'Generating database seed script' });
|
|
110
|
-
try {
|
|
111
|
-
const schema = await readFile(schemaPath, 'utf-8');
|
|
112
|
-
const models = parseSeedModels(schema);
|
|
113
|
-
if (models.length === 0) {
|
|
114
|
-
emit({ step: 'seed', status: 'skipped', message: 'No models found in Prisma schema' });
|
|
115
|
-
return { success: true, file: '', modelCount: 0 };
|
|
116
|
-
}
|
|
117
|
-
const prismaDir = join(projectDir, 'prisma');
|
|
118
|
-
await mkdir(prismaDir, { recursive: true });
|
|
119
|
-
const script = buildSeedContent(models);
|
|
120
|
-
await writeFile(join(prismaDir, 'seed.ts'), script, 'utf-8');
|
|
121
|
-
emit({ step: 'seed', status: 'done', message: `Generated prisma/seed.ts — ${models.length} model factories. Run: npx tsx prisma/seed.ts` });
|
|
122
|
-
return { success: true, file: 'prisma/seed.ts', modelCount: models.length };
|
|
123
|
-
}
|
|
124
|
-
catch (err) {
|
|
125
|
-
emit({ step: 'seed', status: 'error', message: 'Failed to generate seed script', detail: err.message });
|
|
126
|
-
return { success: false, file: '', modelCount: 0, error: err.message };
|
|
127
|
-
}
|
|
128
|
-
}
|