contextos-agents 2.0.0 → 2.1.1
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/.agents/AGENTS.md +53 -33
- package/.agents/adapters/aider/export.js +41 -14
- package/.agents/adapters/claude/export.js +54 -3
- package/.agents/adapters/copilot/export.js +1 -1
- package/.agents/adapters/cursor/export.js +1 -1
- package/.agents/adapters/drift-detector.js +86 -10
- package/.agents/adapters/gemini/export.js +1 -1
- package/.agents/adapters/pure-compiler.js +28 -6
- package/.agents/adapters/shared.js +13 -4
- package/.agents/adapters/zed/export.js +1 -1
- package/.agents/compiled/registry.v2.json +29 -25
- package/.agents/compiled/registry.v2.sha256 +1 -1
- package/.agents/core/skills/context-os/SKILL.md +34 -37
- package/.agents/core/skills/engineering-workflow/SKILL.md +24 -24
- package/.agents/core/skills/gemini-precision/EXAMPLES.md +72 -0
- package/.agents/core/skills/gemini-precision/SKILL.md +2 -1
- package/.agents/core/skills/gemini-precision/TROUBLESHOOTING.md +25 -0
- package/.agents/core/skills/gemini-precision/skill.yaml +2 -0
- package/.agents/core/skills/gstack-roles/SKILL.md +7 -6
- package/.agents/core/skills/security/SKILL.md +44 -16
- package/.agents/core/skills/security/skill.yaml +0 -1
- package/.agents/ctx.js +20 -14
- package/.agents/generated/claude/skills/context-os/SKILL.md +34 -37
- package/.agents/generated/claude/skills/engineering-workflow/SKILL.md +24 -24
- package/.agents/generated/claude/skills/gemini-precision/SKILL.md +102 -1
- package/.agents/generated/claude/skills/gstack-roles/SKILL.md +7 -6
- package/.agents/generated/claude/skills/security/SKILL.md +44 -16
- package/.agents/generated/gemini/skills/context-os/SKILL.md +34 -37
- package/.agents/generated/gemini/skills/engineering-workflow/SKILL.md +24 -24
- package/.agents/generated/gemini/skills/gemini-precision/SKILL.md +105 -1
- package/.agents/generated/gemini/skills/gstack-roles/SKILL.md +7 -6
- package/.agents/generated/gemini/skills/security/SKILL.md +44 -125
- package/.agents/plugins.js +105 -8
- package/.agents/profiles.js +32 -11
- package/.agents/resolver/canonical-resolver.js +7 -7
- package/.agents/validate.js +69 -1
- package/README.md +81 -24
- package/bin/commands/hook.js +167 -0
- package/bin/commands/scan.js +77 -0
- package/bin/commands.js +39 -1
- package/bin/index.js +151 -34
- package/bin/lib/gate.js +171 -0
- package/bin/lib/git-snapshot.js +214 -0
- package/bin/lib/scan.js +461 -0
- package/catalog/skills/adapters/EXAMPLES.md +19 -0
- package/catalog/skills/adapters/SKILL.md +101 -0
- package/catalog/skills/adapters/TROUBLESHOOTING.md +7 -0
- package/catalog/skills/adapters/VALIDATION.json +12 -0
- package/catalog/skills/adapters/skill.yaml +13 -0
- package/catalog/skills/api-design/EXAMPLES.md +91 -0
- package/catalog/skills/api-design/SKILL.md +63 -0
- package/catalog/skills/api-design/TROUBLESHOOTING.md +54 -0
- package/catalog/skills/api-design/VALIDATION.json +11 -0
- package/catalog/skills/api-design/skill.yaml +14 -0
- package/catalog/skills/architecture-diagrams/SKILL.md +108 -0
- package/catalog/skills/architecture-diagrams/VALIDATION.json +12 -0
- package/catalog/skills/architecture-diagrams/skill.yaml +9 -0
- package/catalog/skills/brutalist-design/EXAMPLES.md +59 -0
- package/catalog/skills/brutalist-design/SKILL.md +150 -0
- package/catalog/skills/brutalist-design/VALIDATION.json +12 -0
- package/catalog/skills/brutalist-design/skill.yaml +10 -0
- package/catalog/skills/ci-cd/EXAMPLES.md +79 -0
- package/catalog/skills/ci-cd/SKILL.md +69 -0
- package/catalog/skills/ci-cd/TROUBLESHOOTING.md +52 -0
- package/catalog/skills/ci-cd/VALIDATION.json +11 -0
- package/catalog/skills/ci-cd/skill.yaml +13 -0
- package/catalog/skills/database/EXAMPLES.md +74 -0
- package/catalog/skills/database/SKILL.md +101 -0
- package/catalog/skills/database/TROUBLESHOOTING.md +18 -0
- package/catalog/skills/database/VALIDATION.json +11 -0
- package/catalog/skills/database/skill.yaml +14 -0
- package/catalog/skills/ddd/EXAMPLES.md +42 -0
- package/catalog/skills/ddd/SKILL.md +247 -0
- package/catalog/skills/ddd/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/ddd/VALIDATION.json +12 -0
- package/catalog/skills/ddd/skill.yaml +14 -0
- package/catalog/skills/decisions/EXAMPLES.md +35 -0
- package/catalog/skills/decisions/SKILL.md +90 -0
- package/catalog/skills/decisions/TROUBLESHOOTING.md +13 -0
- package/catalog/skills/decisions/VALIDATION.json +12 -0
- package/catalog/skills/decisions/skill.yaml +13 -0
- package/catalog/skills/docker/EXAMPLES.md +56 -0
- package/catalog/skills/docker/SKILL.md +169 -0
- package/catalog/skills/docker/TROUBLESHOOTING.md +18 -0
- package/catalog/skills/docker/VALIDATION.json +11 -0
- package/catalog/skills/docker/skill.yaml +13 -0
- package/catalog/skills/fastapi/EXAMPLES.md +36 -0
- package/catalog/skills/fastapi/SKILL.md +171 -0
- package/catalog/skills/fastapi/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/fastapi/VALIDATION.json +12 -0
- package/catalog/skills/fastapi/skill.yaml +14 -0
- package/catalog/skills/generators/EXAMPLES.md +19 -0
- package/catalog/skills/generators/SKILL.md +110 -0
- package/catalog/skills/generators/TROUBLESHOOTING.md +7 -0
- package/catalog/skills/generators/VALIDATION.json +12 -0
- package/catalog/skills/generators/skill.yaml +22 -0
- package/catalog/skills/generators/templates/API.md +77 -0
- package/catalog/skills/generators/templates/ARCHITECTURE.md +70 -0
- package/catalog/skills/generators/templates/DATABASE.md +42 -0
- package/catalog/skills/generators/templates/DECISION.md +46 -0
- package/catalog/skills/generators/templates/PRD.md +67 -0
- package/catalog/skills/generators/templates/PROJECT_GRAPH.md +56 -0
- package/catalog/skills/generators/templates/ROADMAP.md +51 -0
- package/catalog/skills/generators/templates/TASKS.md +43 -0
- package/catalog/skills/generators/templates/UI.md +73 -0
- package/catalog/skills/graphify/EXAMPLES.md +73 -0
- package/catalog/skills/graphify/SKILL.md +130 -0
- package/catalog/skills/graphify/VALIDATION.json +12 -0
- package/catalog/skills/graphify/skill.yaml +13 -0
- package/catalog/skills/impeccable-design/EXAMPLES.md +26 -0
- package/catalog/skills/impeccable-design/SKILL.md +201 -0
- package/catalog/skills/impeccable-design/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/impeccable-design/VALIDATION.json +12 -0
- package/catalog/skills/impeccable-design/skill.yaml +15 -0
- package/catalog/skills/interview-me/SKILL.md +97 -0
- package/catalog/skills/interview-me/VALIDATION.json +12 -0
- package/catalog/skills/interview-me/skill.yaml +9 -0
- package/catalog/skills/microservices/EXAMPLES.md +38 -0
- package/catalog/skills/microservices/SKILL.md +164 -0
- package/catalog/skills/microservices/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/microservices/VALIDATION.json +12 -0
- package/catalog/skills/microservices/skill.yaml +14 -0
- package/catalog/skills/minimalist-design/EXAMPLES.md +58 -0
- package/catalog/skills/minimalist-design/SKILL.md +113 -0
- package/catalog/skills/minimalist-design/VALIDATION.json +12 -0
- package/catalog/skills/minimalist-design/skill.yaml +10 -0
- package/catalog/skills/nestjs/EXAMPLES.md +40 -0
- package/catalog/skills/nestjs/SKILL.md +139 -0
- package/catalog/skills/nestjs/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/nestjs/VALIDATION.json +12 -0
- package/catalog/skills/nestjs/skill.yaml +14 -0
- package/catalog/skills/nextjs/EXAMPLES.md +40 -0
- package/catalog/skills/nextjs/SKILL.md +163 -0
- package/catalog/skills/nextjs/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/nextjs/VALIDATION.json +12 -0
- package/catalog/skills/nextjs/skill.yaml +14 -0
- package/catalog/skills/node/EXAMPLES.md +80 -0
- package/catalog/skills/node/SKILL.md +128 -0
- package/catalog/skills/node/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/node/VALIDATION.json +12 -0
- package/catalog/skills/node/skill.yaml +14 -0
- package/catalog/skills/performance/EXAMPLES.md +30 -0
- package/catalog/skills/performance/SKILL.md +75 -0
- package/catalog/skills/performance/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/performance/VALIDATION.json +12 -0
- package/catalog/skills/performance/skill.yaml +14 -0
- package/catalog/skills/react/EXAMPLES.md +79 -0
- package/catalog/skills/react/SKILL.md +132 -0
- package/catalog/skills/react/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/react/VALIDATION.json +12 -0
- package/catalog/skills/react/skill.yaml +14 -0
- package/catalog/skills/react-best-practices/SKILL.md +158 -0
- package/catalog/skills/react-best-practices/VALIDATION.json +12 -0
- package/catalog/skills/react-best-practices/skill.yaml +13 -0
- package/catalog/skills/redesign-audit/SKILL.md +117 -0
- package/catalog/skills/redesign-audit/VALIDATION.json +12 -0
- package/catalog/skills/redesign-audit/skill.yaml +9 -0
- package/catalog/skills/security-audit/EXAMPLES.md +79 -0
- package/catalog/skills/security-audit/SKILL.md +91 -0
- package/catalog/skills/security-audit/TROUBLESHOOTING.md +46 -0
- package/catalog/skills/security-audit/VALIDATION.json +11 -0
- package/catalog/skills/security-audit/skill.yaml +14 -0
- package/catalog/skills/soft-design/EXAMPLES.md +51 -0
- package/catalog/skills/soft-design/SKILL.md +108 -0
- package/catalog/skills/soft-design/VALIDATION.json +12 -0
- package/catalog/skills/soft-design/skill.yaml +10 -0
- package/catalog/skills/state-management/EXAMPLES.md +56 -0
- package/catalog/skills/state-management/SKILL.md +168 -0
- package/catalog/skills/state-management/TROUBLESHOOTING.md +18 -0
- package/catalog/skills/state-management/VALIDATION.json +11 -0
- package/catalog/skills/state-management/skill.yaml +14 -0
- package/catalog/skills/subagent-orchestrator/SKILL.md +117 -0
- package/catalog/skills/subagent-orchestrator/VALIDATION.json +12 -0
- package/catalog/skills/subagent-orchestrator/skill.yaml +9 -0
- package/catalog/skills/system-design/EXAMPLES.md +75 -0
- package/catalog/skills/system-design/SKILL.md +419 -0
- package/catalog/skills/system-design/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/system-design/VALIDATION.json +12 -0
- package/catalog/skills/system-design/skill.yaml +14 -0
- package/catalog/skills/terraform/EXAMPLES.md +74 -0
- package/catalog/skills/terraform/SKILL.md +55 -0
- package/catalog/skills/terraform/TROUBLESHOOTING.md +53 -0
- package/catalog/skills/terraform/VALIDATION.json +11 -0
- package/catalog/skills/terraform/skill.yaml +14 -0
- package/catalog/skills/testing/EXAMPLES.md +122 -0
- package/catalog/skills/testing/SKILL.md +70 -0
- package/catalog/skills/testing/TROUBLESHOOTING.md +18 -0
- package/catalog/skills/testing/VALIDATION.json +11 -0
- package/catalog/skills/testing/skill.yaml +14 -0
- package/catalog/skills/typescript/EXAMPLES.md +64 -0
- package/catalog/skills/typescript/SKILL.md +112 -0
- package/catalog/skills/typescript/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/typescript/VALIDATION.json +12 -0
- package/catalog/skills/typescript/skill.yaml +14 -0
- package/catalog/skills/ui-design/EXAMPLES.md +21 -0
- package/catalog/skills/ui-design/SKILL.md +124 -0
- package/catalog/skills/ui-design/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/ui-design/VALIDATION.json +12 -0
- package/catalog/skills/ui-design/skill.yaml +16 -0
- package/catalog/skills/ui-ux-pro/EXAMPLES.md +62 -0
- package/catalog/skills/ui-ux-pro/SKILL.md +418 -0
- package/catalog/skills/ui-ux-pro/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/ui-ux-pro/VALIDATION.json +12 -0
- package/catalog/skills/ui-ux-pro/skill.yaml +14 -0
- package/catalog/skills/ux-design/EXAMPLES.md +36 -0
- package/catalog/skills/ux-design/SKILL.md +116 -0
- package/catalog/skills/ux-design/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/ux-design/VALIDATION.json +12 -0
- package/catalog/skills/ux-design/skill.yaml +16 -0
- package/catalog/skills/vercel-optimize/SKILL.md +83 -0
- package/catalog/skills/vercel-optimize/VALIDATION.json +12 -0
- package/catalog/skills/vercel-optimize/scripts/collect-signals.mjs +131 -0
- package/catalog/skills/vercel-optimize/scripts/gate-investigations.mjs +142 -0
- package/catalog/skills/vercel-optimize/scripts/merge-signals.mjs +143 -0
- package/catalog/skills/vercel-optimize/scripts/scan-codebase.mjs +174 -0
- package/catalog/skills/vercel-optimize/skill.yaml +15 -0
- package/catalog/skills/web-accessibility/EXAMPLES.md +39 -0
- package/catalog/skills/web-accessibility/SKILL.md +151 -0
- package/catalog/skills/web-accessibility/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/web-accessibility/VALIDATION.json +12 -0
- package/catalog/skills/web-accessibility/skill.yaml +14 -0
- package/package.json +5 -2
- package/.agents/core/skills/security/security.md +0 -106
package/bin/index.js
CHANGED
|
@@ -32,6 +32,15 @@ const flags = {
|
|
|
32
32
|
const i = args.indexOf('--add-skill');
|
|
33
33
|
return i !== -1 ? args[i + 1] : null;
|
|
34
34
|
})(),
|
|
35
|
+
project: (() => {
|
|
36
|
+
const i = args.indexOf('--project');
|
|
37
|
+
return i !== -1 && args[i + 1] && !args[i + 1].startsWith('-') ? path.resolve(args[i + 1]) : process.cwd();
|
|
38
|
+
})(),
|
|
39
|
+
target: (() => {
|
|
40
|
+
const i = args.indexOf('--target');
|
|
41
|
+
return i !== -1 && args[i + 1] && !args[i + 1].startsWith('-') ? args[i + 1] : null;
|
|
42
|
+
})(),
|
|
43
|
+
githubAnnotations: args.includes('--github-annotations'),
|
|
35
44
|
};
|
|
36
45
|
|
|
37
46
|
// ── Help / Version ────────────────────────────────────────────────────────────
|
|
@@ -64,12 +73,16 @@ Options:
|
|
|
64
73
|
--minimal Install only 7 core skills (lightweight footprint)
|
|
65
74
|
--profile <name> Install a specific profile (mvp, startup, enterprise, frontend, backend)
|
|
66
75
|
--auto Auto-detect tech stack and apply recommended profile
|
|
76
|
+
--project <path> Specify target project root directory (default: current directory)
|
|
77
|
+
--target <adapters> Target adapter(s) to verify or export (default: all)
|
|
78
|
+
--github-annotations Emit GitHub Actions workflow commands and step summary
|
|
67
79
|
--with-mcp, --mcp [DEPRECATED] Use the separate @contextos/mcp package instead
|
|
68
80
|
--skip-compile Skip running ctx.js export after installation
|
|
69
81
|
--add-skill <ref> Install a community plugin skill after setup
|
|
70
82
|
|
|
71
83
|
Commands:
|
|
72
84
|
init Install and configure .agents/ in target project
|
|
85
|
+
gate Run deterministic quality gate against adapter drift
|
|
73
86
|
status Display project configuration, active profile, and lockfile status
|
|
74
87
|
update Safely update skills without overwriting custom changes
|
|
75
88
|
uninstall Safely uninstall ContextOS files (preserves user custom skills)
|
|
@@ -84,6 +97,10 @@ Commands:
|
|
|
84
97
|
watch Start continuous file watcher and auto-sync daemon
|
|
85
98
|
detect Analyze project and display detected tech stack & IDE
|
|
86
99
|
install-skill Interactive skill installer (or pass <ref> / --from-repo)
|
|
100
|
+
scan Scan Git staged changes for secrets, placeholders, and scope
|
|
101
|
+
hook <cmd> Manage isolated pre-commit Git governance hooks (install, uninstall)
|
|
102
|
+
recover List or recover from interrupted filesystem transactions
|
|
103
|
+
explain [rule] Inspect rule catalog, enforcement levels, and checkers
|
|
87
104
|
setup-mcp [DEPRECATED] Add MCP execution server to an existing .agents/ project
|
|
88
105
|
|
|
89
106
|
Profiles:
|
|
@@ -250,20 +267,141 @@ if (mainCommand === 'recover') {
|
|
|
250
267
|
process.exit(0);
|
|
251
268
|
}
|
|
252
269
|
|
|
253
|
-
//
|
|
270
|
+
// Gate command (deterministic quality gate verification)
|
|
271
|
+
if (mainCommand === 'gate') {
|
|
272
|
+
const { runGate, emitGitHubAnnotations, writeGitHubSummary } = require('./lib/gate.js');
|
|
273
|
+
let target = flags.target || (args[1] && !args[1].startsWith('-') ? args[1] : 'all');
|
|
274
|
+
if (typeof target === 'string' && target.includes(',')) {
|
|
275
|
+
target = target.split(',').map(s => s.trim()).filter(Boolean);
|
|
276
|
+
}
|
|
277
|
+
const result = runGate(flags.project, {
|
|
278
|
+
target,
|
|
279
|
+
profile: flags.profile,
|
|
280
|
+
json: flags.json,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (process.env.GITHUB_ACTIONS === 'true' || flags.githubAnnotations) {
|
|
284
|
+
emitGitHubAnnotations(result);
|
|
285
|
+
writeGitHubSummary(result);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (flags.json) {
|
|
289
|
+
console.log(JSON.stringify(result, null, 2));
|
|
290
|
+
} else {
|
|
291
|
+
console.log(`\nContextOS Quality Gate (${result.schemaVersion})\n`);
|
|
292
|
+
console.log(` Project: ${result.projectRoot}`);
|
|
293
|
+
console.log(` Status : ${result.status.toUpperCase()} (code ${result.code})`);
|
|
294
|
+
console.log(` Message: ${result.message}\n`);
|
|
295
|
+
if (result.drift && result.drift.hasDrift) {
|
|
296
|
+
console.log(`! Drift detected (${result.drift.totalFindings} findings):`);
|
|
297
|
+
for (const [state, items] of Object.entries(result.drift.findings)) {
|
|
298
|
+
if (items.length > 0) {
|
|
299
|
+
console.log(` [${state}] (${items.length}):`);
|
|
300
|
+
for (const item of items) {
|
|
301
|
+
console.log(` • ${item.path || item.reason || JSON.stringify(item)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (result.drift.collisions && result.drift.collisions.length > 0) {
|
|
306
|
+
console.log(`\n [PATH_COLLISIONS] (${result.drift.collisions.length}):`);
|
|
307
|
+
for (const c of result.drift.collisions) {
|
|
308
|
+
console.log(` • ${c.path} (between ${c.firstAdapter} and ${c.secondAdapter})`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
process.exit(result.code);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Export check mode (in-process verification without spawning untrusted project scripts)
|
|
317
|
+
if (mainCommand === 'export' && args.includes('--check')) {
|
|
318
|
+
const { detectDrift } = require('../.agents/adapters/drift-detector.js');
|
|
319
|
+
const rawTarget = flags.target || (args[1] && !args[1].startsWith('-') ? args[1] : 'all');
|
|
320
|
+
const drift = detectDrift(flags.project, rawTarget, { profile: flags.profile });
|
|
321
|
+
|
|
322
|
+
if (process.env.GITHUB_ACTIONS === 'true' || flags.githubAnnotations) {
|
|
323
|
+
const { emitGitHubAnnotations, writeGitHubSummary } = require('./lib/gate.js');
|
|
324
|
+
const gateFormat = {
|
|
325
|
+
ok: !drift.hasDrift && !drift.hasError,
|
|
326
|
+
code: drift.code !== undefined ? drift.code : (drift.hasDrift ? 1 : 0),
|
|
327
|
+
status: drift.status || (drift.hasError ? 'error' : (drift.hasDrift ? 'drift' : 'pass')),
|
|
328
|
+
projectRoot: flags.project,
|
|
329
|
+
target: rawTarget,
|
|
330
|
+
profile: flags.profile || 'default',
|
|
331
|
+
drift,
|
|
332
|
+
message: drift.hasDrift ? 'Adapter drift detected' : 'Artifacts synchronized',
|
|
333
|
+
};
|
|
334
|
+
emitGitHubAnnotations(gateFormat);
|
|
335
|
+
writeGitHubSummary(gateFormat);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (flags.json) {
|
|
339
|
+
console.log(JSON.stringify(drift, null, 2));
|
|
340
|
+
} else {
|
|
341
|
+
console.log('\nContextOS - Adapter Output Drift Check\n');
|
|
342
|
+
console.log(` Project: ${flags.project}`);
|
|
343
|
+
if (!drift.hasDrift && !drift.hasError) {
|
|
344
|
+
console.log(`✓ No adapter drift detected. All ${drift.projectedCount} output artifacts are synchronized.`);
|
|
345
|
+
} else {
|
|
346
|
+
console.log(`! Drift detected (${drift.totalFindings} finding(s)):\n`);
|
|
347
|
+
for (const [state, items] of Object.entries(drift.findings)) {
|
|
348
|
+
if (items.length > 0) {
|
|
349
|
+
console.log(` [${state}] (${items.length}):`);
|
|
350
|
+
for (const item of items) {
|
|
351
|
+
console.log(` • ${item.path || item.reason || JSON.stringify(item)}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (drift.collisions && drift.collisions.length > 0) {
|
|
356
|
+
console.log(`\n [PATH_COLLISIONS] (${drift.collisions.length}):`);
|
|
357
|
+
for (const c of drift.collisions) {
|
|
358
|
+
console.log(` • ${c.path} (between ${c.firstAdapter} and ${c.secondAdapter})`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
console.log('\nRun: contextos export all to synchronize outputs with source skills.\n');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
process.exit(drift.code !== undefined ? drift.code : (drift.hasDrift ? 1 : 0));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Scan command (Git staged index security and placeholder scanner)
|
|
368
|
+
if (mainCommand === 'scan') {
|
|
369
|
+
const scanModule = require('./commands/scan.js');
|
|
370
|
+
scanModule(args, flags);
|
|
371
|
+
process.exit(0);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Hook command (safe Git hook lifecycle management)
|
|
375
|
+
if (mainCommand === 'hook') {
|
|
376
|
+
const { hookCommand } = require('./commands/hook.js');
|
|
377
|
+
hookCommand(args, flags);
|
|
378
|
+
process.exit(0);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Proxy commands to trusted package .agents/ctx.js targeting flags.project
|
|
254
382
|
const PROXY_COMMANDS = [
|
|
255
|
-
'profile', 'export', 'validate', 'resolve', 'skill', 'index',
|
|
383
|
+
'profile', 'export', 'validate', 'audit', 'resolve', 'skill', 'index',
|
|
256
384
|
'clean-worktrees', 'stats', 'watch', 'compile', 'explain',
|
|
257
385
|
'thread'
|
|
258
386
|
];
|
|
259
387
|
|
|
260
|
-
const
|
|
261
|
-
const hasLocalCtx = fs.existsSync(ctxPath);
|
|
388
|
+
const packageCtxPath = path.join(__dirname, '..', '.agents', 'ctx.js');
|
|
262
389
|
|
|
263
|
-
if (mainCommand && PROXY_COMMANDS.includes(mainCommand)
|
|
390
|
+
if (mainCommand && PROXY_COMMANDS.includes(mainCommand)) {
|
|
264
391
|
const { execFileSync } = require('child_process');
|
|
392
|
+
const filteredArgs = [];
|
|
393
|
+
for (let i = 0; i < args.length; i++) {
|
|
394
|
+
if (args[i] === '--project') {
|
|
395
|
+
i++; // skip project path
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
filteredArgs.push(args[i]);
|
|
399
|
+
}
|
|
265
400
|
try {
|
|
266
|
-
execFileSync(process.execPath, [
|
|
401
|
+
execFileSync(process.execPath, [packageCtxPath, ...filteredArgs], {
|
|
402
|
+
cwd: flags.project,
|
|
403
|
+
stdio: 'inherit',
|
|
404
|
+
});
|
|
267
405
|
} catch (e) {
|
|
268
406
|
process.exit(e.status || 1);
|
|
269
407
|
}
|
|
@@ -300,34 +438,7 @@ if (mainCommand === 'watch') {
|
|
|
300
438
|
watchModule.runWatch(process.cwd());
|
|
301
439
|
}
|
|
302
440
|
|
|
303
|
-
const PROJECT_ONLY_COMMANDS = ['profile', 'export', 'validate', 'resolve', 'skill', 'index', 'clean-worktrees', 'compile', 'explain'];
|
|
304
|
-
if (mainCommand && PROJECT_ONLY_COMMANDS.includes(mainCommand) && !hasLocalCtx) {
|
|
305
|
-
console.error('[ERROR] .agents/ctx.js not found in current directory.');
|
|
306
|
-
console.error(' Are you in a ContextOS project? Run `contextos` or `npx contextos-agents` first.');
|
|
307
|
-
process.exit(1);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (mainCommand === 'audit') {
|
|
311
|
-
if (!hasLocalCtx) {
|
|
312
|
-
console.error('[ERROR] .agents/ctx.js not found. Are you in a ContextOS project?');
|
|
313
|
-
process.exit(1);
|
|
314
|
-
}
|
|
315
|
-
const { execFileSync } = require('child_process');
|
|
316
|
-
try {
|
|
317
|
-
execFileSync(process.execPath, [ctxPath, 'validate'], { stdio: 'inherit' });
|
|
318
|
-
} catch (e) {
|
|
319
|
-
process.exit(1);
|
|
320
|
-
}
|
|
321
|
-
process.exit(0);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
441
|
if (mainCommand === 'install-skill') {
|
|
325
|
-
const ctxPath = path.join(process.cwd(), '.agents', 'ctx.js');
|
|
326
|
-
if (!fs.existsSync(ctxPath)) {
|
|
327
|
-
console.error('[ERROR] .agents/ctx.js not found. Are you in a ContextOS project?');
|
|
328
|
-
process.exit(1);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
442
|
const fromRepo = (() => {
|
|
332
443
|
const i = args.indexOf('--from-repo');
|
|
333
444
|
return i !== -1 ? args[i + 1] : null;
|
|
@@ -338,7 +449,10 @@ if (mainCommand === 'install-skill') {
|
|
|
338
449
|
if (ref) {
|
|
339
450
|
const { execFileSync } = require('child_process');
|
|
340
451
|
try {
|
|
341
|
-
execFileSync(process.execPath, [
|
|
452
|
+
execFileSync(process.execPath, [packageCtxPath, 'skill', 'add', ref], {
|
|
453
|
+
cwd: flags.project,
|
|
454
|
+
stdio: 'inherit',
|
|
455
|
+
});
|
|
342
456
|
} catch (e) {
|
|
343
457
|
process.exit(1);
|
|
344
458
|
}
|
|
@@ -434,6 +548,9 @@ if (mainCommand === 'install-skill') {
|
|
|
434
548
|
console.error(' To use the MCP Bridge or execution runtime, install @contextos/mcp:');
|
|
435
549
|
console.error(' npm install @contextos/mcp --save-dev');
|
|
436
550
|
process.exit(1);
|
|
551
|
+
} else if (mainCommand && mainCommand !== 'init') {
|
|
552
|
+
console.error(`[ERROR] Unknown command: '${mainCommand}'. Run 'contextos --help' for available commands.`);
|
|
553
|
+
process.exit(1);
|
|
437
554
|
} else {
|
|
438
555
|
|
|
439
556
|
// ── Paths ─────────────────────────────────────────────────────────────────────
|
package/bin/lib/gate.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/lib/gate.js
|
|
3
|
+
* ContextOS - Quality Gate Verification Engine
|
|
4
|
+
*
|
|
5
|
+
* Implements deterministic verification paths without executing untrusted
|
|
6
|
+
* user project scripts. Operates in-process using trusted package modules.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { detectDrift } = require('../../.agents/adapters/drift-detector.js');
|
|
14
|
+
const pureCompiler = require('../../.agents/adapters/pure-compiler.js');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs the quality gate against a target project root.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} [projectRoot=process.cwd()] - Target project directory
|
|
20
|
+
* @param {object} [options]
|
|
21
|
+
* @param {string|string[]} [options.target='all'] - Adapters to verify
|
|
22
|
+
* @param {string} [options.profile=null] - Profile name or override
|
|
23
|
+
* @param {boolean} [options.json=false] - Format as machine-readable JSON
|
|
24
|
+
* @returns {{ ok: boolean, code: number, status: string, drift: object, message: string }}
|
|
25
|
+
*/
|
|
26
|
+
function runGate(projectRoot = process.cwd(), options = {}) {
|
|
27
|
+
const absRoot = path.resolve(projectRoot);
|
|
28
|
+
const target = options.target || 'all';
|
|
29
|
+
const profile = options.profile || null;
|
|
30
|
+
|
|
31
|
+
// 1. Run Drift Detection via trusted in-package drift-detector
|
|
32
|
+
const drift = detectDrift(absRoot, target, { profile });
|
|
33
|
+
|
|
34
|
+
let status = 'pass';
|
|
35
|
+
let code = 0;
|
|
36
|
+
let message = 'All adapter outputs are synchronized with source skills.';
|
|
37
|
+
|
|
38
|
+
if (drift.hasError) {
|
|
39
|
+
status = 'error';
|
|
40
|
+
code = drift.code || 2;
|
|
41
|
+
message = drift.error || 'Configuration or execution error occurred during verification.';
|
|
42
|
+
} else if (drift.hasDrift) {
|
|
43
|
+
status = 'drift';
|
|
44
|
+
code = drift.code || 1;
|
|
45
|
+
message = `Adapter drift detected across ${drift.totalFindings} finding(s).`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = {
|
|
49
|
+
schemaVersion: '2.0.0',
|
|
50
|
+
status,
|
|
51
|
+
code,
|
|
52
|
+
ok: code === 0,
|
|
53
|
+
projectRoot: absRoot,
|
|
54
|
+
target,
|
|
55
|
+
profile,
|
|
56
|
+
message,
|
|
57
|
+
drift,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Emits GitHub Actions workflow annotations for drift or errors.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} result - Gate result
|
|
67
|
+
*/
|
|
68
|
+
function emitGitHubAnnotations(result) {
|
|
69
|
+
if (!result || !result.drift) return;
|
|
70
|
+
|
|
71
|
+
const escapeActionData = (str) =>
|
|
72
|
+
String(str)
|
|
73
|
+
.replace(/%/g, '%25')
|
|
74
|
+
.replace(/\r/g, '%0D')
|
|
75
|
+
.replace(/\n/g, '%0A');
|
|
76
|
+
|
|
77
|
+
const normalizePath = (p) => (p ? String(p).replace(/\\/g, '/') : '');
|
|
78
|
+
|
|
79
|
+
const { findings, collisions } = result.drift;
|
|
80
|
+
|
|
81
|
+
if (findings) {
|
|
82
|
+
for (const item of findings.MISSING_OUTPUT || []) {
|
|
83
|
+
console.error(`::error file=${normalizePath(item.path)}::[MISSING_OUTPUT] ${escapeActionData(item.reason || 'Missing projected output file')}`);
|
|
84
|
+
}
|
|
85
|
+
for (const item of findings.MODIFIED_MANAGED_OUTPUT || []) {
|
|
86
|
+
console.error(`::error file=${normalizePath(item.path)}::[MODIFIED_OUTPUT] ${escapeActionData(item.reason || 'Managed output modified on disk')}`);
|
|
87
|
+
}
|
|
88
|
+
for (const item of findings.ORPHAN_MANAGED_OUTPUT || []) {
|
|
89
|
+
console.error(`::warning file=${normalizePath(item.path)}::[ORPHAN_OUTPUT] ${escapeActionData(item.reason || 'Orphan file recorded in lockfile')}`);
|
|
90
|
+
}
|
|
91
|
+
for (const item of findings.CORRUPT_LOCKFILE || []) {
|
|
92
|
+
console.error(`::error::[CORRUPT_LOCKFILE] ${escapeActionData(item.reason || 'Lockfile is corrupt')}`);
|
|
93
|
+
}
|
|
94
|
+
for (const item of findings.CONFIG_ERROR || []) {
|
|
95
|
+
console.error(`::error::[CONFIG_ERROR] ${escapeActionData(item.reason || 'Configuration error')}`);
|
|
96
|
+
}
|
|
97
|
+
for (const item of findings.EMPTY_PROJECTION || []) {
|
|
98
|
+
console.error(`::error::[EMPTY_PROJECTION] ${escapeActionData(item.reason || 'Projected artifact set is empty')}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const c of collisions || []) {
|
|
103
|
+
console.error(`::error file=${normalizePath(c.path)}::[PATH_COLLISION] Collision between adapter ${c.firstAdapter} and ${c.secondAdapter}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Formats a markdown summary for GitHub Step Summary.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} result - Gate result
|
|
111
|
+
* @returns {string} Markdown summary
|
|
112
|
+
*/
|
|
113
|
+
function formatGitHubSummary(result) {
|
|
114
|
+
const icon = result.ok ? '✅' : (result.status === 'drift' ? '⚠️' : '❌');
|
|
115
|
+
const lines = [
|
|
116
|
+
`# ${icon} ContextOS Quality Gate Report`,
|
|
117
|
+
'',
|
|
118
|
+
`**Status**: \`${result.status.toUpperCase()}\` (exit code: \`${result.code}\`) `,
|
|
119
|
+
`**Project Root**: \`${result.projectRoot}\` `,
|
|
120
|
+
`**Target Adapters**: \`${Array.isArray(result.target) ? result.target.join(', ') : result.target}\` `,
|
|
121
|
+
`**Active Profile**: \`${result.profile || 'default'}\` `,
|
|
122
|
+
`**Projected Artifacts**: \`${result.drift?.projectedCount || 0}\` `,
|
|
123
|
+
'',
|
|
124
|
+
`### Overview`,
|
|
125
|
+
result.message,
|
|
126
|
+
'',
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
if (result.drift && result.drift.totalFindings > 0) {
|
|
130
|
+
lines.push(`### Findings (${result.drift.totalFindings})`);
|
|
131
|
+
lines.push('| State | Path | Detail |');
|
|
132
|
+
lines.push('| --- | --- | --- |');
|
|
133
|
+
|
|
134
|
+
for (const [state, items] of Object.entries(result.drift.findings)) {
|
|
135
|
+
for (const item of items) {
|
|
136
|
+
const itemPath = item.path ? `\`${item.path}\`` : '*(project)*';
|
|
137
|
+
const detail = (item.reason || item.diff || '').replace(/\|/g, '\\|');
|
|
138
|
+
lines.push(`| **${state}** | ${itemPath} | ${detail} |`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
lines.push('');
|
|
142
|
+
lines.push('> **Remediation**: Run `npx contextos-agents export all` and commit synchronized adapter artifacts.');
|
|
143
|
+
lines.push('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Writes the markdown summary to GITHUB_STEP_SUMMARY if available.
|
|
151
|
+
*
|
|
152
|
+
* @param {object} result - Gate result
|
|
153
|
+
*/
|
|
154
|
+
function writeGitHubSummary(result) {
|
|
155
|
+
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
156
|
+
if (!summaryFile) return;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const summaryMd = formatGitHubSummary(result);
|
|
160
|
+
fs.appendFileSync(summaryFile, summaryMd + '\n', 'utf8');
|
|
161
|
+
} catch {
|
|
162
|
+
// Best-effort write to step summary
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
runGate,
|
|
168
|
+
emitGitHubAnnotations,
|
|
169
|
+
formatGitHubSummary,
|
|
170
|
+
writeGitHubSummary,
|
|
171
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/lib/git-snapshot.js
|
|
3
|
+
* ContextOS Git Index Snapshot & Staged Blob Reader
|
|
4
|
+
*
|
|
5
|
+
* Inspects staged changes directly from the Git index without modifying
|
|
6
|
+
* the working directory. Safely handles NUL-delimited filenames, Unicode,
|
|
7
|
+
* binary files, and path spaces.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { execFileSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Finds the top-level repository root via Git.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} [cwd=process.cwd()] - Working directory
|
|
19
|
+
* @returns {string|null} Absolute repository root or null if not a Git repository
|
|
20
|
+
*/
|
|
21
|
+
function findGitRoot(cwd = process.cwd()) {
|
|
22
|
+
try {
|
|
23
|
+
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
});
|
|
28
|
+
return path.resolve(root.trim());
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Discovers the git hooks directory, respecting core.hooksPath if set.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} [cwd=process.cwd()] - Working directory
|
|
38
|
+
* @returns {string|null} Absolute path to hooks directory or null
|
|
39
|
+
*/
|
|
40
|
+
function findHooksDir(cwd = process.cwd()) {
|
|
41
|
+
try {
|
|
42
|
+
const gitDir = execFileSync('git', ['rev-parse', '--git-path', 'hooks'], {
|
|
43
|
+
cwd,
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
46
|
+
}).trim();
|
|
47
|
+
return path.resolve(cwd, gitDir);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Retrieves the list of staged files from the Git index.
|
|
55
|
+
* Uses NUL-delimited output (-z) to support spaces, Unicode, and escapes.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} [cwd=process.cwd()] - Working directory
|
|
58
|
+
* @returns {Array<{ status: string, path: string, oldPath?: string }>} Staged entries
|
|
59
|
+
*/
|
|
60
|
+
function getStagedFiles(cwd = process.cwd()) {
|
|
61
|
+
try {
|
|
62
|
+
const output = execFileSync(
|
|
63
|
+
'git',
|
|
64
|
+
['-c', 'core.quotepath=false', 'diff', '--cached', '-z', '--name-status', '--no-ext-diff'],
|
|
65
|
+
{ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!output) return [];
|
|
69
|
+
|
|
70
|
+
const parts = output.split('\0');
|
|
71
|
+
const entries = [];
|
|
72
|
+
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < parts.length) {
|
|
75
|
+
const statusRaw = parts[i];
|
|
76
|
+
if (!statusRaw) {
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const status = statusRaw[0];
|
|
82
|
+
if (status === 'R' || status === 'C') {
|
|
83
|
+
// Rename or copy includes old path followed by new path
|
|
84
|
+
const oldPath = parts[i + 1];
|
|
85
|
+
const newPath = parts[i + 2];
|
|
86
|
+
if (newPath) {
|
|
87
|
+
entries.push({ status, path: newPath, oldPath });
|
|
88
|
+
}
|
|
89
|
+
i += 3;
|
|
90
|
+
} else {
|
|
91
|
+
const filePath = parts[i + 1];
|
|
92
|
+
if (filePath) {
|
|
93
|
+
entries.push({ status, path: filePath });
|
|
94
|
+
}
|
|
95
|
+
i += 2;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return entries;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw new Error(`Failed to query staged files from Git: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reads the content of a file directly from the Git staged index.
|
|
107
|
+
* Fails closed with an error if the staged blob cannot be read.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} relativePath - Path relative to repo root
|
|
110
|
+
* @param {string} [cwd=process.cwd()] - Working directory
|
|
111
|
+
* @returns {Buffer} Staged file buffer
|
|
112
|
+
*/
|
|
113
|
+
function getStagedBlob(relativePath, cwd = process.cwd()) {
|
|
114
|
+
const posixPath = relativePath.replace(/\\/g, '/');
|
|
115
|
+
try {
|
|
116
|
+
const buf = execFileSync(
|
|
117
|
+
'git',
|
|
118
|
+
['--literal-pathspecs', 'show', `:${posixPath}`],
|
|
119
|
+
{
|
|
120
|
+
cwd,
|
|
121
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
122
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
return buf;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const errMsg = err.stderr ? err.stderr.toString('utf8').trim() : err.message;
|
|
128
|
+
throw new Error(`Failed to read staged blob for "${posixPath}": ${errMsg}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extracts added lines from git diff --cached -U0 for each staged file.
|
|
134
|
+
* Returns only lines that were added (+), excluding diff headers and deleted lines (-).
|
|
135
|
+
* Uses --literal-pathspecs, --no-ext-diff, and --no-textconv for strict path isolation.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} [cwd=process.cwd()] - Working directory
|
|
138
|
+
* @param {string[]} [files=null] - Optional list of staged file paths. If omitted, discovered via getStagedFiles.
|
|
139
|
+
* @returns {Map<string, Array<{ line: number, content: string }>>} Added lines by file
|
|
140
|
+
*/
|
|
141
|
+
function getStagedAddedLines(cwd = process.cwd(), files = null) {
|
|
142
|
+
const addedLinesByFile = new Map();
|
|
143
|
+
|
|
144
|
+
let targetFiles = files;
|
|
145
|
+
if (!targetFiles) {
|
|
146
|
+
const staged = getStagedFiles(cwd);
|
|
147
|
+
targetFiles = staged.filter(e => e.status !== 'D').map(e => e.path);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const filePath of targetFiles) {
|
|
151
|
+
try {
|
|
152
|
+
const posixPath = filePath.replace(/\\/g, '/');
|
|
153
|
+
const diffOutput = execFileSync(
|
|
154
|
+
'git',
|
|
155
|
+
[
|
|
156
|
+
'--literal-pathspecs',
|
|
157
|
+
'-c', 'core.quotepath=false',
|
|
158
|
+
'diff',
|
|
159
|
+
'--cached',
|
|
160
|
+
'-U0',
|
|
161
|
+
'--no-ext-diff',
|
|
162
|
+
'--no-textconv',
|
|
163
|
+
'--',
|
|
164
|
+
posixPath,
|
|
165
|
+
],
|
|
166
|
+
{
|
|
167
|
+
cwd,
|
|
168
|
+
encoding: 'utf8',
|
|
169
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
170
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (!diffOutput) continue;
|
|
175
|
+
|
|
176
|
+
const lines = diffOutput.split(/\r?\n/);
|
|
177
|
+
let currentLineNum = 0;
|
|
178
|
+
const fileAddedLines = [];
|
|
179
|
+
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
if (line.startsWith('@@ ')) {
|
|
182
|
+
// Parse @@ -a,b +c,d @@ to get current target line number
|
|
183
|
+
const match = line.match(/\+([0-9]+)(?:,([0-9]+))?/);
|
|
184
|
+
if (match) {
|
|
185
|
+
currentLineNum = parseInt(match[1], 10);
|
|
186
|
+
}
|
|
187
|
+
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
188
|
+
const content = line.slice(1);
|
|
189
|
+
fileAddedLines.push({
|
|
190
|
+
line: currentLineNum,
|
|
191
|
+
content,
|
|
192
|
+
});
|
|
193
|
+
currentLineNum++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (fileAddedLines.length > 0) {
|
|
198
|
+
addedLinesByFile.set(filePath, fileAddedLines);
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
throw new Error(`Failed to extract staged diff for "${filePath}": ${err.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return addedLinesByFile;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
findGitRoot,
|
|
210
|
+
findHooksDir,
|
|
211
|
+
getStagedFiles,
|
|
212
|
+
getStagedBlob,
|
|
213
|
+
getStagedAddedLines,
|
|
214
|
+
};
|