ccperm 1.12.1 → 1.13.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.
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.startInteractive = startInteractive;
7
7
  const node_readline_1 = __importDefault(require("node:readline"));
8
8
  const colors_js_1 = require("./colors.js");
9
+ const scanner_js_1 = require("./scanner.js");
9
10
  const explain_js_1 = require("./explain.js");
10
11
  function severityTag(s) {
11
12
  const labels = {
@@ -49,7 +50,7 @@ function boxTop(title, info, width) {
49
50
  function boxBottom(hint, width) {
50
51
  const inner = width - 2;
51
52
  const hintPart = ` ${hint} `;
52
- const fill = Math.max(0, inner - hintPart.length);
53
+ const fill = Math.max(0, inner - visLen(hintPart));
53
54
  return `${colors_js_1.DIM}└${'─'.repeat(fill)}${hintPart}┘${colors_js_1.NC}`;
54
55
  }
55
56
  function boxSep(width) {
@@ -113,7 +114,42 @@ function startInteractive(merged, results) {
113
114
  }
114
115
  }
115
116
  else {
116
- if (key.name === 'escape' || key.name === 'backspace') {
117
+ if (state.confirmDelete) {
118
+ if (key.name === 'y') {
119
+ const { rawPerm, filePath } = state.confirmDelete;
120
+ if ((0, scanner_js_1.removePerm)(filePath, rawPerm)) {
121
+ const idx = results.findIndex((r) => r.path === filePath);
122
+ if (idx >= 0) {
123
+ const updated = (0, scanner_js_1.scanFile)(filePath);
124
+ if (updated) {
125
+ results[idx] = updated;
126
+ const entry = withPerms[state.selectedProject];
127
+ entry.totalCount = updated.totalCount;
128
+ entry.groups = new Map();
129
+ for (const g of updated.groups)
130
+ entry.groups.set(g.category, g.items.length);
131
+ }
132
+ }
133
+ state.flash = `${colors_js_1.GREEN}✔ Deleted${colors_js_1.NC}`;
134
+ }
135
+ state.confirmDelete = undefined;
136
+ }
137
+ else {
138
+ state.confirmDelete = undefined;
139
+ }
140
+ }
141
+ else if (state.confirmGlobal) {
142
+ if (key.name === 'y') {
143
+ if ((0, scanner_js_1.addPermToGlobal)(state.confirmGlobal.rawPerm)) {
144
+ state.flash = `${colors_js_1.GREEN}✔ Copied to global${colors_js_1.NC}`;
145
+ }
146
+ else {
147
+ state.flash = `${colors_js_1.DIM}· Already in global${colors_js_1.NC}`;
148
+ }
149
+ }
150
+ state.confirmGlobal = undefined;
151
+ }
152
+ else if (key.name === 'escape' || key.name === 'backspace') {
117
153
  state.view = 'list';
118
154
  state.detailCursor = 0;
119
155
  state.detailScroll = 0;
@@ -130,6 +166,12 @@ function startInteractive(merged, results) {
130
166
  else if (key.name === 'i') {
131
167
  state.showInfo = !state.showInfo;
132
168
  }
169
+ else if (key.name === 'd') {
170
+ state._delete = true;
171
+ }
172
+ else if (key.name === 'g') {
173
+ state._global = true;
174
+ }
133
175
  }
134
176
  render();
135
177
  };
@@ -267,6 +309,8 @@ function renderDetail(state, withPerms, results) {
267
309
  if (isOpen) {
268
310
  for (const item of group.items) {
269
311
  const clean = cleanLabel(item.name);
312
+ // Find the raw permission string from the original permissions array
313
+ const rawPerm = fileResult.permissions.find((p) => p.includes(item.name)) || '';
270
314
  if (state.showInfo) {
271
315
  const info = (0, explain_js_1.explain)(group.category, item.name);
272
316
  const tag = severityTag(info.risk);
@@ -274,12 +318,12 @@ function renderDetail(state, withPerms, results) {
274
318
  const nameMax = Math.min(30, w - tagLen - 14);
275
319
  const name = clean.length > nameMax ? clean.slice(0, nameMax - 1) + '…' : clean;
276
320
  const desc = info.description ? `${colors_js_1.DIM}${info.description}${colors_js_1.NC}` : '';
277
- navRows.push({ text: ` ${pad(name, nameMax)} ${tag} ${desc}`, perm: item.name });
321
+ navRows.push({ text: ` ${pad(name, nameMax)} ${tag} ${desc}`, perm: item.name, rawPerm });
278
322
  }
279
323
  else {
280
324
  const maxLen = w - 8;
281
325
  const name = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '…' : clean;
282
- navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, perm: item.name });
326
+ navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, perm: item.name, rawPerm });
283
327
  }
284
328
  }
285
329
  }
@@ -297,6 +341,22 @@ function renderDetail(state, withPerms, results) {
297
341
  return;
298
342
  }
299
343
  }
344
+ // handle delete
345
+ if (state._delete) {
346
+ delete state._delete;
347
+ const row = navRows[state.detailCursor];
348
+ if (row?.rawPerm) {
349
+ state.confirmDelete = { perm: row.perm, rawPerm: row.rawPerm, filePath: fileResult.path };
350
+ }
351
+ }
352
+ // handle global copy
353
+ if (state._global) {
354
+ delete state._global;
355
+ const row = navRows[state.detailCursor];
356
+ if (row?.rawPerm && !project.isGlobal) {
357
+ state.confirmGlobal = { perm: row.perm, rawPerm: row.rawPerm };
358
+ }
359
+ }
300
360
  if (state.detailCursor >= navRows.length)
301
361
  state.detailCursor = Math.max(0, navRows.length - 1);
302
362
  // box chrome: top(1) + sep(1) + bottom(1) = 3
@@ -317,8 +377,25 @@ function renderDetail(state, withPerms, results) {
317
377
  const prefix = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
318
378
  lines.push(boxLine(`${prefix}${row.text}`, w));
319
379
  }
320
- const infoHint = state.showInfo ? '[i] hide info' : '[i] info';
321
- lines.push(boxBottom(`[↑↓] navigate [Enter] expand ${infoHint} [Esc] back [q] quit`, w));
380
+ if (state.flash) {
381
+ lines.push(boxBottom(state.flash, w));
382
+ state.flash = undefined;
383
+ }
384
+ else if (state.confirmDelete) {
385
+ const name = cleanLabel(state.confirmDelete.perm);
386
+ const truncName = name.length > 30 ? name.slice(0, 29) + '…' : name;
387
+ lines.push(boxBottom(`${colors_js_1.RED}Delete "${truncName}"? [y/N]${colors_js_1.NC}`, w));
388
+ }
389
+ else if (state.confirmGlobal) {
390
+ const name = cleanLabel(state.confirmGlobal.perm);
391
+ const truncName = name.length > 30 ? name.slice(0, 29) + '…' : name;
392
+ lines.push(boxBottom(`${colors_js_1.CYAN}Copy "${truncName}" to global? [y/N]${colors_js_1.NC}`, w));
393
+ }
394
+ else {
395
+ const infoHint = state.showInfo ? '[i] hide info' : '[i] info';
396
+ const globalHint = project.isGlobal ? '' : ' [g] global';
397
+ lines.push(boxBottom(`[↑↓] navigate [Enter] expand ${infoHint} [d] delete${globalHint} [Esc] back [q] quit`, w));
398
+ }
322
399
  process.stdout.write(lines.join('\n') + '\n');
323
400
  }
324
401
  function pad(s, n) {
package/dist/scanner.js CHANGED
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.removePerm = removePerm;
7
+ exports.addPermToGlobal = addPermToGlobal;
6
8
  exports.countDeprecated = countDeprecated;
7
9
  exports.findSettingsFiles = findSettingsFiles;
8
10
  exports.scanFile = scanFile;
@@ -11,6 +13,72 @@ const node_path_1 = __importDefault(require("node:path"));
11
13
  const node_os_1 = __importDefault(require("node:os"));
12
14
  const PERM_RE = /"(Bash|Write|Edit|Read|Glob|Grep|WebSearch|WebFetch|mcp_)[^"]*"/g;
13
15
  const DEPRECATED_RE = /:\*\)|:\*"/g;
