proof-of-commitment 1.21.1 → 1.22.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/index.js +219 -84
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* proof-of-commitment CLI v1.
|
|
3
|
+
* proof-of-commitment CLI v1.22.1
|
|
4
4
|
* Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
|
|
5
5
|
* Usage: npx proof-of-commitment [packages...] [options]
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const API = 'https://poc-backend.amdal-dev.workers.dev/api/audit';
|
|
8
|
+
const API = process.env.COMMIT_API_URL || 'https://poc-backend.amdal-dev.workers.dev/api/audit';
|
|
9
9
|
const KEYS_API = 'https://poc-backend.amdal-dev.workers.dev/api/keys';
|
|
10
10
|
const WATCHLIST_API = 'https://poc-backend.amdal-dev.workers.dev/api/watchlist';
|
|
11
11
|
const WEB = 'https://getcommit.dev/audit';
|
|
@@ -272,9 +272,11 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
272
272
|
|
|
273
273
|
let criticalInDisplay = 0;
|
|
274
274
|
let provenanceCount = 0;
|
|
275
|
+
let compromisedCount = 0;
|
|
275
276
|
|
|
276
277
|
for (const pkg of results) {
|
|
277
278
|
const rc = riskColor(pkg.riskFlags, pkg.score);
|
|
279
|
+
if (pkg.compromised) compromisedCount++;
|
|
278
280
|
const label = riskLabel(pkg.riskFlags, pkg.score);
|
|
279
281
|
if (hasCritical(pkg.riskFlags)) criticalInDisplay++;
|
|
280
282
|
if (pkg.hasProvenance) provenanceCount++;
|
|
@@ -310,6 +312,12 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
310
312
|
console.log(clr(c.dim, ` ↳ ${ghCount} GitHub contributors — publish-access concentration risk despite active community`));
|
|
311
313
|
}
|
|
312
314
|
|
|
315
|
+
// Recently compromised warning
|
|
316
|
+
if (pkg.compromised) {
|
|
317
|
+
const atk = pkg.compromised;
|
|
318
|
+
console.log(clr(c.red, ` ⚠ COMPROMISED — ${atk.attack} (${atk.date}) — ${atk.url}`));
|
|
319
|
+
}
|
|
320
|
+
|
|
313
321
|
// Score breakdown if available
|
|
314
322
|
if (pkg.scoreBreakdown) {
|
|
315
323
|
const b = pkg.scoreBreakdown;
|
|
@@ -342,6 +350,11 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
342
350
|
console.log('\n' + clr(c.green, `✓ No CRITICAL packages found${suffix}.`));
|
|
343
351
|
}
|
|
344
352
|
|
|
353
|
+
if (compromisedCount > 0) {
|
|
354
|
+
console.log(clr(c.red + c.bold, `\n⚠ ${compromisedCount} package${compromisedCount > 1 ? 's' : ''} recently compromised in supply chain attacks.`));
|
|
355
|
+
console.log(clr(c.dim, ' Verify you are on clean versions. See URLs above for incident details.'));
|
|
356
|
+
}
|
|
357
|
+
|
|
345
358
|
// Footer with web link + CI integration CTA
|
|
346
359
|
const topPkgs = results.slice(0, 10).map(r => r.name).join(',');
|
|
347
360
|
const utm = 'utm_source=cli&utm_medium=audit';
|
|
@@ -506,9 +519,11 @@ ${clr(c.bold, 'Reports:')}
|
|
|
506
519
|
Saves audit-report.html to cwd + prints Markdown for GitHub issues
|
|
507
520
|
|
|
508
521
|
${clr(c.bold, 'IDE Hooks:')}
|
|
509
|
-
poc hook Install Cursor
|
|
510
|
-
poc hook --
|
|
511
|
-
poc hook --
|
|
522
|
+
poc hook Install supply chain gate for Cursor + Claude Code (blocks CRITICAL packages)
|
|
523
|
+
poc hook --cursor Install only the Cursor beforeShellExecution hook
|
|
524
|
+
poc hook --claude-code Install only the Claude Code PreToolUse hook
|
|
525
|
+
poc hook --global Install for the current user (~/.cursor + ~/.claude)
|
|
526
|
+
poc hook --uninstall Remove the hook from both Cursor and Claude Code
|
|
512
527
|
|
|
513
528
|
${clr(c.bold, 'Account:')}
|
|
514
529
|
poc login [key] Save and validate your API key (interactive or direct)
|
|
@@ -1132,10 +1147,18 @@ async function cmdLogout() {
|
|
|
1132
1147
|
}
|
|
1133
1148
|
|
|
1134
1149
|
/**
|
|
1135
|
-
* poc hook [--cursor] [--global] [--uninstall]
|
|
1136
|
-
* Install a
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1150
|
+
* poc hook [--cursor] [--claude-code] [--global] [--uninstall]
|
|
1151
|
+
* Install a supply chain gate hook for Cursor (beforeShellExecution) and/or
|
|
1152
|
+
* Claude Code (PreToolUse) that scores packages before install.
|
|
1153
|
+
*
|
|
1154
|
+
* Writes a single hook script to ~/.commit/cursor-hook.js (the filename is
|
|
1155
|
+
* kept for backward compatibility with v1.21.x installs; the same script
|
|
1156
|
+
* now auto-detects whether stdin is in Cursor or Claude Code format and
|
|
1157
|
+
* emits the matching response shape).
|
|
1158
|
+
*
|
|
1159
|
+
* Default installs both Cursor + Claude Code configs. Pass --cursor or
|
|
1160
|
+
* --claude-code to install only one. --global writes to ~/.cursor and
|
|
1161
|
+
* ~/.claude; default writes to ./.cursor and ./.claude.
|
|
1139
1162
|
*/
|
|
1140
1163
|
async function cmdHook(args) {
|
|
1141
1164
|
const os = await import('os');
|
|
@@ -1144,18 +1167,27 @@ async function cmdHook(args) {
|
|
|
1144
1167
|
|
|
1145
1168
|
const isGlobal = args.includes('--global') || args.includes('-g');
|
|
1146
1169
|
const uninstall = args.includes('--uninstall') || args.includes('--remove');
|
|
1170
|
+
const onlyCursor = args.includes('--cursor');
|
|
1171
|
+
const onlyClaude = args.includes('--claude-code') || args.includes('--claude');
|
|
1172
|
+
// Default (no client flag) = install both. --cursor and --claude-code narrow scope.
|
|
1173
|
+
const installCursor = !onlyClaude;
|
|
1174
|
+
const installClaude = !onlyCursor;
|
|
1147
1175
|
|
|
1148
1176
|
// ── Hook script (plain Node.js, no external deps) ─────────────────────
|
|
1177
|
+
// Single script serves BOTH Cursor (beforeShellExecution) and Claude Code
|
|
1178
|
+
// (PreToolUse). It auto-detects which client called it by inspecting the
|
|
1179
|
+
// stdin JSON and emits the matching response format.
|
|
1149
1180
|
const hookScript = `#!/usr/bin/env node
|
|
1150
1181
|
/**
|
|
1151
|
-
* Commit supply chain hook for Cursor (auto-generated by \`poc hook\`)
|
|
1182
|
+
* Commit supply chain hook for Cursor + Claude Code (auto-generated by \`poc hook\`)
|
|
1152
1183
|
* Intercepts npm/pip/cargo/go install commands and scores packages
|
|
1153
1184
|
* against getcommit.dev before they run.
|
|
1154
1185
|
*
|
|
1155
1186
|
* CRITICAL packages are blocked. HIGH packages trigger confirmation.
|
|
1187
|
+
* Auto-detects Cursor vs Claude Code stdin format and replies in kind.
|
|
1156
1188
|
* Docs: https://getcommit.dev/docs/cursor-hook
|
|
1157
1189
|
*/
|
|
1158
|
-
const API = 'https://poc-backend.amdal-dev.workers.dev/api/audit';
|
|
1190
|
+
const API = process.env.COMMIT_API_URL || 'https://poc-backend.amdal-dev.workers.dev/api/audit';
|
|
1159
1191
|
const fs = require('fs');
|
|
1160
1192
|
const path = require('path');
|
|
1161
1193
|
|
|
@@ -1169,7 +1201,7 @@ function readKey() {
|
|
|
1169
1201
|
}
|
|
1170
1202
|
|
|
1171
1203
|
function parseInstall(cmd) {
|
|
1172
|
-
const t = cmd.trim();
|
|
1204
|
+
const t = (cmd || '').trim();
|
|
1173
1205
|
let m;
|
|
1174
1206
|
// npm / pnpm / yarn
|
|
1175
1207
|
m = t.match(/^(?:npm\\s+(?:i|install|add)|pnpm\\s+(?:i|install|add)|yarn\\s+add)\\s+(.+)/);
|
|
@@ -1186,11 +1218,51 @@ function parseInstall(cmd) {
|
|
|
1186
1218
|
return null;
|
|
1187
1219
|
}
|
|
1188
1220
|
|
|
1221
|
+
// Detect which client called us and how to extract the command.
|
|
1222
|
+
// Cursor: stdin = { command: 'npm install ...', workingDirectory? }
|
|
1223
|
+
// Claude Code: stdin = { tool_name: 'Bash', tool_input: { command: '...' }, hook_event_name: 'PreToolUse', ... }
|
|
1224
|
+
function detectClient(input) {
|
|
1225
|
+
if (input && input.tool_input && typeof input.tool_input.command === 'string') {
|
|
1226
|
+
return { client: 'claude-code', cmd: input.tool_input.command };
|
|
1227
|
+
}
|
|
1228
|
+
if (input && typeof input.command === 'string') {
|
|
1229
|
+
return { client: 'cursor', cmd: input.command };
|
|
1230
|
+
}
|
|
1231
|
+
return { client: 'cursor', cmd: '' };
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Emit the appropriate "no decision" / "allow" output for the detected client.
|
|
1235
|
+
function emitAllow(client) {
|
|
1236
|
+
if (client === 'claude-code') {
|
|
1237
|
+
// No stdout + exit 0 = defer to normal permission flow.
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
process.stdout.write(JSON.stringify({ permission: 'allow' }));
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Emit deny / ask in the matching format.
|
|
1244
|
+
function emit(client, decision, userMsg, agentMsg) {
|
|
1245
|
+
if (client === 'claude-code') {
|
|
1246
|
+
process.stdout.write(JSON.stringify({
|
|
1247
|
+
hookSpecificOutput: {
|
|
1248
|
+
hookEventName: 'PreToolUse',
|
|
1249
|
+
permissionDecision: decision,
|
|
1250
|
+
permissionDecisionReason: userMsg,
|
|
1251
|
+
},
|
|
1252
|
+
}));
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const body = { permission: decision, user_message: userMsg };
|
|
1256
|
+
if (agentMsg) body.agent_message = agentMsg;
|
|
1257
|
+
process.stdout.write(JSON.stringify(body));
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1189
1260
|
async function main() {
|
|
1190
1261
|
let input;
|
|
1191
|
-
try { input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf-8')); } catch {
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1262
|
+
try { input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf-8')); } catch { emitAllow('cursor'); return; }
|
|
1263
|
+
const { client, cmd } = detectClient(input);
|
|
1264
|
+
const parsed = parseInstall(cmd);
|
|
1265
|
+
if (!parsed || parsed.pkgs.length === 0) { emitAllow(client); return; }
|
|
1194
1266
|
|
|
1195
1267
|
const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
|
|
1196
1268
|
const key = readKey();
|
|
@@ -1201,7 +1273,7 @@ async function main() {
|
|
|
1201
1273
|
const timer = setTimeout(() => ctrl.abort(), 4000);
|
|
1202
1274
|
const res = await fetch(API, { method: 'POST', headers, body: JSON.stringify({ packages: parsed.pkgs, ecosystem: parsed.eco }), signal: ctrl.signal });
|
|
1203
1275
|
clearTimeout(timer);
|
|
1204
|
-
if (!res.ok && res.status !== 429) {
|
|
1276
|
+
if (!res.ok && res.status !== 429) { emitAllow(client); return; }
|
|
1205
1277
|
const data = await res.json();
|
|
1206
1278
|
const results = data.results || data.packages_already_scored || [];
|
|
1207
1279
|
|
|
@@ -1209,12 +1281,12 @@ async function main() {
|
|
|
1209
1281
|
const high = results.filter(r => (r.riskFlags || []).some(f => f.startsWith('HIGH')));
|
|
1210
1282
|
const url = 'https://getcommit.dev/audit?packages=' + parsed.pkgs.join(',') + '&ecosystem=' + parsed.eco;
|
|
1211
1283
|
|
|
1212
|
-
//
|
|
1213
|
-
// Without this, hook silently
|
|
1214
|
-
// and the conversion driver (signup URL in 429 body) never reached the user.
|
|
1284
|
+
// Detect rate-limit hit and surface signup CTA + unscored-package warning.
|
|
1285
|
+
// Without this, hook would silently allow unscored packages on 429 (false sense of security).
|
|
1215
1286
|
const rateLimited = res.status === 429;
|
|
1216
|
-
//
|
|
1217
|
-
const
|
|
1287
|
+
// Per-client attribution so /api/keys/create source counters split traffic cleanly.
|
|
1288
|
+
const refTag = client === 'claude-code' ? 'claude-code-hook-429' : 'cursor-hook-429';
|
|
1289
|
+
const rlUrl = rateLimited ? 'https://getcommit.dev/get-started?ref=' + refTag + '&utm_source=cli' : '';
|
|
1218
1290
|
const unscored = rateLimited ? Math.max(0, parsed.pkgs.length - results.length) : 0;
|
|
1219
1291
|
const rlNote = rateLimited
|
|
1220
1292
|
? '\\n\\n\\u26A0 Commit free limit reached'
|
|
@@ -1224,19 +1296,17 @@ async function main() {
|
|
|
1224
1296
|
|
|
1225
1297
|
if (critical.length > 0) {
|
|
1226
1298
|
const lines = critical.map(r => ' \\u{1F534} ' + r.name + ' (score ' + (r.score||'?') + ') \\u2014 ' + (r.riskFlags||[]).slice(0,1).join(', '));
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
}));
|
|
1299
|
+
emit(client, 'deny',
|
|
1300
|
+
'\\u{1F534} Commit blocked: ' + critical.map(r=>r.name).join(', ') + ' flagged CRITICAL\\n\\n' + lines.join('\\n') + '\\n\\n\\u2192 ' + url + rlNote,
|
|
1301
|
+
'Package install blocked by Commit. CRITICAL = sole publisher + high downloads (attack surface of axios/node-ipc incidents). ' + critical.map(r=>r.name).join(', ') + '. Report: ' + url
|
|
1302
|
+
);
|
|
1232
1303
|
return;
|
|
1233
1304
|
}
|
|
1234
1305
|
if (high.length > 0) {
|
|
1235
1306
|
const lines = high.map(r => ' \\u{1F7E1} ' + r.name + ' (score ' + (r.score||'?') + ') \\u2014 ' + (r.riskFlags||[]).slice(0,1).join(', '));
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
}));
|
|
1307
|
+
emit(client, 'ask',
|
|
1308
|
+
'\\u{1F7E1} Commit: ' + high.map(r=>r.name).join(', ') + ' scored HIGH risk\\n\\n' + lines.join('\\n') + '\\n\\nProceed? \\u2192 ' + url + rlNote
|
|
1309
|
+
);
|
|
1240
1310
|
return;
|
|
1241
1311
|
}
|
|
1242
1312
|
// Rate-limited with no critical/high in the scored partial: still alert user.
|
|
@@ -1246,14 +1316,11 @@ async function main() {
|
|
|
1246
1316
|
const head = unscored > 0
|
|
1247
1317
|
? '\\u26A0 Commit free limit reached \\u2014 ' + unscored + ' of ' + parsed.pkgs.length + ' package(s) NOT audited'
|
|
1248
1318
|
: '\\u2713 ' + parsed.pkgs.join(', ') + ' look clean (free-tier audit)';
|
|
1249
|
-
|
|
1250
|
-
permission: 'ask',
|
|
1251
|
-
user_message: head + '\\n\\nFree API key (200/day, no card, 30s):\\n ' + rlUrl + '\\n\\nProceed anyway?',
|
|
1252
|
-
}));
|
|
1319
|
+
emit(client, 'ask', head + '\\n\\nFree API key (200/day, no card, 30s):\\n ' + rlUrl + '\\n\\nProceed anyway?');
|
|
1253
1320
|
return;
|
|
1254
1321
|
}
|
|
1255
|
-
|
|
1256
|
-
} catch {
|
|
1322
|
+
emitAllow(client);
|
|
1323
|
+
} catch { emitAllow(client); }
|
|
1257
1324
|
}
|
|
1258
1325
|
main();
|
|
1259
1326
|
`;
|
|
@@ -1261,35 +1328,119 @@ main();
|
|
|
1261
1328
|
const commitDir = path.join(os.homedir(), '.commit');
|
|
1262
1329
|
const hookPath = path.join(commitDir, 'cursor-hook.js');
|
|
1263
1330
|
|
|
1331
|
+
// ── Cursor config helpers ─────────────────────────────────────────────
|
|
1332
|
+
function cursorHooksFile(global) {
|
|
1333
|
+
const dir = global ? path.join(os.homedir(), '.cursor') : path.join(process.cwd(), '.cursor');
|
|
1334
|
+
return { dir, file: path.join(dir, 'hooks.json') };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function installCursorHook(global) {
|
|
1338
|
+
const { dir, file } = cursorHooksFile(global);
|
|
1339
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1340
|
+
let cfg = { version: 1, hooks: {} };
|
|
1341
|
+
if (fs.existsSync(file)) {
|
|
1342
|
+
try { cfg = JSON.parse(fs.readFileSync(file, 'utf-8')); } catch {}
|
|
1343
|
+
}
|
|
1344
|
+
if (!cfg.hooks) cfg.hooks = {};
|
|
1345
|
+
if (!cfg.hooks.beforeShellExecution) cfg.hooks.beforeShellExecution = [];
|
|
1346
|
+
const existing = cfg.hooks.beforeShellExecution.some(h => h.command?.includes('cursor-hook.js'));
|
|
1347
|
+
if (!existing) cfg.hooks.beforeShellExecution.push({ command: `node ${hookPath}` });
|
|
1348
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
1349
|
+
return file;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function uninstallCursorHook(global) {
|
|
1353
|
+
const { file } = cursorHooksFile(global);
|
|
1354
|
+
if (!fs.existsSync(file)) return false;
|
|
1355
|
+
try {
|
|
1356
|
+
const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
1357
|
+
const hooks = cfg.hooks?.beforeShellExecution || [];
|
|
1358
|
+
const filtered = hooks.filter(h => !h.command?.includes('cursor-hook.js'));
|
|
1359
|
+
if (filtered.length === hooks.length) return false;
|
|
1360
|
+
cfg.hooks.beforeShellExecution = filtered;
|
|
1361
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
1362
|
+
return true;
|
|
1363
|
+
} catch { return false; }
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// ── Claude Code config helpers ────────────────────────────────────────
|
|
1367
|
+
function claudeSettingsFile(global) {
|
|
1368
|
+
const dir = global ? path.join(os.homedir(), '.claude') : path.join(process.cwd(), '.claude');
|
|
1369
|
+
return { dir, file: path.join(dir, 'settings.json') };
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function installClaudeHook(global) {
|
|
1373
|
+
const { dir, file } = claudeSettingsFile(global);
|
|
1374
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1375
|
+
let cfg = {};
|
|
1376
|
+
if (fs.existsSync(file)) {
|
|
1377
|
+
try { cfg = JSON.parse(fs.readFileSync(file, 'utf-8')); } catch {}
|
|
1378
|
+
}
|
|
1379
|
+
if (!cfg.hooks) cfg.hooks = {};
|
|
1380
|
+
if (!Array.isArray(cfg.hooks.PreToolUse)) cfg.hooks.PreToolUse = [];
|
|
1381
|
+
|
|
1382
|
+
// Find or create the Bash matcher entry.
|
|
1383
|
+
let bashEntry = cfg.hooks.PreToolUse.find(e => e.matcher === 'Bash');
|
|
1384
|
+
if (!bashEntry) {
|
|
1385
|
+
bashEntry = { matcher: 'Bash', hooks: [] };
|
|
1386
|
+
cfg.hooks.PreToolUse.push(bashEntry);
|
|
1387
|
+
}
|
|
1388
|
+
if (!Array.isArray(bashEntry.hooks)) bashEntry.hooks = [];
|
|
1389
|
+
const existing = bashEntry.hooks.some(h => h.command?.includes('cursor-hook.js'));
|
|
1390
|
+
if (!existing) {
|
|
1391
|
+
bashEntry.hooks.push({
|
|
1392
|
+
type: 'command',
|
|
1393
|
+
command: `node ${hookPath}`,
|
|
1394
|
+
timeout: 10,
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
1398
|
+
return file;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function uninstallClaudeHook(global) {
|
|
1402
|
+
const { file } = claudeSettingsFile(global);
|
|
1403
|
+
if (!fs.existsSync(file)) return false;
|
|
1404
|
+
try {
|
|
1405
|
+
const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
1406
|
+
const pre = cfg.hooks?.PreToolUse || [];
|
|
1407
|
+
let changed = false;
|
|
1408
|
+
for (const entry of pre) {
|
|
1409
|
+
if (!Array.isArray(entry.hooks)) continue;
|
|
1410
|
+
const before = entry.hooks.length;
|
|
1411
|
+
entry.hooks = entry.hooks.filter(h => !h.command?.includes('cursor-hook.js'));
|
|
1412
|
+
if (entry.hooks.length !== before) changed = true;
|
|
1413
|
+
}
|
|
1414
|
+
// Drop empty Bash entries so we don't leave a dangling matcher.
|
|
1415
|
+
cfg.hooks.PreToolUse = pre.filter(e => !(e.matcher === 'Bash' && (!e.hooks || e.hooks.length === 0)));
|
|
1416
|
+
if (!changed) return false;
|
|
1417
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
1418
|
+
return true;
|
|
1419
|
+
} catch { return false; }
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1264
1422
|
// ── Uninstall ──────────────────────────────────────────────────────────
|
|
1265
1423
|
if (uninstall) {
|
|
1266
1424
|
let removed = false;
|
|
1267
|
-
if (fs.existsSync(hookPath)) {
|
|
1268
|
-
|
|
1269
|
-
|
|
1425
|
+
if (fs.existsSync(hookPath)) { fs.unlinkSync(hookPath); removed = true; }
|
|
1426
|
+
|
|
1427
|
+
// Cursor: clean both project and global, regardless of flags — be thorough.
|
|
1428
|
+
for (const g of [false, true]) {
|
|
1429
|
+
if (uninstallCursorHook(g)) {
|
|
1430
|
+
removed = true;
|
|
1431
|
+
console.log(clr(c.dim, ` Updated: ${cursorHooksFile(g).file}`));
|
|
1432
|
+
}
|
|
1270
1433
|
}
|
|
1271
|
-
//
|
|
1272
|
-
for (const
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
const hooksFile = path.join(hooksDir, 'hooks.json');
|
|
1277
|
-
if (fs.existsSync(hooksFile)) {
|
|
1278
|
-
try {
|
|
1279
|
-
const cfg = JSON.parse(fs.readFileSync(hooksFile, 'utf-8'));
|
|
1280
|
-
const hooks = cfg.hooks?.beforeShellExecution || [];
|
|
1281
|
-
const filtered = hooks.filter(h => !h.command?.includes('cursor-hook.js'));
|
|
1282
|
-
if (filtered.length !== hooks.length) {
|
|
1283
|
-
cfg.hooks.beforeShellExecution = filtered;
|
|
1284
|
-
fs.writeFileSync(hooksFile, JSON.stringify(cfg, null, 2) + '\n');
|
|
1285
|
-
removed = true;
|
|
1286
|
-
console.log(clr(c.dim, ` Updated: ${hooksFile}`));
|
|
1287
|
-
}
|
|
1288
|
-
} catch {}
|
|
1434
|
+
// Claude Code: same.
|
|
1435
|
+
for (const g of [false, true]) {
|
|
1436
|
+
if (uninstallClaudeHook(g)) {
|
|
1437
|
+
removed = true;
|
|
1438
|
+
console.log(clr(c.dim, ` Updated: ${claudeSettingsFile(g).file}`));
|
|
1289
1439
|
}
|
|
1290
1440
|
}
|
|
1441
|
+
|
|
1291
1442
|
if (removed) {
|
|
1292
|
-
console.log(clr(c.green, '\n ✓
|
|
1443
|
+
console.log(clr(c.green, '\n ✓ Commit hook uninstalled (Cursor + Claude Code).'));
|
|
1293
1444
|
} else {
|
|
1294
1445
|
console.log(clr(c.dim, '\n No hook found to remove.'));
|
|
1295
1446
|
}
|
|
@@ -1302,49 +1453,33 @@ main();
|
|
|
1302
1453
|
if (!fs.existsSync(commitDir)) fs.mkdirSync(commitDir, { recursive: true });
|
|
1303
1454
|
fs.writeFileSync(hookPath, hookScript, { mode: 0o755 });
|
|
1304
1455
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
: path.join(process.cwd(), '.cursor');
|
|
1309
|
-
if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
|
|
1310
|
-
|
|
1311
|
-
const hooksFile = path.join(hooksDir, 'hooks.json');
|
|
1312
|
-
let cfg = { version: 1, hooks: {} };
|
|
1313
|
-
if (fs.existsSync(hooksFile)) {
|
|
1314
|
-
try { cfg = JSON.parse(fs.readFileSync(hooksFile, 'utf-8')); } catch {}
|
|
1315
|
-
}
|
|
1316
|
-
if (!cfg.hooks) cfg.hooks = {};
|
|
1317
|
-
if (!cfg.hooks.beforeShellExecution) cfg.hooks.beforeShellExecution = [];
|
|
1318
|
-
|
|
1319
|
-
// Avoid duplicates
|
|
1320
|
-
const existing = cfg.hooks.beforeShellExecution.some(h => h.command?.includes('cursor-hook.js'));
|
|
1321
|
-
if (!existing) {
|
|
1322
|
-
cfg.hooks.beforeShellExecution.push({
|
|
1323
|
-
command: `node ${hookPath}`,
|
|
1324
|
-
});
|
|
1325
|
-
}
|
|
1326
|
-
fs.writeFileSync(hooksFile, JSON.stringify(cfg, null, 2) + '\n');
|
|
1456
|
+
const writtenFiles = [];
|
|
1457
|
+
if (installCursor) writtenFiles.push({ client: 'Cursor', file: installCursorHook(isGlobal) });
|
|
1458
|
+
if (installClaude) writtenFiles.push({ client: 'Claude Code', file: installClaudeHook(isGlobal) });
|
|
1327
1459
|
|
|
1328
1460
|
// 3. Report
|
|
1329
|
-
|
|
1461
|
+
const clientList = writtenFiles.map(w => w.client).join(' + ');
|
|
1462
|
+
console.log(clr(c.green, `\n ✓ Commit supply chain hook installed (${clientList})`));
|
|
1330
1463
|
console.log();
|
|
1331
1464
|
console.log(clr(c.bold, ' What happens now:'));
|
|
1332
1465
|
console.log(clr(c.dim, ' Every ') + clr(c.cyan, 'npm install') + clr(c.dim, ', ') +
|
|
1333
1466
|
clr(c.cyan, 'pip install') + clr(c.dim, ', ') + clr(c.cyan, 'cargo add') + clr(c.dim, ', and ') +
|
|
1334
|
-
clr(c.cyan, 'go get') + clr(c.dim,
|
|
1467
|
+
clr(c.cyan, 'go get') + clr(c.dim, ` in ${clientList}`));
|
|
1335
1468
|
console.log(clr(c.dim, ' is scored against Commit before it runs.'));
|
|
1336
1469
|
console.log(clr(c.dim, ' CRITICAL packages are blocked. HIGH packages ask for confirmation.'));
|
|
1337
1470
|
console.log();
|
|
1338
1471
|
console.log(clr(c.bold, ' Files:'));
|
|
1339
1472
|
console.log(clr(c.dim, ` Hook script: ${hookPath}`));
|
|
1340
|
-
|
|
1473
|
+
for (const w of writtenFiles) {
|
|
1474
|
+
console.log(clr(c.dim, ` ${w.client.padEnd(11)} ${w.file}`));
|
|
1475
|
+
}
|
|
1341
1476
|
console.log();
|
|
1342
1477
|
|
|
1343
1478
|
const key = await readApiKey();
|
|
1344
1479
|
if (!key) {
|
|
1345
1480
|
console.log(clr(c.yellow, ' ⚠ No API key found.') + clr(c.dim, ' Anonymous limit: 15 audits/day.'));
|
|
1346
1481
|
console.log(clr(c.dim, ' Get a free key (200/day): ') + clr(c.cyan, 'poc login'));
|
|
1347
|
-
console.log(clr(c.dim, ' Or: ') + clr(c.cyan, 'https://getcommit.dev/get-started?ref=
|
|
1482
|
+
console.log(clr(c.dim, ' Or: ') + clr(c.cyan, 'https://getcommit.dev/get-started?ref=poc-hook&utm_source=cli'));
|
|
1348
1483
|
console.log();
|
|
1349
1484
|
}
|
|
1350
1485
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proof-of-commitment",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.1",
|
|
4
4
|
"mcpName": "io.github.piiiico/proof-of-commitment",
|
|
5
5
|
"description": "Supply chain risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
|
|
6
6
|
"type": "module",
|