viepilot 2.4.0 → 2.12.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 +127 -0
- package/README.md +6 -5
- package/docs/brainstorm/session-2026-04-11.md +194 -0
- package/docs/skills-reference.md +46 -0
- package/docs/user/features/proposal.md +196 -0
- package/lib/google-slides-exporter.cjs +80 -0
- package/lib/proposal-generator.cjs +249 -0
- package/lib/screenshot-artifact.cjs +142 -0
- package/lib/viepilot-config.cjs +32 -1
- package/package.json +8 -1
- package/skills/vp-audit/SKILL.md +11 -0
- package/skills/vp-crystallize/SKILL.md +27 -0
- package/skills/vp-proposal/SKILL.md +175 -0
- package/templates/proposal/docx/project-detail.docx +0 -0
- package/templates/proposal/pptx/general.pptx +0 -0
- package/templates/proposal/pptx/product-pitch.pptx +0 -0
- package/templates/proposal/pptx/project-proposal-creative.pptx +0 -0
- package/templates/proposal/pptx/project-proposal-enterprise.pptx +0 -0
- package/templates/proposal/pptx/project-proposal-modern-tech.pptx +0 -0
- package/templates/proposal/pptx/project-proposal.pptx +0 -0
- package/templates/proposal/pptx/tech-architecture.pptx +0 -0
- package/workflows/crystallize.md +514 -0
- package/workflows/proposal.md +807 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* google-slides-exporter.cjs
|
|
4
|
+
* Uploads a .pptx file to Google Drive and converts it to a Google Slides presentation.
|
|
5
|
+
*
|
|
6
|
+
* Requirements (optional — only needed when --slides flag is used):
|
|
7
|
+
* npm install @googleapis/slides
|
|
8
|
+
* export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json
|
|
9
|
+
*
|
|
10
|
+
* See docs/user/features/proposal.md → "Google Slides Export" for setup guide.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Upload a local .pptx file to Google Drive and convert to Google Slides.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} pptxPath Absolute path to the .pptx file
|
|
20
|
+
* @param {string} title Title for the Google Slides presentation
|
|
21
|
+
* @returns {Promise<string>} Public edit URL of the created presentation
|
|
22
|
+
*/
|
|
23
|
+
async function uploadToSlides(pptxPath, title) {
|
|
24
|
+
// Lazy-load @googleapis/slides — it is an optionalDependency.
|
|
25
|
+
// Provide a clear, actionable error when the package is not installed.
|
|
26
|
+
let googleApis;
|
|
27
|
+
try {
|
|
28
|
+
googleApis = require('googleapis');
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'Google Slides export requires the @googleapis/slides package.\n\n' +
|
|
32
|
+
'Install it:\n' +
|
|
33
|
+
' npm install @googleapis/slides\n\n' +
|
|
34
|
+
'Then set your service account credentials:\n' +
|
|
35
|
+
' export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json\n\n' +
|
|
36
|
+
'See: docs/user/features/proposal.md → "Google Slides Export"'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { google } = googleApis;
|
|
41
|
+
|
|
42
|
+
// Verify credentials env var is set before attempting auth
|
|
43
|
+
if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'GOOGLE_APPLICATION_CREDENTIALS environment variable is not set.\n\n' +
|
|
46
|
+
'Set it to the path of your Google service account JSON key:\n' +
|
|
47
|
+
' export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json\n\n' +
|
|
48
|
+
'See: docs/user/features/proposal.md → "Google Slides Export"'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Authenticate via service account (no browser interaction required)
|
|
53
|
+
const auth = new google.auth.GoogleAuth({
|
|
54
|
+
scopes: [
|
|
55
|
+
'https://www.googleapis.com/auth/drive',
|
|
56
|
+
'https://www.googleapis.com/auth/presentations',
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const authClient = await auth.getClient();
|
|
61
|
+
const drive = google.drive({ version: 'v3', auth: authClient });
|
|
62
|
+
|
|
63
|
+
// Upload .pptx and request conversion to Google Slides format
|
|
64
|
+
const { data: file } = await drive.files.create({
|
|
65
|
+
requestBody: {
|
|
66
|
+
name: title,
|
|
67
|
+
mimeType: 'application/vnd.google-apps.presentation',
|
|
68
|
+
},
|
|
69
|
+
media: {
|
|
70
|
+
mimeType:
|
|
71
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
72
|
+
body: fs.createReadStream(pptxPath),
|
|
73
|
+
},
|
|
74
|
+
fields: 'id',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return `https://docs.google.com/presentation/d/${file.id}/edit`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { uploadToSlides };
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// Proposal type definitions
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
const PROPOSAL_TYPES = {
|
|
10
|
+
'project-proposal': { slides: 10, label: 'Project Proposal' },
|
|
11
|
+
'tech-architecture': { slides: 12, label: 'Technical Architecture' },
|
|
12
|
+
'product-pitch': { slides: 12, label: 'Product Pitch Deck' },
|
|
13
|
+
'general': { slides: 8, label: 'General Proposal' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// Template resolution — 2-tier
|
|
18
|
+
// Tier 1: {projectRoot}/.viepilot/proposal-templates/{type}.{ext} (project override)
|
|
19
|
+
// Tier 2: {packageRoot}/templates/proposal/{ext}/{type}.{ext} (stock)
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
function resolveTemplate(type, ext, projectRoot) {
|
|
22
|
+
const override = path.join(projectRoot, '.viepilot', 'proposal-templates', `${type}.${ext}`);
|
|
23
|
+
if (fs.existsSync(override)) return override;
|
|
24
|
+
// Stock fallback — lives next to this file at ../templates/proposal/{ext}/
|
|
25
|
+
return path.join(__dirname, '..', 'templates', 'proposal', ext, `${type}.${ext}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
// Context detection — auto-load latest brainstorm session
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
function detectBrainstormSession(projectRoot) {
|
|
32
|
+
const dir = path.join(projectRoot, 'docs', 'brainstorm');
|
|
33
|
+
if (!fs.existsSync(dir)) return null;
|
|
34
|
+
const files = fs.readdirSync(dir)
|
|
35
|
+
.filter(f => f.startsWith('session-') && f.endsWith('.md'))
|
|
36
|
+
.sort()
|
|
37
|
+
.reverse();
|
|
38
|
+
return files.length ? path.join(dir, files[0]) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// Proposal type validation
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
function validateType(type) {
|
|
45
|
+
if (!PROPOSAL_TYPES[type]) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Unknown proposal type "${type}". Valid types: ${Object.keys(PROPOSAL_TYPES).join(', ')}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return PROPOSAL_TYPES[type];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// Manifest meta schema (ENH-040)
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* Manifest meta object — collected via Step 2C quality brief.
|
|
58
|
+
*
|
|
59
|
+
* @typedef {Object} ProposalMeta
|
|
60
|
+
* @property {string} cta - Decision/action the proposal should drive
|
|
61
|
+
* @property {string} [budget] - Approximate budget range (optional)
|
|
62
|
+
* @property {string} [timeline] - Key deadline or constraint (optional)
|
|
63
|
+
* @property {string} decisionMaker - Who is the primary audience/decision-maker
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// Output path builder
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
function buildOutputPaths(slug, projectRoot) {
|
|
70
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
71
|
+
const base = path.join(projectRoot, 'docs', 'proposals', `${slug}-${date}`);
|
|
72
|
+
return {
|
|
73
|
+
md: `${base}.md`,
|
|
74
|
+
pptx: `${base}.pptx`,
|
|
75
|
+
docx: `${base}.docx`,
|
|
76
|
+
slides: `${base}-slides.txt`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// Language instruction builder (ENH-039)
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
const LANG_NAMES = {
|
|
85
|
+
vi: 'Vietnamese', ja: 'Japanese', fr: 'French', zh: 'Chinese',
|
|
86
|
+
ko: 'Korean', de: 'German', es: 'Spanish', pt: 'Portuguese',
|
|
87
|
+
it: 'Italian', th: 'Thai', ar: 'Arabic', hi: 'Hindi',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build a language instruction string to prepend to the AI content generation prompt.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} lang - ISO 639-1 code (e.g. 'vi', 'en', 'ja')
|
|
94
|
+
* @param {boolean} [contentOnly=false] - if true, translate content only; keep structural labels English
|
|
95
|
+
* @returns {string} Instruction to inject into AI prompt (empty string for English)
|
|
96
|
+
*/
|
|
97
|
+
function buildLangInstruction(lang, contentOnly = false) {
|
|
98
|
+
if (!lang || lang === 'en') {
|
|
99
|
+
return ''; // English is default — no explicit instruction needed
|
|
100
|
+
}
|
|
101
|
+
const langName = LANG_NAMES[lang] || lang.toUpperCase();
|
|
102
|
+
|
|
103
|
+
if (contentOnly) {
|
|
104
|
+
return (
|
|
105
|
+
`LANGUAGE INSTRUCTION: Generate all slide content (bullet points, body text, ` +
|
|
106
|
+
`speaker notes, and paragraph content) in ${langName}. ` +
|
|
107
|
+
`Keep structural labels, section names, and template placeholders in English.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return (
|
|
111
|
+
`LANGUAGE INSTRUCTION: Generate ALL content — slide headings, bullet points, ` +
|
|
112
|
+
`body text, speaker notes, document section titles, and paragraph content — in ${langName}. ` +
|
|
113
|
+
`Do not mix languages.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @type {Record<string, string[]>} */
|
|
118
|
+
const DIAGRAM_TYPES_BY_PROPOSAL = {
|
|
119
|
+
'project-proposal': ['flowchart', 'gantt'],
|
|
120
|
+
'tech-architecture': ['flowchart', 'sequenceDiagram', 'classDiagram'],
|
|
121
|
+
'product-pitch': ['flowchart', 'sequenceDiagram'],
|
|
122
|
+
'general': ['flowchart'],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns the list of Mermaid diagram types to generate for a given proposal type.
|
|
127
|
+
* @param {string} typeId - Proposal type ID (e.g. 'project-proposal')
|
|
128
|
+
* @returns {string[]} Array of Mermaid diagram type identifiers
|
|
129
|
+
*/
|
|
130
|
+
function getDiagramTypes(typeId) {
|
|
131
|
+
return DIAGRAM_TYPES_BY_PROPOSAL[typeId] || ['flowchart'];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Known architect workspace page filenames (from vp-brainstorm --architect) */
|
|
135
|
+
const ARCHITECT_PAGES = [
|
|
136
|
+
'architecture.html',
|
|
137
|
+
'erd.html',
|
|
138
|
+
'sequence-diagram.html',
|
|
139
|
+
'feature-map.html',
|
|
140
|
+
'user-use-cases.html',
|
|
141
|
+
'deployment.html',
|
|
142
|
+
'apis.html',
|
|
143
|
+
'tech-notes.html',
|
|
144
|
+
'data-flow.html',
|
|
145
|
+
'decisions.html',
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Detect available HTML visual artifacts for screenshot embedding in PPTX slides.
|
|
150
|
+
* Scans the ui-direction session directory for index.html, pages/*.html, and
|
|
151
|
+
* architect workspace pages (architecture.html, erd.html, etc.).
|
|
152
|
+
*
|
|
153
|
+
* @param {string} [sessionDir] - Absolute path to a .viepilot/ui-direction/{session}/ dir.
|
|
154
|
+
* If omitted, auto-detects the latest session by scanning CWD/.viepilot/ui-direction/.
|
|
155
|
+
* @returns {{ uiPages: string[], architectPages: string[], sessionDir: string|null }}
|
|
156
|
+
*/
|
|
157
|
+
function detectVisualArtifacts(sessionDir) {
|
|
158
|
+
const fs = require('fs');
|
|
159
|
+
const path = require('path');
|
|
160
|
+
|
|
161
|
+
// Auto-detect latest session when no dir provided
|
|
162
|
+
let resolvedDir = sessionDir || null;
|
|
163
|
+
if (!resolvedDir) {
|
|
164
|
+
const uiBase = path.join(process.cwd(), '.viepilot', 'ui-direction');
|
|
165
|
+
if (fs.existsSync(uiBase)) {
|
|
166
|
+
const entries = fs.readdirSync(uiBase)
|
|
167
|
+
.filter(e => {
|
|
168
|
+
try { return fs.statSync(path.join(uiBase, e)).isDirectory(); } catch { return false; }
|
|
169
|
+
})
|
|
170
|
+
.sort()
|
|
171
|
+
.reverse(); // ISO date dirs: latest first
|
|
172
|
+
if (entries.length > 0) resolvedDir = path.join(uiBase, entries[0]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = { uiPages: [], architectPages: [], sessionDir: resolvedDir };
|
|
177
|
+
if (!resolvedDir || !fs.existsSync(resolvedDir)) return result;
|
|
178
|
+
|
|
179
|
+
// ui-direction index page
|
|
180
|
+
const indexHtml = path.join(resolvedDir, 'index.html');
|
|
181
|
+
if (fs.existsSync(indexHtml)) result.uiPages.push(indexHtml);
|
|
182
|
+
|
|
183
|
+
// ui-direction sub-pages
|
|
184
|
+
const pagesDir = path.join(resolvedDir, 'pages');
|
|
185
|
+
if (fs.existsSync(pagesDir)) {
|
|
186
|
+
const pageFiles = fs.readdirSync(pagesDir)
|
|
187
|
+
.filter(f => f.endsWith('.html'))
|
|
188
|
+
.sort()
|
|
189
|
+
.map(f => path.join(pagesDir, f));
|
|
190
|
+
result.uiPages.push(...pageFiles);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Architect workspace pages (in same session dir)
|
|
194
|
+
for (const page of ARCHITECT_PAGES) {
|
|
195
|
+
const p = path.join(resolvedDir, page);
|
|
196
|
+
if (fs.existsSync(p)) result.architectPages.push(p);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
// Design configs — ENH-045
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
const DESIGN_CONFIGS = {
|
|
206
|
+
'modern-tech': {
|
|
207
|
+
colorPalette: 'navy-electric',
|
|
208
|
+
layoutStyle: 'modern-tech',
|
|
209
|
+
fontPair: 'Calibri / Calibri Light',
|
|
210
|
+
},
|
|
211
|
+
'enterprise': {
|
|
212
|
+
colorPalette: 'navy-gold',
|
|
213
|
+
layoutStyle: 'enterprise',
|
|
214
|
+
fontPair: 'Georgia / Calibri',
|
|
215
|
+
},
|
|
216
|
+
'creative': {
|
|
217
|
+
colorPalette: 'dark-vibrant',
|
|
218
|
+
layoutStyle: 'creative',
|
|
219
|
+
fontPair: 'Calibri / Calibri',
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Choose design config based on project context signals (ENH-045).
|
|
225
|
+
* @param {Object} ctx - { sector?, audience?, tone?, proposalType? }
|
|
226
|
+
* @returns {{ colorPalette: string, layoutStyle: string, fontPair: string }}
|
|
227
|
+
*/
|
|
228
|
+
function getDesignConfig(ctx = {}) {
|
|
229
|
+
const { sector = '', audience = '', tone = '' } = ctx;
|
|
230
|
+
const lower = `${sector} ${audience} ${tone}`.toLowerCase();
|
|
231
|
+
if (/bank|financ|enterprise|corp|legal|compliance|government/.test(lower))
|
|
232
|
+
return DESIGN_CONFIGS['enterprise'];
|
|
233
|
+
if (/startup|creative|design|game|media|agency|art/.test(lower))
|
|
234
|
+
return DESIGN_CONFIGS['creative'];
|
|
235
|
+
return DESIGN_CONFIGS['modern-tech'];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = {
|
|
239
|
+
PROPOSAL_TYPES,
|
|
240
|
+
resolveTemplate,
|
|
241
|
+
detectBrainstormSession,
|
|
242
|
+
validateType,
|
|
243
|
+
buildOutputPaths,
|
|
244
|
+
buildLangInstruction,
|
|
245
|
+
getDiagramTypes,
|
|
246
|
+
detectVisualArtifacts,
|
|
247
|
+
DESIGN_CONFIGS,
|
|
248
|
+
getDesignConfig,
|
|
249
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* screenshot-artifact.cjs
|
|
4
|
+
* ENH-042 + ENH-043: Optional screenshot/render utility for vp-proposal visual embedding.
|
|
5
|
+
*
|
|
6
|
+
* - puppeteer (optional): screenshots HTML artifact files → PNG
|
|
7
|
+
* - mmdc CLI (optional): renders Mermaid source → PNG
|
|
8
|
+
* Both return null gracefully when the tool is absent — no crash, no hard dep.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const { screenshotArtifact, isPuppeteerAvailable,
|
|
12
|
+
* isMmdcAvailable, renderMermaidToPng, cleanupScreenshot }
|
|
13
|
+
* = require('./lib/screenshot-artifact.cjs');
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const cp = require('child_process');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if puppeteer is available without throwing.
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
function isPuppeteerAvailable() {
|
|
26
|
+
try { require.resolve('puppeteer'); return true; } catch { return false; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Screenshot an HTML file to a temporary PNG using puppeteer (headless Chrome).
|
|
31
|
+
* Returns null silently when puppeteer is not installed.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} htmlPath - Absolute path to the HTML file to screenshot
|
|
34
|
+
* @param {object} [opts]
|
|
35
|
+
* @param {number} [opts.width=1280] - Viewport width in px
|
|
36
|
+
* @param {number} [opts.height=720] - Viewport height in px (matches 16:9 slide ratio)
|
|
37
|
+
* @returns {Promise<string|null>} Absolute path to a temp PNG file, or null if unavailable
|
|
38
|
+
*/
|
|
39
|
+
async function screenshotArtifact(htmlPath, opts = {}) {
|
|
40
|
+
// Graceful: return null if puppeteer not installed
|
|
41
|
+
let puppeteer;
|
|
42
|
+
try { puppeteer = require('puppeteer'); } catch { return null; }
|
|
43
|
+
|
|
44
|
+
if (!htmlPath || !fs.existsSync(htmlPath)) return null;
|
|
45
|
+
|
|
46
|
+
const { width = 1280, height = 720 } = opts;
|
|
47
|
+
const tmpFile = path.join(os.tmpdir(), `vp-artifact-${Date.now()}.png`);
|
|
48
|
+
|
|
49
|
+
const browser = await puppeteer.launch({
|
|
50
|
+
headless: 'new', // puppeteer ≥ 20 compat
|
|
51
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'], // CI-safe
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const page = await browser.newPage();
|
|
55
|
+
await page.setViewport({ width, height });
|
|
56
|
+
await page.goto(`file://${htmlPath}`, { waitUntil: 'networkidle0', timeout: 15000 });
|
|
57
|
+
await page.screenshot({ path: tmpFile, fullPage: false });
|
|
58
|
+
return tmpFile;
|
|
59
|
+
} finally {
|
|
60
|
+
await browser.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clean up a temp screenshot file after embedding.
|
|
66
|
+
* @param {string|null} tmpPath
|
|
67
|
+
*/
|
|
68
|
+
function cleanupScreenshot(tmpPath) {
|
|
69
|
+
if (tmpPath && fs.existsSync(tmpPath)) {
|
|
70
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore cleanup errors */ }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Mermaid rendering (ENH-043) ───────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if @mermaid-js/mermaid-cli (mmdc) is available on PATH.
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
function isMmdcAvailable() {
|
|
81
|
+
try {
|
|
82
|
+
const r = cp.spawnSync('mmdc', ['--version'], { encoding: 'utf8', timeout: 5000 });
|
|
83
|
+
return r.status === 0;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render a Mermaid diagram source string to a PNG file using the mmdc CLI.
|
|
91
|
+
* Returns null silently when mmdc is not available or rendering fails.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} mermaidSource - Valid Mermaid 10+ source code
|
|
94
|
+
* @param {string} outputPath - Absolute path for the output .png file
|
|
95
|
+
* @returns {string|null} outputPath on success, null if mmdc absent or error
|
|
96
|
+
*/
|
|
97
|
+
function renderMermaidToPng(mermaidSource, outputPath) {
|
|
98
|
+
if (!isMmdcAvailable()) return null;
|
|
99
|
+
if (!mermaidSource || !mermaidSource.trim()) return null;
|
|
100
|
+
if (!outputPath) return null;
|
|
101
|
+
|
|
102
|
+
const tmpInput = outputPath.replace(/\.png$/i, '.mmd');
|
|
103
|
+
try {
|
|
104
|
+
fs.writeFileSync(tmpInput, mermaidSource, 'utf8');
|
|
105
|
+
const r = cp.spawnSync(
|
|
106
|
+
'mmdc',
|
|
107
|
+
['-i', tmpInput, '-o', outputPath, '-b', 'white'],
|
|
108
|
+
{ encoding: 'utf8', timeout: 30000 }
|
|
109
|
+
);
|
|
110
|
+
return (r.status === 0 && fs.existsSync(outputPath)) ? outputPath : null;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
} finally {
|
|
114
|
+
try { if (fs.existsSync(tmpInput)) fs.unlinkSync(tmpInput); } catch { /* ignore */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Missing-tool warning (ENH-044) ───────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Emit a standardized warning to stderr when a visual rendering tool is absent
|
|
122
|
+
* but visual artifacts exist and embedding is mandatory.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} tool - Tool name (e.g. 'puppeteer', 'mmdc')
|
|
125
|
+
* @param {string} installCmd - Install hint (e.g. 'npm install puppeteer')
|
|
126
|
+
*/
|
|
127
|
+
function warnMissingTool(tool, installCmd) {
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
`\n[vp-proposal] ⚠ Visual artifacts found but '${tool}' is not installed.\n` +
|
|
130
|
+
` Install to enable screenshots: ${installCmd}\n` +
|
|
131
|
+
` Using placeholder/text fallback instead.\n\n`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
screenshotArtifact,
|
|
137
|
+
isPuppeteerAvailable,
|
|
138
|
+
cleanupScreenshot,
|
|
139
|
+
isMmdcAvailable,
|
|
140
|
+
renderMermaidToPng,
|
|
141
|
+
warnMissingTool,
|
|
142
|
+
};
|
package/lib/viepilot-config.cjs
CHANGED
|
@@ -13,12 +13,16 @@ const fs = require('fs');
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const os = require('os');
|
|
15
15
|
|
|
16
|
-
/** @type {{ language: { communication: string, document: string } }} */
|
|
16
|
+
/** @type {{ language: { communication: string, document: string }, proposal: { recentLangs: string[], defaultLang: string } }} */
|
|
17
17
|
const DEFAULTS = {
|
|
18
18
|
language: {
|
|
19
19
|
communication: 'en',
|
|
20
20
|
document: 'en',
|
|
21
21
|
},
|
|
22
|
+
proposal: {
|
|
23
|
+
recentLangs: [], // MRU list, most recent first, max 5
|
|
24
|
+
defaultLang: 'en', // kept in sync with recentLangs[0]
|
|
25
|
+
},
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -94,10 +98,37 @@ function resetConfig(overrideHomedir) {
|
|
|
94
98
|
fs.writeFileSync(configPath, JSON.stringify(DEFAULTS, null, 2) + '\n', 'utf8');
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Get the suggested default language for proposals.
|
|
103
|
+
* Returns recentLangs[0] if present, else 'en'.
|
|
104
|
+
* @param {string | undefined} overrideHomedir
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
function getProposalLang(overrideHomedir) {
|
|
108
|
+
const cfg = readConfig(overrideHomedir);
|
|
109
|
+
const recent = cfg.proposal && cfg.proposal.recentLangs;
|
|
110
|
+
return (Array.isArray(recent) && recent.length > 0) ? recent[0] : 'en';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Record a used language: prepend to recentLangs, dedup, cap at 5, sync defaultLang.
|
|
115
|
+
* @param {string} lang - ISO 639-1 code
|
|
116
|
+
* @param {string | undefined} overrideHomedir
|
|
117
|
+
*/
|
|
118
|
+
function recordProposalLang(lang, overrideHomedir) {
|
|
119
|
+
const cfg = readConfig(overrideHomedir);
|
|
120
|
+
const existing = (cfg.proposal && Array.isArray(cfg.proposal.recentLangs))
|
|
121
|
+
? cfg.proposal.recentLangs : [];
|
|
122
|
+
const updated = [lang, ...existing.filter(l => l !== lang)].slice(0, 5);
|
|
123
|
+
writeConfig({ proposal: { recentLangs: updated, defaultLang: updated[0] } }, overrideHomedir);
|
|
124
|
+
}
|
|
125
|
+
|
|
97
126
|
module.exports = {
|
|
98
127
|
DEFAULTS,
|
|
99
128
|
getConfigPath,
|
|
100
129
|
readConfig,
|
|
101
130
|
writeConfig,
|
|
102
131
|
resetConfig,
|
|
132
|
+
getProposalLang,
|
|
133
|
+
recordProposalLang,
|
|
103
134
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viepilot",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "**Autonomous Vibe Coding Framework / Bộ khung phát triển tự động có kiểm soát**",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -72,6 +72,13 @@
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"docx": "^9.0.0",
|
|
77
|
+
"pptxgenjs": "^3.12.0"
|
|
78
|
+
},
|
|
79
|
+
"optionalDependencies": {
|
|
80
|
+
"@googleapis/slides": "^1.0.0"
|
|
81
|
+
},
|
|
75
82
|
"devDependencies": {
|
|
76
83
|
"jest": "^30.3.0"
|
|
77
84
|
}
|
package/skills/vp-audit/SKILL.md
CHANGED
|
@@ -34,6 +34,17 @@ Audit ViePilot project state and documentation to detect drift.
|
|
|
34
34
|
Works on **any project** using ViePilot (Java, Node, Python, etc.).
|
|
35
35
|
Auto-detects if running inside the viepilot framework repo to enable framework-specific checks.
|
|
36
36
|
|
|
37
|
+
**Brownfield Import Compatibility (FEAT-018):**
|
|
38
|
+
|
|
39
|
+
When auditing a project bootstrapped via `vp-crystallize --brownfield`:
|
|
40
|
+
|
|
41
|
+
- If `docs/brainstorm/` exists and contains **only** `session-brownfield-import.md` (no `session-*.md` greenfield files):
|
|
42
|
+
- **Valid brownfield import** — do NOT flag as missing brainstorm session.
|
|
43
|
+
- Verify `session-brownfield-import.md` contains a `## Scan Report` section with a YAML block.
|
|
44
|
+
- If YAML block absent → flag LOW severity: "Brownfield stub missing Scan Report content."
|
|
45
|
+
- If `.viepilot/TRACKER.md` contains `## Brownfield Import` section → brownfield metadata confirmed; no further brainstorm check needed.
|
|
46
|
+
- If `docs/brainstorm/` is completely absent → flag MEDIUM severity: "No brainstorm session or brownfield stub found — run `/vp-crystallize` or `/vp-crystallize --brownfield`."
|
|
47
|
+
|
|
37
48
|
**Tier 1 — ViePilot State Consistency (all projects):**
|
|
38
49
|
- `.viepilot/TRACKER.md` current state vs `.viepilot/phases/*/PHASE-STATE.md`
|
|
39
50
|
- `.viepilot/ROADMAP.md` phase status vs PHASE-STATE.md
|
|
@@ -77,6 +77,33 @@ Convert brainstorm sessions into structured artifacts for autonomous AI executio
|
|
|
77
77
|
- `DOCUMENT_LANG` controls content language for all generated files (ROADMAP, TRACKER, ARCHITECTURE, etc.).
|
|
78
78
|
- `COMMUNICATION_LANG` controls prompt/confirmation language for this session.
|
|
79
79
|
- Configure via: `vp-tools config set language.document vi`
|
|
80
|
+
|
|
81
|
+
**Brownfield Mode (`--brownfield`) — FEAT-018:**
|
|
82
|
+
|
|
83
|
+
Use when adopting ViePilot on an **existing project** (no brainstorm session required).
|
|
84
|
+
|
|
85
|
+
Flags:
|
|
86
|
+
- `--brownfield` : Explicit brownfield mode
|
|
87
|
+
- *(auto-detected)* : Triggers when `docs/brainstorm/` is absent/empty AND `.viepilot/` does not exist
|
|
88
|
+
|
|
89
|
+
Scanner runs 12 signal categories across the existing codebase:
|
|
90
|
+
1. **Build manifests** — `package.json`, `pom.xml`, `pyproject.toml`, `Cargo.toml`, `go.mod`, etc. (11 platforms) → infers project_name, version, language, deps
|
|
91
|
+
2. **Framework detection** — 40+ dependency patterns → backend/frontend/ORM/auth/broker/test frameworks
|
|
92
|
+
3. **Architecture layers** — 18 directory patterns → controller/service/repository/frontend/infra/etc.
|
|
93
|
+
4. **Database schema signals** — Flyway/Liquibase/Prisma/Rails migrations + docker-compose services
|
|
94
|
+
5. **API contracts** — OpenAPI, gRPC `.proto`, GraphQL schemas
|
|
95
|
+
6. **Infrastructure** — Dockerfile, docker-compose, k8s, Terraform, Vercel, Fly.io, etc. (16 patterns)
|
|
96
|
+
7. **Environment config** — `.env.example` key names (never reads `.env`)
|
|
97
|
+
8. **Test coverage** — Jest/pytest/JUnit/Cypress config + coverage report dirs
|
|
98
|
+
9. **Code quality tools** — ESLint/Prettier/SonarQube/pre-commit/golangci-lint/etc. (14 patterns)
|
|
99
|
+
10. **Documentation** — README, CHANGELOG, ADRs, docs/ (priority-ordered)
|
|
100
|
+
11. **Git history** — commit convention, version pattern, contributors, repo URL
|
|
101
|
+
12. **Language survey** — file extension glob → language distribution
|
|
102
|
+
|
|
103
|
+
Produces **Scan Report** (YAML) with DETECTED / ASSUMED / MISSING classification.
|
|
104
|
+
MUST-DETECT gaps (project_name, primary_language, ≥1 framework, current_version) block artifact generation until user fills interactively.
|
|
105
|
+
Generates `docs/brainstorm/session-brownfield-import.md` stub for `vp-audit` compatibility.
|
|
106
|
+
Safety: never reads `.env`; skips `node_modules/`, `.git/`, `target/`, `build/`, `dist/`.
|
|
80
107
|
</objective>
|
|
81
108
|
|
|
82
109
|
<execution_context>
|