16
+ const AUDIT_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.ccperm', 'audit');
17
+ function writeAudit(action, filePath, perm, before, after) {
18
+ try {
19
+ node_fs_1.default.mkdirSync(AUDIT_DIR, { recursive: true });
20
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
21
+ const entry = { action, file: filePath, perm, before, after, timestamp: new Date().toISOString() };
22
+ node_fs_1.default.writeFileSync(node_path_1.default.join(AUDIT_DIR, `${ts}_${action}.json`), JSON.stringify(entry, null, 2) + '\n');
23
+ }
24
+ catch { /* audit is best-effort */ }
25
+ }
26
+ function removePerm(filePath, rawPerm) {
27
+ let content;
28
+ try {
29
+ content = node_fs_1.default.readFileSync(filePath, 'utf8');
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ let json;
35
+ try {
36
+ json = JSON.parse(content);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ const allow = json?.permissions?.allow;
42
+ if (!Array.isArray(allow))
43
+ return false;
44
+ const idx = allow.indexOf(rawPerm);
45
+ if (idx === -1)
46
+ return false;
47
+ const before = [...allow];
48
+ allow.splice(idx, 1);
49
+ node_fs_1.default.writeFileSync(filePath, JSON.stringify(json, null, 2) + '\n', 'utf8');
50
+ writeAudit('DELETE', filePath, rawPerm, before, allow);
51
+ return true;
52
+ }
53
+ function addPermToGlobal(rawPerm) {
54
+ const globalPath = node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'settings.json');
55
+ let content;
56
+ try {
57
+ content = node_fs_1.default.readFileSync(globalPath, 'utf8');
58
+ }
59
+ catch {
60
+ content = '{}';
61
+ }
62
+ let json;
63
+ try {
64
+ json = JSON.parse(content);
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ if (!json.permissions)
70
+ json.permissions = {};
71
+ if (!Array.isArray(json.permissions.allow))
72
+ json.permissions.allow = [];
73
+ const allow = json.permissions.allow;
74
+ if (allow.includes(rawPerm))
75
+ return false; // already exists
76
+ const before = [...allow];
77
+ allow.push(rawPerm);
78
+ node_fs_1.default.writeFileSync(globalPath, JSON.stringify(json, null, 2) + '\n', 'utf8');
79
+ writeAudit('COPY_TO_GLOBAL', globalPath, rawPerm, before, allow);
80
+ return true;
81
+ }
14
82
  function countDeprecated(results) {
15
83
  const out = [];
16
84
  for (const r of results) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.12.1",
3
+ "version": "1.13.0",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"