@vibecheckai/cli 3.7.0 → 3.9.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/README.md +135 -63
- package/bin/_deprecations.js +447 -19
- package/bin/_router.js +1 -1
- package/bin/registry.js +347 -280
- package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/index.js +62 -3
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/engine/ast-cache.js +210 -210
- package/bin/runners/lib/engine/auth-extractor.js +211 -211
- package/bin/runners/lib/engine/billing-extractor.js +112 -112
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
- package/bin/runners/lib/engine/env-extractor.js +207 -207
- package/bin/runners/lib/engine/express-extractor.js +208 -208
- package/bin/runners/lib/engine/extractors.js +849 -849
- package/bin/runners/lib/engine/index.js +207 -207
- package/bin/runners/lib/engine/repo-index.js +514 -514
- package/bin/runners/lib/engine/types.js +124 -124
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/entitlements-v2.js +2 -2
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +421 -32
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output.js +1 -1
- package/bin/runners/lib/unified-cli-output.js +710 -383
- package/bin/runners/lib/upsell.js +3 -3
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/runAllowlist.js +33 -4
- package/bin/runners/runApprove.js +240 -1122
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +325 -29
- package/bin/runners/runCheckpoint.js +442 -494
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runDoctor.js +269 -19
- package/bin/runners/runFix.js +411 -32
- package/bin/runners/runForge.js +411 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1741 -837
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.js +41 -0
- package/bin/runners/runReality.js +178 -1
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +21 -9
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +395 -16
- package/bin/vibecheck.js +34 -6
- package/mcp-server/README.md +117 -158
- package/mcp-server/handlers/index.ts +2 -2
- package/mcp-server/handlers/tool-handler.ts +50 -11
- package/mcp-server/index.js +16 -0
- package/mcp-server/intent-firewall-interceptor.js +529 -0
- package/mcp-server/lib/executor.ts +5 -5
- package/mcp-server/lib/index.ts +14 -4
- package/mcp-server/lib/sandbox.test.ts +4 -4
- package/mcp-server/lib/sandbox.ts +2 -2
- package/mcp-server/manifest.json +473 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry/tool-registry.js +315 -523
- package/mcp-server/registry/tools.json +442 -428
- package/mcp-server/registry.test.ts +18 -12
- package/mcp-server/tier-auth.js +68 -11
- package/mcp-server/tools-v3.js +70 -16
- package/mcp-server/tsconfig.json +1 -0
- package/package.json +2 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Artifact Envelope
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* Single envelope schema for ALL VibeCheck artifacts
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* Used by: audit, ship, reality, shield, fix, packs
|
|
9
|
+
*
|
|
10
|
+
* @module artifact-envelope
|
|
11
|
+
* @version 1.0.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
const crypto = require("crypto");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const { execSync } = require("child_process");
|
|
20
|
+
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
// CONSTANTS
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
const ENVELOPE_VERSION = "1.0.0";
|
|
26
|
+
const SCHEMA_URL = "https://vibecheck.dev/schemas/artifact-envelope.v1.json";
|
|
27
|
+
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
// ARTIFACT TYPES
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
const ARTIFACT_TYPES = {
|
|
33
|
+
AUDIT: "audit",
|
|
34
|
+
SHIP: "ship",
|
|
35
|
+
REALITY: "reality",
|
|
36
|
+
SHIELD: "shield",
|
|
37
|
+
FIX: "fix",
|
|
38
|
+
PACK: "pack",
|
|
39
|
+
CHECKPOINT: "checkpoint",
|
|
40
|
+
INTENT: "intent",
|
|
41
|
+
VERDICT: "verdict",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
45
|
+
// REPO INFO
|
|
46
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get repository information for artifact envelope
|
|
50
|
+
*
|
|
51
|
+
* @param {string} projectRoot - Project root directory
|
|
52
|
+
* @returns {Object} Repository info
|
|
53
|
+
*/
|
|
54
|
+
function getRepoInfo(projectRoot) {
|
|
55
|
+
const info = {
|
|
56
|
+
root: projectRoot,
|
|
57
|
+
name: path.basename(projectRoot),
|
|
58
|
+
commit: null,
|
|
59
|
+
branch: null,
|
|
60
|
+
treeHash: null,
|
|
61
|
+
isDirty: true,
|
|
62
|
+
fingerprint: null,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Check if it's a git repo
|
|
67
|
+
execSync("git rev-parse --git-dir", {
|
|
68
|
+
cwd: projectRoot,
|
|
69
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Get commit hash
|
|
73
|
+
info.commit = execSync("git rev-parse HEAD", {
|
|
74
|
+
cwd: projectRoot,
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
77
|
+
}).trim();
|
|
78
|
+
|
|
79
|
+
// Get branch name
|
|
80
|
+
try {
|
|
81
|
+
info.branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
82
|
+
cwd: projectRoot,
|
|
83
|
+
encoding: "utf-8",
|
|
84
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
85
|
+
}).trim();
|
|
86
|
+
} catch {
|
|
87
|
+
info.branch = "detached";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Get tree hash
|
|
91
|
+
info.treeHash = execSync("git rev-parse HEAD^{tree}", {
|
|
92
|
+
cwd: projectRoot,
|
|
93
|
+
encoding: "utf-8",
|
|
94
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
95
|
+
}).trim();
|
|
96
|
+
|
|
97
|
+
// Check if dirty
|
|
98
|
+
const status = execSync("git status --porcelain", {
|
|
99
|
+
cwd: projectRoot,
|
|
100
|
+
encoding: "utf-8",
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
102
|
+
}).trim();
|
|
103
|
+
info.isDirty = status.length > 0;
|
|
104
|
+
|
|
105
|
+
// Compute fingerprint
|
|
106
|
+
info.fingerprint = computeRepoFingerprint(projectRoot, info);
|
|
107
|
+
|
|
108
|
+
} catch {
|
|
109
|
+
// Not a git repo or git not available
|
|
110
|
+
info.fingerprint = computeNonGitFingerprint(projectRoot);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return info;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compute deterministic repo fingerprint
|
|
118
|
+
*
|
|
119
|
+
* Same repo state → Same fingerprint. Always.
|
|
120
|
+
*/
|
|
121
|
+
function computeRepoFingerprint(projectRoot, repoInfo) {
|
|
122
|
+
// Config files that affect fingerprint
|
|
123
|
+
const configFiles = [
|
|
124
|
+
".vibecheckrc",
|
|
125
|
+
".vibecheckrc.json",
|
|
126
|
+
"package.json",
|
|
127
|
+
"pnpm-lock.yaml",
|
|
128
|
+
"package-lock.json",
|
|
129
|
+
"yarn.lock",
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const configHashes = configFiles.map(f => {
|
|
133
|
+
const filePath = path.join(projectRoot, f);
|
|
134
|
+
if (fs.existsSync(filePath)) {
|
|
135
|
+
return crypto.createHash("sha256")
|
|
136
|
+
.update(fs.readFileSync(filePath))
|
|
137
|
+
.digest("hex")
|
|
138
|
+
.slice(0, 8);
|
|
139
|
+
}
|
|
140
|
+
return "missing";
|
|
141
|
+
}).join("|");
|
|
142
|
+
|
|
143
|
+
const input = `${repoInfo.commit}|${repoInfo.treeHash}|${configHashes}`;
|
|
144
|
+
|
|
145
|
+
return `sha256:${crypto.createHash("sha256").update(input).digest("hex")}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Compute fingerprint for non-git projects
|
|
150
|
+
*/
|
|
151
|
+
function computeNonGitFingerprint(projectRoot) {
|
|
152
|
+
const input = `${projectRoot}|${Date.now()}`;
|
|
153
|
+
return `nongit:${crypto.createHash("sha256").update(input).digest("hex").slice(0, 32)}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
157
|
+
// ARTIFACT CREATION
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create a new artifact envelope
|
|
162
|
+
*
|
|
163
|
+
* @param {string} type - Artifact type (audit, ship, reality, etc.)
|
|
164
|
+
* @param {Object} payload - Command-specific payload
|
|
165
|
+
* @param {string} projectRoot - Project root directory
|
|
166
|
+
* @param {Object} options - Additional options
|
|
167
|
+
* @returns {Object} Complete artifact envelope
|
|
168
|
+
*/
|
|
169
|
+
function createArtifactEnvelope(type, payload, projectRoot, options = {}) {
|
|
170
|
+
const timestamp = new Date().toISOString();
|
|
171
|
+
const randomId = crypto.randomBytes(4).toString("hex");
|
|
172
|
+
|
|
173
|
+
const envelope = {
|
|
174
|
+
$schema: SCHEMA_URL,
|
|
175
|
+
version: ENVELOPE_VERSION,
|
|
176
|
+
|
|
177
|
+
metadata: {
|
|
178
|
+
id: `${type}-${Date.now()}-${randomId}`,
|
|
179
|
+
type,
|
|
180
|
+
command: options.command || `vibecheck ${type}`,
|
|
181
|
+
generatedAt: timestamp,
|
|
182
|
+
durationMs: options.durationMs || 0,
|
|
183
|
+
tool: {
|
|
184
|
+
name: "vibecheck",
|
|
185
|
+
version: getToolVersion(),
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
repo: getRepoInfo(projectRoot),
|
|
190
|
+
|
|
191
|
+
payload,
|
|
192
|
+
|
|
193
|
+
evidence: {
|
|
194
|
+
files: options.files || [],
|
|
195
|
+
proofs: options.proofs || [],
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
signature: null, // Will be computed
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Compute signature
|
|
202
|
+
envelope.signature = computeSignature(envelope);
|
|
203
|
+
|
|
204
|
+
return envelope;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get tool version from package.json
|
|
209
|
+
*/
|
|
210
|
+
function getToolVersion() {
|
|
211
|
+
try {
|
|
212
|
+
const pkgPath = path.join(__dirname, "..", "..", "..", "package.json");
|
|
213
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
214
|
+
return pkg.version || "0.0.0";
|
|
215
|
+
} catch {
|
|
216
|
+
return "0.0.0";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
221
|
+
// SIGNATURE
|
|
222
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Compute artifact signature
|
|
226
|
+
*
|
|
227
|
+
* @param {Object} envelope - Artifact envelope (without signature)
|
|
228
|
+
* @returns {Object} Signature object
|
|
229
|
+
*/
|
|
230
|
+
function computeSignature(envelope) {
|
|
231
|
+
const fieldsToSign = ["metadata", "repo", "payload", "evidence"];
|
|
232
|
+
|
|
233
|
+
// Create canonical JSON (no whitespace, deterministic key order)
|
|
234
|
+
const dataToSign = fieldsToSign.reduce((acc, field) => {
|
|
235
|
+
acc[field] = envelope[field];
|
|
236
|
+
return acc;
|
|
237
|
+
}, {});
|
|
238
|
+
|
|
239
|
+
const canonical = JSON.stringify(dataToSign, Object.keys(dataToSign).sort(), 0);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
algorithm: "sha256",
|
|
243
|
+
digest: crypto.createHash("sha256").update(canonical).digest("hex"),
|
|
244
|
+
signedFields: fieldsToSign,
|
|
245
|
+
verifiable: true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Verify artifact signature
|
|
251
|
+
*
|
|
252
|
+
* @param {Object} envelope - Artifact envelope with signature
|
|
253
|
+
* @returns {Object} Verification result
|
|
254
|
+
*/
|
|
255
|
+
function verifySignature(envelope) {
|
|
256
|
+
if (!envelope.signature) {
|
|
257
|
+
return { valid: false, reason: "No signature present" };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Recompute expected signature
|
|
261
|
+
const envelopeWithoutSig = { ...envelope, signature: null };
|
|
262
|
+
const expected = computeSignature(envelopeWithoutSig);
|
|
263
|
+
|
|
264
|
+
if (envelope.signature.digest !== expected.digest) {
|
|
265
|
+
return {
|
|
266
|
+
valid: false,
|
|
267
|
+
reason: "Digest mismatch",
|
|
268
|
+
expected: expected.digest,
|
|
269
|
+
actual: envelope.signature.digest,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { valid: true };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
277
|
+
// ARTIFACT NAMING
|
|
278
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Generate deterministic artifact filename
|
|
282
|
+
*
|
|
283
|
+
* Pattern: {type}-{YYYYMMDD}-{HHMMSS}-{hash8}.json
|
|
284
|
+
*
|
|
285
|
+
* @param {string} type - Artifact type
|
|
286
|
+
* @param {string} extension - File extension (default: json)
|
|
287
|
+
* @returns {string} Filename
|
|
288
|
+
*/
|
|
289
|
+
function generateArtifactFilename(type, extension = "json") {
|
|
290
|
+
const now = new Date();
|
|
291
|
+
const datePart = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
292
|
+
const timePart = now.toISOString().slice(11, 19).replace(/:/g, "");
|
|
293
|
+
const hash = crypto.randomBytes(4).toString("hex");
|
|
294
|
+
|
|
295
|
+
return `${type}-${datePart}-${timePart}-${hash}.${extension}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Generate artifact directory path
|
|
300
|
+
*
|
|
301
|
+
* @param {string} projectRoot - Project root
|
|
302
|
+
* @param {string} type - Artifact type
|
|
303
|
+
* @returns {string} Directory path
|
|
304
|
+
*/
|
|
305
|
+
function getArtifactDir(projectRoot, type) {
|
|
306
|
+
const typeToDir = {
|
|
307
|
+
[ARTIFACT_TYPES.AUDIT]: "audit",
|
|
308
|
+
[ARTIFACT_TYPES.SHIP]: "ship",
|
|
309
|
+
[ARTIFACT_TYPES.REALITY]: "reality",
|
|
310
|
+
[ARTIFACT_TYPES.SHIELD]: "shield",
|
|
311
|
+
[ARTIFACT_TYPES.FIX]: "fix",
|
|
312
|
+
[ARTIFACT_TYPES.PACK]: "packs",
|
|
313
|
+
[ARTIFACT_TYPES.CHECKPOINT]: "checkpoints",
|
|
314
|
+
[ARTIFACT_TYPES.INTENT]: "shield/intents",
|
|
315
|
+
[ARTIFACT_TYPES.VERDICT]: "shield/verdicts",
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const subdir = typeToDir[type] || type;
|
|
319
|
+
return path.join(projectRoot, ".vibecheck", subdir);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
323
|
+
// ARTIFACT I/O
|
|
324
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Save artifact to disk
|
|
328
|
+
*
|
|
329
|
+
* @param {Object} envelope - Artifact envelope
|
|
330
|
+
* @param {string} projectRoot - Project root
|
|
331
|
+
* @param {Object} options - Save options
|
|
332
|
+
* @returns {Object} Save result with path
|
|
333
|
+
*/
|
|
334
|
+
function saveArtifact(envelope, projectRoot, options = {}) {
|
|
335
|
+
const dir = options.dir || getArtifactDir(projectRoot, envelope.metadata.type);
|
|
336
|
+
const filename = options.filename || generateArtifactFilename(envelope.metadata.type);
|
|
337
|
+
const filePath = path.join(dir, filename);
|
|
338
|
+
|
|
339
|
+
// Ensure directory exists
|
|
340
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
341
|
+
|
|
342
|
+
// Write artifact
|
|
343
|
+
fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
|
|
344
|
+
|
|
345
|
+
// Also write "latest" symlink/copy
|
|
346
|
+
const latestPath = path.join(dir, `last_${envelope.metadata.type}.json`);
|
|
347
|
+
fs.writeFileSync(latestPath, JSON.stringify(envelope, null, 2));
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
success: true,
|
|
351
|
+
path: filePath,
|
|
352
|
+
latestPath,
|
|
353
|
+
id: envelope.metadata.id,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Load artifact from disk
|
|
359
|
+
*
|
|
360
|
+
* @param {string} filePath - Full path to artifact
|
|
361
|
+
* @param {Object} options - Load options
|
|
362
|
+
* @returns {Object} Artifact envelope or null
|
|
363
|
+
*/
|
|
364
|
+
function loadArtifact(filePath, options = {}) {
|
|
365
|
+
try {
|
|
366
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
367
|
+
const envelope = JSON.parse(content);
|
|
368
|
+
|
|
369
|
+
// Optionally verify signature
|
|
370
|
+
if (options.verify) {
|
|
371
|
+
const verification = verifySignature(envelope);
|
|
372
|
+
if (!verification.valid) {
|
|
373
|
+
return {
|
|
374
|
+
envelope: null,
|
|
375
|
+
error: `Invalid signature: ${verification.reason}`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return { envelope, error: null };
|
|
381
|
+
} catch (e) {
|
|
382
|
+
return { envelope: null, error: e.message };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Load latest artifact of a type
|
|
388
|
+
*
|
|
389
|
+
* @param {string} projectRoot - Project root
|
|
390
|
+
* @param {string} type - Artifact type
|
|
391
|
+
* @param {Object} options - Load options
|
|
392
|
+
* @returns {Object} Artifact envelope or null
|
|
393
|
+
*/
|
|
394
|
+
function loadLatestArtifact(projectRoot, type, options = {}) {
|
|
395
|
+
const dir = getArtifactDir(projectRoot, type);
|
|
396
|
+
const latestPath = path.join(dir, `last_${type}.json`);
|
|
397
|
+
|
|
398
|
+
if (!fs.existsSync(latestPath)) {
|
|
399
|
+
return { envelope: null, error: "No artifact found" };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return loadArtifact(latestPath, options);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
406
|
+
// EVIDENCE HELPERS
|
|
407
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Create file evidence entry
|
|
411
|
+
*
|
|
412
|
+
* @param {string} filePath - Relative file path
|
|
413
|
+
* @param {string} lines - Line range (e.g., "42-45")
|
|
414
|
+
* @param {string} projectRoot - Project root for hashing
|
|
415
|
+
* @returns {Object} File evidence entry
|
|
416
|
+
*/
|
|
417
|
+
function createFileEvidence(filePath, lines, projectRoot) {
|
|
418
|
+
const fullPath = path.join(projectRoot, filePath);
|
|
419
|
+
let snippetHash = null;
|
|
420
|
+
|
|
421
|
+
if (fs.existsSync(fullPath) && lines) {
|
|
422
|
+
try {
|
|
423
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
424
|
+
const [start, end] = lines.split("-").map(n => parseInt(n, 10));
|
|
425
|
+
const snippet = content.split("\n").slice(start - 1, end).join("\n");
|
|
426
|
+
snippetHash = `sha256:${crypto.createHash("sha256").update(snippet).digest("hex").slice(0, 16)}`;
|
|
427
|
+
} catch {
|
|
428
|
+
// Ignore errors
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
path: filePath,
|
|
434
|
+
lines,
|
|
435
|
+
snippetHash,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Create proof entry
|
|
441
|
+
*
|
|
442
|
+
* @param {string} type - Proof type (file_citation, runtime, assertion)
|
|
443
|
+
* @param {boolean} verified - Whether proof verified
|
|
444
|
+
* @param {Object} details - Additional details
|
|
445
|
+
* @returns {Object} Proof entry
|
|
446
|
+
*/
|
|
447
|
+
function createProof(type, verified, details = {}) {
|
|
448
|
+
return {
|
|
449
|
+
id: `prf-${type.slice(0, 5)}-${crypto.randomBytes(4).toString("hex")}`,
|
|
450
|
+
type,
|
|
451
|
+
status: verified ? "verified" : "failed",
|
|
452
|
+
verifiedAt: new Date().toISOString(),
|
|
453
|
+
...details,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
458
|
+
// CROSS-REFERENCES
|
|
459
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Create cross-reference to another artifact
|
|
463
|
+
*
|
|
464
|
+
* @param {string} type - Referenced artifact type
|
|
465
|
+
* @param {string} projectRoot - Project root
|
|
466
|
+
* @returns {Object} Cross-reference object
|
|
467
|
+
*/
|
|
468
|
+
function createCrossRef(type, projectRoot) {
|
|
469
|
+
const dir = getArtifactDir(projectRoot, type);
|
|
470
|
+
const latestPath = path.join(dir, `last_${type}.json`);
|
|
471
|
+
|
|
472
|
+
// Use relative path
|
|
473
|
+
const relativePath = path.relative(
|
|
474
|
+
path.join(projectRoot, ".vibecheck"),
|
|
475
|
+
latestPath
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
$ref: `./${relativePath}`,
|
|
480
|
+
type,
|
|
481
|
+
exists: fs.existsSync(latestPath),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Build receipts object with cross-references
|
|
487
|
+
*
|
|
488
|
+
* @param {string} projectRoot - Project root
|
|
489
|
+
* @param {string[]} types - Types to include
|
|
490
|
+
* @returns {Object} Receipts object
|
|
491
|
+
*/
|
|
492
|
+
function buildReceiptsObject(projectRoot, types = ["audit", "ship", "reality", "shield"]) {
|
|
493
|
+
const receipts = {};
|
|
494
|
+
|
|
495
|
+
for (const type of types) {
|
|
496
|
+
const ref = createCrossRef(type, projectRoot);
|
|
497
|
+
if (ref.exists) {
|
|
498
|
+
receipts[type] = ref;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return receipts;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
506
|
+
// EXPORTS
|
|
507
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
508
|
+
|
|
509
|
+
module.exports = {
|
|
510
|
+
// Constants
|
|
511
|
+
ENVELOPE_VERSION,
|
|
512
|
+
SCHEMA_URL,
|
|
513
|
+
ARTIFACT_TYPES,
|
|
514
|
+
|
|
515
|
+
// Core functions
|
|
516
|
+
createArtifactEnvelope,
|
|
517
|
+
computeSignature,
|
|
518
|
+
verifySignature,
|
|
519
|
+
|
|
520
|
+
// Repo info
|
|
521
|
+
getRepoInfo,
|
|
522
|
+
computeRepoFingerprint,
|
|
523
|
+
|
|
524
|
+
// Naming
|
|
525
|
+
generateArtifactFilename,
|
|
526
|
+
getArtifactDir,
|
|
527
|
+
|
|
528
|
+
// I/O
|
|
529
|
+
saveArtifact,
|
|
530
|
+
loadArtifact,
|
|
531
|
+
loadLatestArtifact,
|
|
532
|
+
|
|
533
|
+
// Evidence
|
|
534
|
+
createFileEvidence,
|
|
535
|
+
createProof,
|
|
536
|
+
|
|
537
|
+
// Cross-references
|
|
538
|
+
createCrossRef,
|
|
539
|
+
buildReceiptsObject,
|
|
540
|
+
};
|