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.
- package/dist/interactive.js +83 -6
- package/dist/scanner.js +68 -0
- package/package.json +1 -1
package/dist/interactive.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
321
|
-
|
|
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) {
|