vibepro 0.1.0-alpha.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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.ja.md +448 -0
- package/README.md +520 -0
- package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
- package/bin/vibepro.js +9 -0
- package/docs/assets/vibepro-header.png +0 -0
- package/package.json +51 -0
- package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
- package/skills/vibepro-human-review/SKILL.md +73 -0
- package/skills/vibepro-story-refactor/SKILL.md +89 -0
- package/skills/vibepro-workflow/SKILL.md +139 -0
- package/src/agent-harness-map.js +230 -0
- package/src/agent-harness-scanner.js +337 -0
- package/src/agent-review.js +2180 -0
- package/src/api-boundary-scanner.js +452 -0
- package/src/architecture-profiler.js +423 -0
- package/src/authorization-scoring.js +149 -0
- package/src/brainbase-importer.js +534 -0
- package/src/change-risk-classifier.js +195 -0
- package/src/check-packs.js +605 -0
- package/src/checkpoint-manager.js +233 -0
- package/src/cli.js +2213 -0
- package/src/code-quality-scanner.js +310 -0
- package/src/codex-manager.js +143 -0
- package/src/component-style-scanner.js +336 -0
- package/src/coverage-report.js +99 -0
- package/src/database-access-scanner.js +163 -0
- package/src/decision-records.js +315 -0
- package/src/design-modernize.js +1435 -0
- package/src/design-system.js +1732 -0
- package/src/diagnostic-engine.js +1945 -0
- package/src/diagram-requirement-resolver.js +194 -0
- package/src/doctor.js +677 -0
- package/src/environment-graph.js +424 -0
- package/src/execution-state.js +849 -0
- package/src/explore-evidence.js +425 -0
- package/src/flow-design-scanner.js +896 -0
- package/src/flow-verifier.js +887 -0
- package/src/gesture-interaction-scanner.js +330 -0
- package/src/graph-context.js +263 -0
- package/src/graphify-adapter.js +189 -0
- package/src/html-report.js +1035 -0
- package/src/journey-map.js +1299 -0
- package/src/language.js +48 -0
- package/src/lazy-pattern-detector.js +182 -0
- package/src/local-dev-scanner.js +135 -0
- package/src/managed-worktree-gate.js +187 -0
- package/src/managed-worktree.js +766 -0
- package/src/merge-manager.js +501 -0
- package/src/network-contract-scanner.js +442 -0
- package/src/nocodb-story-sync.js +386 -0
- package/src/oss-readiness-scanner.js +417 -0
- package/src/performance-evidence.js +756 -0
- package/src/performance-measurer.js +591 -0
- package/src/pr-manager.js +8220 -0
- package/src/presets.js +682 -0
- package/src/public-discovery-scanner.js +519 -0
- package/src/refactoring-delta-reporter.js +367 -0
- package/src/refactoring-opportunity-generator.js +797 -0
- package/src/regression-risk-scanner.js +146 -0
- package/src/repo-status.js +266 -0
- package/src/report-fingerprint.js +188 -0
- package/src/report-pr-body-prompt-template.md +108 -0
- package/src/report-pr-body-schema.json +95 -0
- package/src/report-store.js +135 -0
- package/src/report-validator.js +192 -0
- package/src/requirement-consistency.js +1066 -0
- package/src/runtime-info.js +134 -0
- package/src/self-dogfood-scanner.js +476 -0
- package/src/session-learning.js +164 -0
- package/src/skills-manager.js +157 -0
- package/src/spec-drift.js +378 -0
- package/src/spec-fingerprint.js +445 -0
- package/src/spec-prompt-template.md +155 -0
- package/src/spec-schema.json +219 -0
- package/src/spec-store.js +258 -0
- package/src/spec-validator.js +459 -0
- package/src/static-site-scanner.js +316 -0
- package/src/story-candidate-generator.js +85 -0
- package/src/story-catalog-generator.js +2813 -0
- package/src/story-html.js +156 -0
- package/src/story-manager.js +2144 -0
- package/src/story-task-generator.js +522 -0
- package/src/task-manager.js +1029 -0
- package/src/terminal-link-scanner.js +238 -0
- package/src/usage-report.js +417 -0
- package/src/verification-evidence.js +284 -0
- package/src/workspace.js +126 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const IGNORED_DIRS = new Set([
|
|
5
|
+
'.git',
|
|
6
|
+
'.next',
|
|
7
|
+
'.turbo',
|
|
8
|
+
'.vibepro',
|
|
9
|
+
'coverage',
|
|
10
|
+
'node_modules',
|
|
11
|
+
'graphify-out'
|
|
12
|
+
]);
|
|
13
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.ts', '.tsx']);
|
|
14
|
+
const GATE_EFFECTS = ['block', 'review', 'info'];
|
|
15
|
+
|
|
16
|
+
export async function scanCodeQuality(repoRoot) {
|
|
17
|
+
const root = path.resolve(repoRoot);
|
|
18
|
+
const files = await collectFiles(root);
|
|
19
|
+
const result = {
|
|
20
|
+
scanned_files: files.length,
|
|
21
|
+
authorization_order_risks: [],
|
|
22
|
+
duplicate_query_shapes: [],
|
|
23
|
+
responsibility_hotspots: []
|
|
24
|
+
};
|
|
25
|
+
const queryShapeOccurrences = new Map();
|
|
26
|
+
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const content = await readFile(file.absolutePath, 'utf8');
|
|
29
|
+
const code = stripComments(content);
|
|
30
|
+
collectAuthorizationOrderRisks(result.authorization_order_risks, file.relativePath, code);
|
|
31
|
+
collectResponsibilityHotspots(result.responsibility_hotspots, file.relativePath, code);
|
|
32
|
+
collectQueryShapes(queryShapeOccurrences, file.relativePath, code);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
result.duplicate_query_shapes = buildDuplicateQueryShapes(queryShapeOccurrences);
|
|
36
|
+
result.risk_summary = {
|
|
37
|
+
authorization_order_risks: summarizeGateEffects(result.authorization_order_risks),
|
|
38
|
+
duplicate_query_shapes: summarizeGateEffects(result.duplicate_query_shapes),
|
|
39
|
+
responsibility_hotspots: summarizeGateEffects(result.responsibility_hotspots)
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function collectFiles(root, current = root) {
|
|
46
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
47
|
+
const files = [];
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue;
|
|
51
|
+
const absolutePath = path.join(current, entry.name);
|
|
52
|
+
const relativePath = path.relative(root, absolutePath).split(path.sep).join('/');
|
|
53
|
+
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
files.push(...await collectFiles(root, absolutePath));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!entry.isFile()) continue;
|
|
60
|
+
if (!SOURCE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) continue;
|
|
61
|
+
const fileStat = await stat(absolutePath);
|
|
62
|
+
if (fileStat.size > 1024 * 1024) continue;
|
|
63
|
+
files.push({ absolutePath, relativePath });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function collectAuthorizationOrderRisks(hits, file, code) {
|
|
70
|
+
if (!isRuntimeCode(file)) return;
|
|
71
|
+
const authorizationLine = findFirstLine(code, [
|
|
72
|
+
/\b(status\s*:\s*403|Access denied|Forbidden|forbidden|not authorized|not_authorized)\b/gi,
|
|
73
|
+
/\bNextResponse\.json\s*\([^)]*\{\s*status\s*:\s*403\s*\}/g,
|
|
74
|
+
/\bResponse\.json\s*\([^)]*\{\s*status\s*:\s*403\s*\}/g
|
|
75
|
+
]);
|
|
76
|
+
if (!authorizationLine) return;
|
|
77
|
+
|
|
78
|
+
const bulkQueryPattern = /\b(?:prisma|db|client|prismaAny)\.[A-Za-z_$][\w$]*\.(findMany|count|aggregate|groupBy)\s*\(/g;
|
|
79
|
+
let match = bulkQueryPattern.exec(code);
|
|
80
|
+
while (match) {
|
|
81
|
+
const queryLine = lineNumberAt(code, match.index);
|
|
82
|
+
if (queryLine < authorizationLine) {
|
|
83
|
+
const call = extractCall(code, match.index);
|
|
84
|
+
hits.push({
|
|
85
|
+
file,
|
|
86
|
+
line: queryLine,
|
|
87
|
+
kind: 'bulk_data_access_before_authorization_check',
|
|
88
|
+
excerpt: firstLine(call?.text ?? match[0]).slice(0, 160),
|
|
89
|
+
authorization_line: authorizationLine,
|
|
90
|
+
source_kind: 'runtime_code',
|
|
91
|
+
confidence: 'medium',
|
|
92
|
+
gate_effect: 'review'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
match = bulkQueryPattern.exec(code);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function collectResponsibilityHotspots(hits, file, code) {
|
|
100
|
+
if (!isRuntimeCode(file)) return;
|
|
101
|
+
const signals = detectResponsibilitySignals(file, code);
|
|
102
|
+
const lineCount = code.split(/\r?\n/).length;
|
|
103
|
+
const signalCount = Object.values(signals).filter(Boolean).length;
|
|
104
|
+
const isHotspot = (lineCount >= 150 && signalCount >= 4) || (lineCount >= 300 && signalCount >= 3);
|
|
105
|
+
if (!isHotspot) return;
|
|
106
|
+
|
|
107
|
+
hits.push({
|
|
108
|
+
file,
|
|
109
|
+
line: 1,
|
|
110
|
+
kind: 'mixed_responsibility_hotspot',
|
|
111
|
+
line_count: lineCount,
|
|
112
|
+
signals: Object.entries(signals).filter(([, active]) => active).map(([name]) => name),
|
|
113
|
+
source_kind: 'runtime_code',
|
|
114
|
+
confidence: signalCount >= 5 ? 'high' : 'medium',
|
|
115
|
+
gate_effect: 'review'
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function collectQueryShapes(occurrences, file, code) {
|
|
120
|
+
if (!isRuntimeCode(file)) return;
|
|
121
|
+
const pattern = /\b(?:prisma|db|client|prismaAny)\.([A-Za-z_$][\w$]*)\.(findMany|findUnique|findFirst|count|create|update|deleteMany)\s*\(/g;
|
|
122
|
+
let match = pattern.exec(code);
|
|
123
|
+
while (match) {
|
|
124
|
+
const call = extractCall(code, match.index);
|
|
125
|
+
if (!call) {
|
|
126
|
+
match = pattern.exec(code);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const signature = buildQueryShapeSignature(match[1], match[2], call.text);
|
|
130
|
+
if (!signature) {
|
|
131
|
+
match = pattern.exec(code);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const item = {
|
|
135
|
+
file,
|
|
136
|
+
line: lineNumberAt(code, match.index),
|
|
137
|
+
excerpt: firstLine(call.text).slice(0, 160)
|
|
138
|
+
};
|
|
139
|
+
occurrences.set(signature, [...(occurrences.get(signature) ?? []), item]);
|
|
140
|
+
pattern.lastIndex = call.end;
|
|
141
|
+
match = pattern.exec(code);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildDuplicateQueryShapes(occurrences) {
|
|
146
|
+
const duplicates = [];
|
|
147
|
+
for (const [signature, items] of occurrences.entries()) {
|
|
148
|
+
const files = unique(items.map((item) => item.file));
|
|
149
|
+
if (items.length < 3 && files.length < 2) continue;
|
|
150
|
+
if (items.length < 2) continue;
|
|
151
|
+
duplicates.push({
|
|
152
|
+
signature,
|
|
153
|
+
kind: 'duplicate_prisma_query_shape',
|
|
154
|
+
occurrence_count: items.length,
|
|
155
|
+
file_count: files.length,
|
|
156
|
+
files,
|
|
157
|
+
examples: items.slice(0, 8),
|
|
158
|
+
source_kind: 'runtime_code',
|
|
159
|
+
confidence: files.length >= 2 ? 'medium' : 'low',
|
|
160
|
+
gate_effect: files.length >= 2 ? 'review' : 'info'
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return duplicates.sort((a, b) => b.file_count - a.file_count || b.occurrence_count - a.occurrence_count);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildQueryShapeSignature(model, operation, callText) {
|
|
167
|
+
const topLevelKeys = extractObjectKeys(callText);
|
|
168
|
+
if (topLevelKeys.length === 0) return null;
|
|
169
|
+
const whereKeys = extractNestedObjectKeys(callText, 'where');
|
|
170
|
+
const selectKeys = extractNestedObjectKeys(callText, 'select');
|
|
171
|
+
const orderKeys = extractNestedObjectKeys(callText, 'orderBy');
|
|
172
|
+
return [
|
|
173
|
+
`${model}.${operation}`,
|
|
174
|
+
`top:${topLevelKeys.join(',')}`,
|
|
175
|
+
`where:${whereKeys.join(',') || '-'}`,
|
|
176
|
+
`select:${selectKeys.join(',') || '-'}`,
|
|
177
|
+
`order:${orderKeys.join(',') || '-'}`
|
|
178
|
+
].join('|');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function extractObjectKeys(text) {
|
|
182
|
+
const objectStart = text.indexOf('{');
|
|
183
|
+
if (objectStart === -1) return [];
|
|
184
|
+
const objectText = text.slice(objectStart);
|
|
185
|
+
const keys = [];
|
|
186
|
+
const pattern = /(?:^|[,{]\s*)([A-Za-z_$][\w$]*)\s*:/g;
|
|
187
|
+
let match = pattern.exec(objectText);
|
|
188
|
+
while (match) {
|
|
189
|
+
keys.push(match[1]);
|
|
190
|
+
match = pattern.exec(objectText);
|
|
191
|
+
}
|
|
192
|
+
return unique(keys).sort();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function extractNestedObjectKeys(text, propertyName) {
|
|
196
|
+
const pattern = new RegExp(`\\b${propertyName}\\s*:\\s*\\{`, 'g');
|
|
197
|
+
const match = pattern.exec(text);
|
|
198
|
+
if (!match) return [];
|
|
199
|
+
const start = text.indexOf('{', match.index);
|
|
200
|
+
const object = extractBalancedBlock(text, start, '{', '}');
|
|
201
|
+
if (!object) return [];
|
|
202
|
+
return extractObjectKeys(object.text).filter((key) => key !== propertyName);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function detectResponsibilitySignals(file, code) {
|
|
206
|
+
return {
|
|
207
|
+
route_or_action: /(^|\/)(app\/api|pages\/api)\//.test(file) || /\bexport\s+async\s+function\s+(GET|POST|PUT|PATCH|DELETE)\b/.test(code) || /['"]use server['"]/.test(code),
|
|
208
|
+
data_access: /\b(?:prisma|db|client|prismaAny)\.[A-Za-z_$][\w$]*\./.test(code),
|
|
209
|
+
auth: /\b(auth|session|getServerSession|currentUser|requireAuth|authorization|Bearer|token)\b/i.test(code),
|
|
210
|
+
validation: /\b(z\.object|safeParse|parse\(|schema|validate|validation)\b/i.test(code),
|
|
211
|
+
external_io: /\b(fetch|axios|twilio|openai|stripe|resend|sendgrid|webhook|blob)\b/i.test(code),
|
|
212
|
+
notification: /\b(notify|notification|email|slack|webhook)\b/i.test(code),
|
|
213
|
+
environment: /\bprocess\.env\.[A-Z0-9_]+\b/.test(code)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isRuntimeCode(file) {
|
|
218
|
+
const normalized = file.toLowerCase();
|
|
219
|
+
if (!/^(src|app|pages|lib)\//.test(normalized)) return false;
|
|
220
|
+
if (normalized.startsWith('scripts/') || normalized.startsWith('docs/')) return false;
|
|
221
|
+
if (/(^|\/)(__tests__|tests?|spec|fixtures?)(\/|$)/.test(normalized)
|
|
222
|
+
|| /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(normalized)) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function findFirstLine(content, patterns) {
|
|
229
|
+
const lines = [];
|
|
230
|
+
for (const pattern of patterns) {
|
|
231
|
+
let match = pattern.exec(content);
|
|
232
|
+
while (match) {
|
|
233
|
+
lines.push(lineNumberAt(content, match.index));
|
|
234
|
+
match = pattern.exec(content);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return lines.length === 0 ? null : Math.min(...lines);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function extractCall(content, startIndex) {
|
|
241
|
+
const openIndex = content.indexOf('(', startIndex);
|
|
242
|
+
if (openIndex === -1) return null;
|
|
243
|
+
const block = extractBalancedBlock(content, openIndex, '(', ')');
|
|
244
|
+
if (!block) return null;
|
|
245
|
+
return {
|
|
246
|
+
text: content.slice(startIndex, block.end),
|
|
247
|
+
end: block.end
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function extractBalancedBlock(content, openIndex, openChar, closeChar) {
|
|
252
|
+
let depth = 0;
|
|
253
|
+
let quote = null;
|
|
254
|
+
let escaped = false;
|
|
255
|
+
|
|
256
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
257
|
+
const char = content[index];
|
|
258
|
+
if (quote) {
|
|
259
|
+
if (escaped) {
|
|
260
|
+
escaped = false;
|
|
261
|
+
} else if (char === '\\') {
|
|
262
|
+
escaped = true;
|
|
263
|
+
} else if (char === quote) {
|
|
264
|
+
quote = null;
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
269
|
+
quote = char;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (char === openChar) depth += 1;
|
|
273
|
+
if (char === closeChar) {
|
|
274
|
+
depth -= 1;
|
|
275
|
+
if (depth === 0) {
|
|
276
|
+
return {
|
|
277
|
+
text: content.slice(openIndex, index + 1),
|
|
278
|
+
end: index + 1
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function stripComments(content) {
|
|
287
|
+
return content
|
|
288
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
289
|
+
.replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function lineNumberAt(content, index) {
|
|
293
|
+
return content.slice(0, index).split(/\r?\n/).length;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function firstLine(text) {
|
|
297
|
+
return text.trim().split(/\r?\n/)[0].trim();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function summarizeGateEffects(hits) {
|
|
301
|
+
const summary = Object.fromEntries(GATE_EFFECTS.map((effect) => [effect, 0]));
|
|
302
|
+
for (const hit of hits ?? []) {
|
|
303
|
+
if (summary[hit.gate_effect] !== undefined) summary[hit.gate_effect] += 1;
|
|
304
|
+
}
|
|
305
|
+
return summary;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function unique(values) {
|
|
309
|
+
return [...new Set(values.filter(Boolean))];
|
|
310
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
|
+
const TEMPLATE_PATH = path.join(PACKAGE_ROOT, 'agent-instructions', 'codex', 'AGENTS.vibepro.md');
|
|
7
|
+
const TARGET_FILE = 'AGENTS.md';
|
|
8
|
+
const START_MARKER = '<!-- VIBEPRO_CODEX_START -->';
|
|
9
|
+
const END_MARKER = '<!-- VIBEPRO_CODEX_END -->';
|
|
10
|
+
|
|
11
|
+
export async function installCodexInstructions(repoRoot, options = {}) {
|
|
12
|
+
const root = path.resolve(repoRoot);
|
|
13
|
+
const target = path.join(root, TARGET_FILE);
|
|
14
|
+
const block = await readBundledBlock();
|
|
15
|
+
const existing = await readOptional(target);
|
|
16
|
+
const inspection = inspectContent(existing, block);
|
|
17
|
+
const dryRun = Boolean(options.dryRun);
|
|
18
|
+
const force = Boolean(options.force);
|
|
19
|
+
let status = 'up_to_date';
|
|
20
|
+
let nextContent = existing;
|
|
21
|
+
|
|
22
|
+
if (existing === null) {
|
|
23
|
+
status = dryRun ? 'would_install' : 'installed';
|
|
24
|
+
nextContent = `${block}\n`;
|
|
25
|
+
} else if (!inspection.has_block) {
|
|
26
|
+
status = dryRun ? 'would_append' : 'appended';
|
|
27
|
+
nextContent = appendBlock(existing, block);
|
|
28
|
+
} else if (!inspection.matches_bundled) {
|
|
29
|
+
status = force
|
|
30
|
+
? (dryRun ? 'would_overwrite' : 'overwritten')
|
|
31
|
+
: 'skipped';
|
|
32
|
+
if (force) nextContent = replaceBlock(existing, block);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!dryRun && ['installed', 'appended', 'overwritten'].includes(status)) {
|
|
36
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
37
|
+
await writeFile(target, nextContent);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
mode: 'install',
|
|
42
|
+
dry_run: dryRun,
|
|
43
|
+
force,
|
|
44
|
+
target_root: root,
|
|
45
|
+
target_path: TARGET_FILE,
|
|
46
|
+
status,
|
|
47
|
+
has_existing_file: existing !== null,
|
|
48
|
+
has_managed_block: inspection.has_block,
|
|
49
|
+
matches_bundled: inspection.matches_bundled
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function verifyCodexInstructions(repoRoot) {
|
|
54
|
+
const root = path.resolve(repoRoot);
|
|
55
|
+
const target = path.join(root, TARGET_FILE);
|
|
56
|
+
const block = await readBundledBlock();
|
|
57
|
+
const existing = await readOptional(target);
|
|
58
|
+
const inspection = inspectContent(existing, block);
|
|
59
|
+
let status = 'ok';
|
|
60
|
+
if (existing === null || !inspection.has_block) status = 'missing';
|
|
61
|
+
else if (!inspection.matches_bundled) status = 'outdated';
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
mode: 'verify',
|
|
65
|
+
target_root: root,
|
|
66
|
+
target_path: TARGET_FILE,
|
|
67
|
+
overall_status: status === 'ok' ? 'ok' : 'needs_install',
|
|
68
|
+
status,
|
|
69
|
+
has_existing_file: existing !== null,
|
|
70
|
+
has_managed_block: inspection.has_block,
|
|
71
|
+
matches_bundled: inspection.matches_bundled
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function renderCodexInstall(result) {
|
|
76
|
+
return renderCodexResult('VibePro Codex Install', result);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function renderCodexVerify(result) {
|
|
80
|
+
return renderCodexResult('VibePro Codex Verify', result);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function readBundledBlock() {
|
|
84
|
+
const template = (await readFile(TEMPLATE_PATH, 'utf8')).trim();
|
|
85
|
+
return `${START_MARKER}\n${template}\n${END_MARKER}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function inspectContent(content, bundledBlock) {
|
|
89
|
+
if (content === null) {
|
|
90
|
+
return {
|
|
91
|
+
has_block: false,
|
|
92
|
+
matches_bundled: false
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const currentBlock = extractBlock(content);
|
|
96
|
+
return {
|
|
97
|
+
has_block: currentBlock !== null,
|
|
98
|
+
matches_bundled: currentBlock === bundledBlock
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function appendBlock(content, block) {
|
|
103
|
+
const trimmed = content.trimEnd();
|
|
104
|
+
if (!trimmed) return `${block}\n`;
|
|
105
|
+
return `${trimmed}\n\n${block}\n`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function replaceBlock(content, block) {
|
|
109
|
+
const start = content.indexOf(START_MARKER);
|
|
110
|
+
const end = content.indexOf(END_MARKER, start);
|
|
111
|
+
if (start === -1 || end === -1) return appendBlock(content, block);
|
|
112
|
+
return `${content.slice(0, start)}${block}${content.slice(end + END_MARKER.length)}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractBlock(content) {
|
|
116
|
+
const start = content.indexOf(START_MARKER);
|
|
117
|
+
if (start === -1) return null;
|
|
118
|
+
const end = content.indexOf(END_MARKER, start);
|
|
119
|
+
if (end === -1) return null;
|
|
120
|
+
return content.slice(start, end + END_MARKER.length).trim();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function readOptional(filePath) {
|
|
124
|
+
try {
|
|
125
|
+
await stat(filePath);
|
|
126
|
+
return await readFile(filePath, 'utf8');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code === 'ENOENT') return null;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderCodexResult(title, result) {
|
|
134
|
+
const lines = [
|
|
135
|
+
title,
|
|
136
|
+
'',
|
|
137
|
+
`Target: ${path.join(result.target_root, result.target_path)}`,
|
|
138
|
+
`Status: ${result.status}`
|
|
139
|
+
];
|
|
140
|
+
if (result.overall_status) lines.push(`Overall: ${result.overall_status}`);
|
|
141
|
+
if (result.status === 'skipped') lines.push('Hint: rerun with --force to replace the managed VibePro block.');
|
|
142
|
+
return `${lines.join('\n')}\n`;
|
|
143
|
+
}
|