ccperm 1.12.1 → 1.14.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.ko.md +13 -14
- package/README.md +13 -14
- package/demo.gif +0 -0
- package/dist/interactive.js +363 -32
- package/dist/scanner.js +101 -0
- package/package.json +2 -2
- package/screenshot.png +0 -0
package/README.ko.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Claude Code는 프로젝트마다 `.claude/settings*.json`에 허용한 권한(Bash 명령, WebFetch 도메인, MCP 도구 등)을 저장합니다. 여러 프로젝트를 오가다 보면 어디서 뭘 허용했는지 파악하기 어려운데, **ccperm**은 홈 디렉토리 전체를 스캔해서 모든 설정 파일을 찾고, 인터랙티브 TUI 또는 텍스트로 보여줍니다.
|
|
8
8
|
|
|
9
|
-
<img src="./
|
|
9
|
+
<img src="./demo.gif" width="600" />
|
|
10
10
|
|
|
11
11
|
## 빠른 시작
|
|
12
12
|
|
|
@@ -41,25 +41,24 @@ ccperm
|
|
|
41
41
|
|
|
42
42
|
TTY 환경(기본)에서는 박스 프레임 TUI가 실행됩니다:
|
|
43
43
|
|
|
44
|
-
**목록 뷰** — 프로젝트가 권한 수 기준으로 정렬됩니다.
|
|
44
|
+
**목록 뷰** — 프로젝트가 권한 수 기준으로 정렬됩니다. 컬럼: Bash, MCP, Tools, TOTAL, `!` (위험도 경고), `†` (deprecated `:*` 패턴).
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
┌ ccperm
|
|
48
|
-
│
|
|
49
|
-
|
|
50
|
-
│
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
│
|
|
54
|
-
|
|
55
|
-
└ [↑↓] navigate [Enter] detail [q] quit ────┘
|
|
47
|
+
┌ ccperm ──────────────────────────── 1/8 ┐
|
|
48
|
+
│ PROJECT Bash MCP Tools TOTAL │
|
|
49
|
+
├─────────────────────────────────────────┤
|
|
50
|
+
│ ~/.claude 2 2 │
|
|
51
|
+
├─────────────────────────────────────────┤
|
|
52
|
+
│ ▸ my-project local 5 · 3 8 │
|
|
53
|
+
│ other-app shared 2 3 · 5 │
|
|
54
|
+
└ [↑↓] navigate [Enter] detail [q] quit┘
|
|
56
55
|
```
|
|
57
56
|
|
|
58
|
-
**상세 뷰** — Enter로 프로젝트를 펼칩니다. 카테고리를 Enter로 접고 펼 수 있습니다.
|
|
57
|
+
**상세 뷰** — Enter로 프로젝트를 펼칩니다. 카테고리를 Enter로 접고 펼 수 있습니다. `[d]`로 권한 삭제, `[g]`로 글로벌 설정에 복사할 수 있습니다.
|
|
59
58
|
|
|
60
|
-
**정보 모드** — `[i]`를 누르면 각
|
|
59
|
+
**정보 모드** — `[i]`를 누르면 각 권한의 위험도와 설명이 나타납니다.
|
|
61
60
|
|
|
62
|
-
키 조작: `↑↓` 이동, `Enter` 선택/펼치기, `[i]`
|
|
61
|
+
키 조작: `↑↓` 이동, `Enter` 선택/펼치기, `[i]` 정보, `[d]` 삭제, `[g]` 글로벌 복사, `Esc` 뒤로, `q` 종료.
|
|
63
62
|
|
|
64
63
|
## 텍스트 출력
|
|
65
64
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Audit Claude Code permissions across all your projects.
|
|
|
6
6
|
|
|
7
7
|
Claude Code stores allowed permissions (Bash commands, WebFetch domains, MCP tools, etc.) in `.claude/settings*.json` per project. As you work across many projects, these permissions pile up silently. **ccperm** scans your home directory, finds every settings file, and shows what you've allowed — in an interactive TUI or static text output.
|
|
8
8
|
|
|
9
|
-
<img src="./
|
|
9
|
+
<img src="./demo.gif" width="600" />
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
@@ -41,25 +41,24 @@ By default, ccperm scans all projects under `~` and launches an interactive TUI.
|
|
|
41
41
|
|
|
42
42
|
When running in a TTY (the default), ccperm opens a box-frame TUI:
|
|
43
43
|
|
|
44
|
-
**List view** — Projects sorted by permission count.
|
|
44
|
+
**List view** — Projects sorted by permission count. Columns: Bash, MCP, Tools, TOTAL, `!` (risk warnings), `†` (deprecated `:*` patterns).
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
┌ ccperm
|
|
48
|
-
│
|
|
49
|
-
|
|
50
|
-
│
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
│
|
|
54
|
-
|
|
55
|
-
└ [↑↓] navigate [Enter] detail [q] quit ────┘
|
|
47
|
+
┌ ccperm ──────────────────────────── 1/8 ┐
|
|
48
|
+
│ PROJECT Bash MCP Tools TOTAL │
|
|
49
|
+
├─────────────────────────────────────────┤
|
|
50
|
+
│ ~/.claude 2 2 │
|
|
51
|
+
├─────────────────────────────────────────┤
|
|
52
|
+
│ ▸ my-project local 5 · 3 8 │
|
|
53
|
+
│ other-app shared 2 3 · 5 │
|
|
54
|
+
└ [↑↓] navigate [Enter] detail [q] quit┘
|
|
56
55
|
```
|
|
57
56
|
|
|
58
|
-
**Detail view** — Press Enter to expand a project. Categories are collapsible; press Enter to toggle.
|
|
57
|
+
**Detail view** — Press Enter to expand a project. Categories are collapsible; press Enter to toggle. Press `[d]` to delete a permission, `[g]` to copy it to global settings.
|
|
59
58
|
|
|
60
|
-
**Info mode** — Press `[i]` to show
|
|
59
|
+
**Info mode** — Press `[i]` to show risk level and description for each permission.
|
|
61
60
|
|
|
62
|
-
Keys: `↑↓` navigate, `Enter` select/expand, `[i]`
|
|
61
|
+
Keys: `↑↓` navigate, `Enter` select/expand, `[i]` info, `[d]` delete, `[g]` copy to global, `Esc` back, `q` quit.
|
|
63
62
|
|
|
64
63
|
## Static Output
|
|
65
64
|
|
package/demo.gif
ADDED
|
Binary file
|
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 = {
|
|
@@ -41,20 +42,62 @@ function boxLine(text, width) {
|
|
|
41
42
|
}
|
|
42
43
|
function boxTop(title, info, width) {
|
|
43
44
|
const inner = width - 2;
|
|
44
|
-
const titlePart = ` ${title} `;
|
|
45
45
|
const infoPart = info ? ` ${info} ` : '';
|
|
46
|
-
const
|
|
46
|
+
const maxTitle = inner - infoPart.length - 2; // 2 for spaces around title
|
|
47
|
+
let truncTitle = title;
|
|
48
|
+
if (visLen(truncTitle) > maxTitle) {
|
|
49
|
+
// strip ANSI, truncate, re-wrap won't work cleanly — truncate the raw string
|
|
50
|
+
const plain = truncTitle.replace(/\x1b\[[0-9;]*m/g, '');
|
|
51
|
+
truncTitle = plain.slice(0, maxTitle - 1) + '…';
|
|
52
|
+
}
|
|
53
|
+
const titlePart = ` ${truncTitle} `;
|
|
54
|
+
const fill = Math.max(0, inner - visLen(titlePart) - infoPart.length);
|
|
47
55
|
return `${colors_js_1.DIM}┌${titlePart}${'─'.repeat(fill)}${infoPart}┐${colors_js_1.NC}`;
|
|
48
56
|
}
|
|
49
57
|
function boxBottom(hint, width) {
|
|
50
58
|
const inner = width - 2;
|
|
51
59
|
const hintPart = ` ${hint} `;
|
|
52
|
-
const fill = Math.max(0, inner - hintPart
|
|
60
|
+
const fill = Math.max(0, inner - visLen(hintPart));
|
|
53
61
|
return `${colors_js_1.DIM}└${'─'.repeat(fill)}${hintPart}┘${colors_js_1.NC}`;
|
|
54
62
|
}
|
|
63
|
+
function boxBottom2(line1, line2, width) {
|
|
64
|
+
return boxLine(line1, width) + '\n' + boxBottom(line2, width);
|
|
65
|
+
}
|
|
55
66
|
function boxSep(width) {
|
|
56
67
|
return `${colors_js_1.DIM}├${'─'.repeat(width - 2)}┤${colors_js_1.NC}`;
|
|
57
68
|
}
|
|
69
|
+
function refreshProject(results, withPerms, idx, filePath) {
|
|
70
|
+
const ri = results.findIndex((r) => r.path === filePath);
|
|
71
|
+
if (ri >= 0) {
|
|
72
|
+
const updated = (0, scanner_js_1.scanFile)(filePath);
|
|
73
|
+
if (updated) {
|
|
74
|
+
results[ri] = updated;
|
|
75
|
+
const entry = withPerms[idx];
|
|
76
|
+
entry.totalCount = updated.totalCount;
|
|
77
|
+
entry.groups = new Map();
|
|
78
|
+
for (const g of updated.groups)
|
|
79
|
+
entry.groups.set(g.category, g.items.length);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function buildDupMap(results) {
|
|
84
|
+
const globalResult = results.find((r) => r.isGlobal);
|
|
85
|
+
const globalPerms = globalResult ? globalResult.permissions : [];
|
|
86
|
+
const map = new Map();
|
|
87
|
+
for (const r of results) {
|
|
88
|
+
if (r.isGlobal)
|
|
89
|
+
continue;
|
|
90
|
+
const dup = (0, scanner_js_1.findDuplicates)(r.path, globalPerms);
|
|
91
|
+
const total = dup.exact.length + dup.globalDup.length;
|
|
92
|
+
if (total > 0)
|
|
93
|
+
map.set(r.display, { exact: dup.exact.length, global: dup.globalDup.length });
|
|
94
|
+
}
|
|
95
|
+
return map;
|
|
96
|
+
}
|
|
97
|
+
function getGlobalPerms(results) {
|
|
98
|
+
const globalResult = results.find((r) => r.isGlobal);
|
|
99
|
+
return globalResult ? globalResult.permissions : [];
|
|
100
|
+
}
|
|
58
101
|
function startInteractive(merged, results) {
|
|
59
102
|
return new Promise((resolve) => {
|
|
60
103
|
const globals = merged.filter((r) => r.isGlobal);
|
|
@@ -63,12 +106,13 @@ function startInteractive(merged, results) {
|
|
|
63
106
|
const emptyCount = merged.filter((r) => r.totalCount === 0 && !r.isGlobal).length;
|
|
64
107
|
const riskMap = buildRiskMap(results);
|
|
65
108
|
const depMap = buildDeprecatedMap(results);
|
|
109
|
+
let dupMap = buildDupMap(results);
|
|
66
110
|
if (withPerms.length === 0) {
|
|
67
111
|
console.log(`\n ${colors_js_1.GREEN}No projects with permissions found.${colors_js_1.NC}\n`);
|
|
68
112
|
resolve();
|
|
69
113
|
return;
|
|
70
114
|
}
|
|
71
|
-
const state = { view: 'list', cursor: 0, scrollOffset: 0, selectedProject: 0, detailCursor: 0, detailScroll: 0, expanded: new Set(), showInfo: false };
|
|
115
|
+
const state = { view: 'list', cursor: 0, scrollOffset: 0, selectedProject: 0, detailCursor: 0, detailScroll: 0, expanded: new Set(), showInfo: false, searchActive: false, searchQuery: '' };
|
|
72
116
|
node_readline_1.default.emitKeypressEvents(process.stdin);
|
|
73
117
|
if (process.stdin.isTTY)
|
|
74
118
|
process.stdin.setRawMode(true);
|
|
@@ -84,21 +128,83 @@ function startInteractive(merged, results) {
|
|
|
84
128
|
const onSigint = () => { cleanup(); process.exit(0); };
|
|
85
129
|
process.on('SIGINT', onSigint);
|
|
86
130
|
const render = () => {
|
|
87
|
-
process.stdout.write('\x1b[2J\x1b[H
|
|
88
|
-
|
|
89
|
-
|
|
131
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
132
|
+
process.stdout.write(state.searchActive ? '\x1b[?25h' : '\x1b[?25l');
|
|
133
|
+
if (state.view === 'search')
|
|
134
|
+
renderSearch(state, withPerms, results);
|
|
135
|
+
else if (state.view === 'list')
|
|
136
|
+
renderList(state, withPerms, emptyCount, riskMap, depMap, dupMap);
|
|
90
137
|
else
|
|
91
|
-
renderDetail(state, withPerms, results);
|
|
138
|
+
renderDetail(state, withPerms, results, dupMap);
|
|
92
139
|
};
|
|
93
140
|
const onKey = (_str, key) => {
|
|
94
141
|
if (!key)
|
|
95
142
|
return;
|
|
143
|
+
// Search typing mode (active in search view)
|
|
144
|
+
if (state.searchActive) {
|
|
145
|
+
if (key.name === 'escape') {
|
|
146
|
+
state.searchActive = false;
|
|
147
|
+
state.searchQuery = '';
|
|
148
|
+
state.view = 'list';
|
|
149
|
+
state.cursor = 0;
|
|
150
|
+
state.scrollOffset = 0;
|
|
151
|
+
}
|
|
152
|
+
else if (key.name === 'return') {
|
|
153
|
+
// Navigate to selected hit's project
|
|
154
|
+
const hits = buildSearchHits(state.searchQuery, withPerms, results);
|
|
155
|
+
const hit = hits[state.cursor];
|
|
156
|
+
if (hit && !hit.isHeader) {
|
|
157
|
+
state.searchActive = false;
|
|
158
|
+
state.selectedProject = hit.projectIdx;
|
|
159
|
+
state.detailCursor = 0;
|
|
160
|
+
state.detailScroll = 0;
|
|
161
|
+
state.expanded = new Set();
|
|
162
|
+
state.searchQuery = '';
|
|
163
|
+
state.view = 'detail';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (key.name === 'backspace') {
|
|
167
|
+
state.searchQuery = state.searchQuery.slice(0, -1);
|
|
168
|
+
state.cursor = 0;
|
|
169
|
+
state.scrollOffset = 0;
|
|
170
|
+
}
|
|
171
|
+
else if (key.name === 'up' || key.name === 'down') {
|
|
172
|
+
const hits = buildSearchHits(state.searchQuery, withPerms, results);
|
|
173
|
+
if (key.name === 'up') {
|
|
174
|
+
do {
|
|
175
|
+
state.cursor = Math.max(0, state.cursor - 1);
|
|
176
|
+
} while (state.cursor > 0 && hits[state.cursor]?.isHeader);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
do {
|
|
180
|
+
state.cursor = Math.min(hits.length - 1, state.cursor + 1);
|
|
181
|
+
} while (state.cursor < hits.length - 1 && hits[state.cursor]?.isHeader);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (_str && _str.length === 1 && !key.ctrl && !key.meta) {
|
|
185
|
+
state.searchQuery += _str;
|
|
186
|
+
state.cursor = 0;
|
|
187
|
+
state.scrollOffset = 0;
|
|
188
|
+
}
|
|
189
|
+
render();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
96
192
|
if (key.name === 'q' || (key.name === 'c' && key.ctrl)) {
|
|
97
193
|
cleanup();
|
|
98
194
|
console.log('');
|
|
99
195
|
resolve();
|
|
100
196
|
return;
|
|
101
197
|
}
|
|
198
|
+
// `/` activates search from list or search view
|
|
199
|
+
if (_str === '/' && (state.view === 'list' || state.view === 'search') && !state.confirmDelete && !state.confirmGlobal) {
|
|
200
|
+
state.searchActive = true;
|
|
201
|
+
state.searchQuery = '';
|
|
202
|
+
state.view = 'search';
|
|
203
|
+
state.cursor = 0;
|
|
204
|
+
state.scrollOffset = 0;
|
|
205
|
+
render();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
102
208
|
if (state.view === 'list') {
|
|
103
209
|
if (key.name === 'up')
|
|
104
210
|
state.cursor = Math.max(0, state.cursor - 1);
|
|
@@ -112,11 +218,76 @@ function startInteractive(merged, results) {
|
|
|
112
218
|
state.view = 'detail';
|
|
113
219
|
}
|
|
114
220
|
}
|
|
221
|
+
else if (state.view === 'search') {
|
|
222
|
+
const hits = buildSearchHits(state.searchQuery, withPerms, results);
|
|
223
|
+
if (key.name === 'escape') {
|
|
224
|
+
state.view = 'list';
|
|
225
|
+
state.cursor = 0;
|
|
226
|
+
state.scrollOffset = 0;
|
|
227
|
+
state.searchQuery = '';
|
|
228
|
+
}
|
|
229
|
+
else if (key.name === 'up') {
|
|
230
|
+
do {
|
|
231
|
+
state.cursor = Math.max(0, state.cursor - 1);
|
|
232
|
+
} while (state.cursor > 0 && hits[state.cursor]?.isHeader);
|
|
233
|
+
}
|
|
234
|
+
else if (key.name === 'down') {
|
|
235
|
+
do {
|
|
236
|
+
state.cursor = Math.min(hits.length - 1, state.cursor + 1);
|
|
237
|
+
} while (state.cursor < hits.length - 1 && hits[state.cursor]?.isHeader);
|
|
238
|
+
}
|
|
239
|
+
else if (key.name === 'return') {
|
|
240
|
+
const hit = hits[state.cursor];
|
|
241
|
+
if (hit && !hit.isHeader) {
|
|
242
|
+
state.selectedProject = hit.projectIdx;
|
|
243
|
+
state.detailCursor = 0;
|
|
244
|
+
state.detailScroll = 0;
|
|
245
|
+
state.expanded = new Set();
|
|
246
|
+
state.searchQuery = '';
|
|
247
|
+
state.view = 'detail';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
115
251
|
else {
|
|
116
|
-
|
|
252
|
+
// detail view
|
|
253
|
+
if (state.confirmDelete) {
|
|
254
|
+
if (key.name === 'y') {
|
|
255
|
+
const { rawPerm, filePath } = state.confirmDelete;
|
|
256
|
+
if ((0, scanner_js_1.removePerm)(filePath, rawPerm)) {
|
|
257
|
+
refreshProject(results, withPerms, state.selectedProject, filePath);
|
|
258
|
+
dupMap = buildDupMap(results);
|
|
259
|
+
state.flash = `${colors_js_1.GREEN}✔ Deleted${colors_js_1.NC}`;
|
|
260
|
+
}
|
|
261
|
+
state.confirmDelete = undefined;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
state.confirmDelete = undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (state.confirmGlobal) {
|
|
268
|
+
if (key.name === 'y') {
|
|
269
|
+
if ((0, scanner_js_1.addPermToGlobal)(state.confirmGlobal.rawPerm)) {
|
|
270
|
+
// Refresh global scan result so dupMap picks up new global perm
|
|
271
|
+
const globalIdx = results.findIndex((r) => r.isGlobal);
|
|
272
|
+
if (globalIdx >= 0) {
|
|
273
|
+
const updated = (0, scanner_js_1.scanFile)(results[globalIdx].path);
|
|
274
|
+
if (updated)
|
|
275
|
+
results[globalIdx] = updated;
|
|
276
|
+
}
|
|
277
|
+
dupMap = buildDupMap(results);
|
|
278
|
+
state.flash = `${colors_js_1.GREEN}✔ Added to global${colors_js_1.NC}`;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
state.flash = `${colors_js_1.DIM}· Already in global${colors_js_1.NC}`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
state.confirmGlobal = undefined;
|
|
285
|
+
}
|
|
286
|
+
else if (key.name === 'escape' || key.name === 'backspace') {
|
|
117
287
|
state.view = 'list';
|
|
118
288
|
state.detailCursor = 0;
|
|
119
289
|
state.detailScroll = 0;
|
|
290
|
+
state.searchQuery = '';
|
|
120
291
|
}
|
|
121
292
|
else if (key.name === 'up') {
|
|
122
293
|
state.detailCursor = Math.max(0, state.detailCursor - 1);
|
|
@@ -130,6 +301,19 @@ function startInteractive(merged, results) {
|
|
|
130
301
|
else if (key.name === 'i') {
|
|
131
302
|
state.showInfo = !state.showInfo;
|
|
132
303
|
}
|
|
304
|
+
else if (key.name === 'd') {
|
|
305
|
+
state._delete = true;
|
|
306
|
+
}
|
|
307
|
+
else if (key.name === 'g') {
|
|
308
|
+
state._global = true;
|
|
309
|
+
}
|
|
310
|
+
else if (_str === '/' && !state.confirmDelete && !state.confirmGlobal) {
|
|
311
|
+
state.searchActive = true;
|
|
312
|
+
state.searchQuery = '';
|
|
313
|
+
state.view = 'search';
|
|
314
|
+
state.cursor = 0;
|
|
315
|
+
state.scrollOffset = 0;
|
|
316
|
+
}
|
|
133
317
|
}
|
|
134
318
|
render();
|
|
135
319
|
};
|
|
@@ -167,27 +351,27 @@ function buildRiskMap(results) {
|
|
|
167
351
|
}
|
|
168
352
|
return map;
|
|
169
353
|
}
|
|
170
|
-
function renderList(state, withPerms, emptyCount, riskMap, depMap) {
|
|
354
|
+
function renderList(state, withPerms, emptyCount, riskMap, depMap, dupMap) {
|
|
171
355
|
const rows = process.stdout.rows || 24;
|
|
172
356
|
const cols = process.stdout.columns || 80;
|
|
173
357
|
const cats = ['Bash', 'MCP', 'Tools'];
|
|
174
358
|
const catsPresent = cats.filter((c) => withPerms.some((r) => r.groups.has(c)));
|
|
175
359
|
const hasRisk = [...riskMap.values()].some((v) => v.critical > 0 || v.high > 0);
|
|
176
360
|
const hasDep = depMap.size > 0;
|
|
361
|
+
const hasDup = dupMap.size > 0;
|
|
177
362
|
const riskColWidth = hasRisk ? 3 : 0;
|
|
178
363
|
const depColWidth = hasDep ? 3 : 0;
|
|
364
|
+
const dupColWidth = hasDup ? 4 : 0;
|
|
179
365
|
const catColWidth = catsPresent.length * 7;
|
|
180
366
|
const typeColWidth = 7;
|
|
181
367
|
const maxName = Math.max(...withPerms.map((r) => r.shortName.length), 7);
|
|
182
368
|
const nameColWidth = Math.min(maxName + typeColWidth, 35);
|
|
183
369
|
const nameWidth = nameColWidth - typeColWidth;
|
|
184
|
-
|
|
185
|
-
const contentWidth = 2 + nameColWidth + 2 + catColWidth + 2 + 5 + (hasRisk ? riskColWidth : 0) + (hasDep ? depColWidth : 0);
|
|
370
|
+
const contentWidth = 2 + nameColWidth + 2 + catColWidth + 2 + 5 + (hasRisk ? riskColWidth : 0) + (hasDep ? depColWidth : 0) + (hasDup ? dupColWidth : 0);
|
|
186
371
|
const w = Math.min(cols, contentWidth + 4);
|
|
187
|
-
const inner = w - 4;
|
|
188
372
|
const hasGlobalSep = withPerms.some((r) => r.isGlobal) && withPerms.some((r) => !r.isGlobal);
|
|
189
|
-
|
|
190
|
-
const chrome = 5 + (hasGlobalSep ? 1 : 0) + (emptyCount > 0 ? 1 : 0);
|
|
373
|
+
const hasLegend = hasRisk || hasDep || hasDup;
|
|
374
|
+
const chrome = 5 + (hasGlobalSep ? 1 : 0) + (emptyCount > 0 ? 1 : 0) + (hasLegend ? 1 : 0);
|
|
191
375
|
const visibleRows = Math.min(25, Math.max(1, rows - chrome));
|
|
192
376
|
if (state.cursor < state.scrollOffset)
|
|
193
377
|
state.scrollOffset = state.cursor;
|
|
@@ -198,9 +382,9 @@ function renderList(state, withPerms, emptyCount, riskMap, depMap) {
|
|
|
198
382
|
lines.push(boxTop('ccperm', scrollInfo, w));
|
|
199
383
|
const riskHeader = hasRisk ? ` ${rpad('!', 2)}` : '';
|
|
200
384
|
const depHeader = hasDep ? ` ${rpad('†', 2)}` : '';
|
|
201
|
-
|
|
385
|
+
const dupHeader = hasDup ? ` ${rpad('G', 3)}` : '';
|
|
386
|
+
lines.push(boxLine(`${colors_js_1.DIM} ${pad('PROJECT', nameColWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} ${rpad('TOTAL', 5)}${riskHeader}${depHeader}${dupHeader}${colors_js_1.NC}`, w));
|
|
202
387
|
lines.push(boxSep(w));
|
|
203
|
-
const globalCount = withPerms.filter((r) => r.isGlobal).length;
|
|
204
388
|
const end = Math.min(state.scrollOffset + visibleRows, withPerms.length);
|
|
205
389
|
for (let i = state.scrollOffset; i < end; i++) {
|
|
206
390
|
const r = withPerms[i];
|
|
@@ -235,7 +419,17 @@ function renderList(state, withPerms, emptyCount, riskMap, depMap) {
|
|
|
235
419
|
else
|
|
236
420
|
depCol = ` ${colors_js_1.DIM}${rpad('·', 2)}${colors_js_1.NC}`;
|
|
237
421
|
}
|
|
238
|
-
|
|
422
|
+
let dupCol = '';
|
|
423
|
+
if (hasDup) {
|
|
424
|
+
const dup = dupMap.get(r.display);
|
|
425
|
+
if (dup && dup.global > 0) {
|
|
426
|
+
dupCol = ` ${colors_js_1.YELLOW}${rpad(dup.global, 3)}${colors_js_1.NC}`;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
dupCol = ` ${colors_js_1.DIM}${rpad('·', 3)}${colors_js_1.NC}`;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}${riskCol}${depCol}${dupCol}`, w));
|
|
239
433
|
// separator after global section
|
|
240
434
|
if (r.isGlobal && i + 1 < withPerms.length && !withPerms[i + 1].isGlobal) {
|
|
241
435
|
lines.push(boxSep(w));
|
|
@@ -244,10 +438,93 @@ function renderList(state, withPerms, emptyCount, riskMap, depMap) {
|
|
|
244
438
|
if (emptyCount > 0) {
|
|
245
439
|
lines.push(boxLine(`${colors_js_1.DIM}+ ${emptyCount} projects with no permissions${colors_js_1.NC}`, w));
|
|
246
440
|
}
|
|
247
|
-
|
|
441
|
+
const legendParts = [];
|
|
442
|
+
if (hasRisk)
|
|
443
|
+
legendParts.push(`${colors_js_1.RED}!${colors_js_1.NC} risk`);
|
|
444
|
+
if (hasDep)
|
|
445
|
+
legendParts.push(`${colors_js_1.DIM}†${colors_js_1.NC} deprecated`);
|
|
446
|
+
if (hasDup)
|
|
447
|
+
legendParts.push(`${colors_js_1.YELLOW}G${colors_js_1.NC} in global`);
|
|
448
|
+
if (legendParts.length > 0) {
|
|
449
|
+
const legendStr = legendParts.join(' ');
|
|
450
|
+
const legendVisLen = visLen(legendStr);
|
|
451
|
+
const padLeft = Math.max(0, w - 4 - legendVisLen);
|
|
452
|
+
lines.push(boxLine(`${' '.repeat(padLeft)}${legendStr}`, w));
|
|
453
|
+
lines.push(boxBottom('[↑↓] navigate [Enter] detail [/] search [q] quit', w));
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
lines.push(boxBottom('[↑↓] navigate [Enter] detail [/] search [q] quit', w));
|
|
457
|
+
}
|
|
248
458
|
process.stdout.write(lines.join('\n') + '\n');
|
|
249
459
|
}
|
|
250
|
-
function
|
|
460
|
+
function buildSearchHits(query, withPerms, results) {
|
|
461
|
+
if (!query)
|
|
462
|
+
return [];
|
|
463
|
+
const q = query.toLowerCase();
|
|
464
|
+
const hits = [];
|
|
465
|
+
for (let pi = 0; pi < withPerms.length; pi++) {
|
|
466
|
+
const entry = withPerms[pi];
|
|
467
|
+
const r = results.find((r) => r.display === entry.display);
|
|
468
|
+
if (!r)
|
|
469
|
+
continue;
|
|
470
|
+
const matched = r.permissions.filter((p) => p.toLowerCase().includes(q));
|
|
471
|
+
if (matched.length === 0)
|
|
472
|
+
continue;
|
|
473
|
+
hits.push({ projectName: entry.shortName, projectIdx: pi, perm: '', rawPerm: '', filePath: r.path, isHeader: true });
|
|
474
|
+
for (const p of matched) {
|
|
475
|
+
hits.push({ projectName: entry.shortName, projectIdx: pi, perm: p, rawPerm: p, filePath: r.path, isHeader: false });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return hits;
|
|
479
|
+
}
|
|
480
|
+
function renderSearch(state, withPerms, results) {
|
|
481
|
+
const rows = process.stdout.rows || 24;
|
|
482
|
+
const cols = process.stdout.columns || 80;
|
|
483
|
+
const w = Math.min(cols, 82);
|
|
484
|
+
const hits = buildSearchHits(state.searchQuery, withPerms, results);
|
|
485
|
+
const permCount = hits.filter((h) => !h.isHeader).length;
|
|
486
|
+
if (state.cursor >= hits.length)
|
|
487
|
+
state.cursor = Math.max(0, hits.length - 1);
|
|
488
|
+
// Skip header rows when navigating
|
|
489
|
+
if (hits[state.cursor]?.isHeader && state.cursor + 1 < hits.length)
|
|
490
|
+
state.cursor++;
|
|
491
|
+
const chrome = 3; // top + bottom + search bar
|
|
492
|
+
const visibleRows = Math.max(1, rows - chrome);
|
|
493
|
+
if (state.cursor < state.scrollOffset)
|
|
494
|
+
state.scrollOffset = state.cursor;
|
|
495
|
+
if (state.cursor >= state.scrollOffset + visibleRows)
|
|
496
|
+
state.scrollOffset = state.cursor - visibleRows + 1;
|
|
497
|
+
const lines = [];
|
|
498
|
+
const scrollInfo = hits.length > visibleRows ? `${state.cursor + 1}/${hits.length}` : '';
|
|
499
|
+
lines.push(boxTop(`search: ${state.searchQuery} ${permCount} permissions`, scrollInfo, w));
|
|
500
|
+
const end = Math.min(state.scrollOffset + visibleRows, hits.length);
|
|
501
|
+
for (let i = state.scrollOffset; i < end; i++) {
|
|
502
|
+
const hit = hits[i];
|
|
503
|
+
const isCursor = i === state.cursor;
|
|
504
|
+
if (hit.isHeader) {
|
|
505
|
+
const tag = withPerms[hit.projectIdx]?.isGlobal ? 'global' : withPerms[hit.projectIdx]?.fileType || '';
|
|
506
|
+
lines.push(boxLine(`${colors_js_1.YELLOW} ${hit.projectName}${colors_js_1.NC} ${colors_js_1.DIM}${tag}${colors_js_1.NC}`, w));
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
const prefix = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
|
|
510
|
+
const clean = cleanLabel(hit.perm);
|
|
511
|
+
const maxLen = w - 8;
|
|
512
|
+
const name = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '…' : clean;
|
|
513
|
+
lines.push(boxLine(`${prefix} ${colors_js_1.DIM}${name}${colors_js_1.NC}`, w));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (hits.length === 0 && state.searchQuery) {
|
|
517
|
+
lines.push(boxLine(`${colors_js_1.DIM} No matches${colors_js_1.NC}`, w));
|
|
518
|
+
}
|
|
519
|
+
if (state.searchActive) {
|
|
520
|
+
lines.push(boxBottom(`/ ${state.searchQuery}█ (${permCount} matches)`, w));
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
lines.push(boxBottom(`[↑↓] navigate [Enter] go to project [/] new search [Esc] back`, w));
|
|
524
|
+
}
|
|
525
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
526
|
+
}
|
|
527
|
+
function renderDetail(state, withPerms, results, dupMap) {
|
|
251
528
|
const rows = process.stdout.rows || 24;
|
|
252
529
|
const cols = process.stdout.columns || 80;
|
|
253
530
|
const w = Math.min(cols, 82);
|
|
@@ -257,33 +534,47 @@ function renderDetail(state, withPerms, results) {
|
|
|
257
534
|
const fileResult = results.find((r) => r.display === project.display);
|
|
258
535
|
if (!fileResult || fileResult.totalCount === 0)
|
|
259
536
|
return;
|
|
537
|
+
// Compute dup info for this file
|
|
538
|
+
const globalPerms = getGlobalPerms(results);
|
|
539
|
+
const dupInfo = project.isGlobal ? { exact: [], globalDup: [] } : (0, scanner_js_1.findDuplicates)(fileResult.path, globalPerms);
|
|
540
|
+
const exactSet = new Set(dupInfo.exact);
|
|
541
|
+
const globalDupSet = new Set(dupInfo.globalDup);
|
|
260
542
|
// build navigable rows
|
|
261
|
-
const
|
|
543
|
+
const allNavRows = [];
|
|
262
544
|
for (const group of fileResult.groups) {
|
|
263
545
|
const key = `${fileResult.path}:${group.category}`;
|
|
264
546
|
const isOpen = state.expanded.has(key);
|
|
265
547
|
const arrow = isOpen ? '▾' : '▸';
|
|
266
|
-
|
|
548
|
+
allNavRows.push({ text: `${colors_js_1.YELLOW}${arrow} ${group.category}${colors_js_1.NC} ${colors_js_1.DIM}(${group.items.length})${colors_js_1.NC}`, key });
|
|
267
549
|
if (isOpen) {
|
|
268
550
|
for (const item of group.items) {
|
|
269
551
|
const clean = cleanLabel(item.name);
|
|
552
|
+
const rawPerm = fileResult.permissions.find((p) => p.includes(item.name)) || '';
|
|
553
|
+
// Dup tag
|
|
554
|
+
let dupTag = '';
|
|
555
|
+
if (globalDupSet.has(rawPerm))
|
|
556
|
+
dupTag = ` ${colors_js_1.YELLOW}(in global)${colors_js_1.NC}`;
|
|
557
|
+
else if (exactSet.has(rawPerm))
|
|
558
|
+
dupTag = ` ${colors_js_1.DIM}(dup)${colors_js_1.NC}`;
|
|
270
559
|
if (state.showInfo) {
|
|
271
560
|
const info = (0, explain_js_1.explain)(group.category, item.name);
|
|
272
561
|
const tag = severityTag(info.risk);
|
|
273
|
-
const tagLen = info.risk.length + 2;
|
|
562
|
+
const tagLen = info.risk.length + 2;
|
|
274
563
|
const nameMax = Math.min(30, w - tagLen - 14);
|
|
275
564
|
const name = clean.length > nameMax ? clean.slice(0, nameMax - 1) + '…' : clean;
|
|
276
565
|
const desc = info.description ? `${colors_js_1.DIM}${info.description}${colors_js_1.NC}` : '';
|
|
277
|
-
|
|
566
|
+
allNavRows.push({ text: ` ${pad(name, nameMax)} ${tag} ${desc}${dupTag}`, perm: item.name, rawPerm });
|
|
278
567
|
}
|
|
279
568
|
else {
|
|
280
|
-
const
|
|
569
|
+
const dupTagVis = dupTag ? 10 : 0; // reserve space for tag
|
|
570
|
+
const maxLen = w - 8 - dupTagVis;
|
|
281
571
|
const name = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '…' : clean;
|
|
282
|
-
|
|
572
|
+
allNavRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}${dupTag}`, perm: item.name, rawPerm });
|
|
283
573
|
}
|
|
284
574
|
}
|
|
285
575
|
}
|
|
286
576
|
}
|
|
577
|
+
const navRows = allNavRows;
|
|
287
578
|
// handle toggle
|
|
288
579
|
if (state._toggle) {
|
|
289
580
|
delete state._toggle;
|
|
@@ -293,14 +584,31 @@ function renderDetail(state, withPerms, results) {
|
|
|
293
584
|
state.expanded.delete(row.key);
|
|
294
585
|
else
|
|
295
586
|
state.expanded.add(row.key);
|
|
296
|
-
renderDetail(state, withPerms, results);
|
|
587
|
+
renderDetail(state, withPerms, results, dupMap);
|
|
297
588
|
return;
|
|
298
589
|
}
|
|
299
590
|
}
|
|
591
|
+
// handle delete
|
|
592
|
+
if (state._delete) {
|
|
593
|
+
delete state._delete;
|
|
594
|
+
const row = navRows[state.detailCursor];
|
|
595
|
+
if (row?.rawPerm) {
|
|
596
|
+
state.confirmDelete = { perm: row.perm, rawPerm: row.rawPerm, filePath: fileResult.path };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// handle global copy
|
|
600
|
+
if (state._global) {
|
|
601
|
+
delete state._global;
|
|
602
|
+
const row = navRows[state.detailCursor];
|
|
603
|
+
if (row?.rawPerm && !project.isGlobal) {
|
|
604
|
+
state.confirmGlobal = { perm: row.perm, rawPerm: row.rawPerm };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
300
607
|
if (state.detailCursor >= navRows.length)
|
|
301
608
|
state.detailCursor = Math.max(0, navRows.length - 1);
|
|
302
|
-
//
|
|
303
|
-
const
|
|
609
|
+
// top(1) + bottom(2 for hint, or 1 for flash/confirm/search)
|
|
610
|
+
const bottomChrome = (!state.flash && !state.confirmDelete && !state.confirmGlobal && !state.searchActive) ? 4 : 3;
|
|
611
|
+
const visibleRows = Math.max(1, rows - bottomChrome);
|
|
304
612
|
if (state.detailCursor < state.detailScroll)
|
|
305
613
|
state.detailScroll = state.detailCursor;
|
|
306
614
|
if (state.detailCursor >= state.detailScroll + visibleRows)
|
|
@@ -309,7 +617,10 @@ function renderDetail(state, withPerms, results) {
|
|
|
309
617
|
const scrollInfo = navRows.length > visibleRows ? `${state.detailCursor + 1}/${navRows.length}` : '';
|
|
310
618
|
const lines = [];
|
|
311
619
|
const typeTag = project.fileType === 'global' ? 'global' : project.fileType;
|
|
312
|
-
|
|
620
|
+
const dupCount = dupMap.get(project.display);
|
|
621
|
+
const globalCount = dupCount?.global || 0;
|
|
622
|
+
const dupSuffix = globalCount > 0 ? ` ${colors_js_1.YELLOW}${globalCount} in global${colors_js_1.NC}` : '';
|
|
623
|
+
lines.push(boxTop(`${project.shortName} (${typeTag}) ${project.totalCount} permissions${dupSuffix}`, scrollInfo, w));
|
|
313
624
|
for (let i = 0; i < visible.length; i++) {
|
|
314
625
|
const globalIdx = state.detailScroll + i;
|
|
315
626
|
const isCursor = globalIdx === state.detailCursor;
|
|
@@ -317,8 +628,28 @@ function renderDetail(state, withPerms, results) {
|
|
|
317
628
|
const prefix = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
|
|
318
629
|
lines.push(boxLine(`${prefix}${row.text}`, w));
|
|
319
630
|
}
|
|
320
|
-
|
|
321
|
-
|
|
631
|
+
if (state.flash) {
|
|
632
|
+
lines.push(boxBottom(state.flash, w));
|
|
633
|
+
state.flash = undefined;
|
|
634
|
+
}
|
|
635
|
+
else if (state.confirmDelete) {
|
|
636
|
+
const name = cleanLabel(state.confirmDelete.perm);
|
|
637
|
+
const truncName = name.length > 30 ? name.slice(0, 29) + '…' : name;
|
|
638
|
+
lines.push(boxBottom(`${colors_js_1.RED}Delete "${truncName}"? [y/N]${colors_js_1.NC}`, w));
|
|
639
|
+
}
|
|
640
|
+
else if (state.confirmGlobal) {
|
|
641
|
+
const name = cleanLabel(state.confirmGlobal.perm);
|
|
642
|
+
const truncName = name.length > 30 ? name.slice(0, 29) + '…' : name;
|
|
643
|
+
lines.push(boxBottom(`${colors_js_1.CYAN}Copy "${truncName}" to global? [y/N]${colors_js_1.NC}`, w));
|
|
644
|
+
}
|
|
645
|
+
else if (state.searchActive) {
|
|
646
|
+
lines.push(boxBottom(`/ ${state.searchQuery}█ (${navRows.length} matches)`, w));
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
const infoHint = state.showInfo ? '[i] hide info' : '[i] info';
|
|
650
|
+
const globalHint = project.isGlobal ? '' : ' [g] +global';
|
|
651
|
+
lines.push(boxBottom2(`${colors_js_1.DIM}[↑↓] navigate [Enter] expand ${infoHint} [d] delete${globalHint}${colors_js_1.NC}`, `[/] search [Esc] back [q] quit`, w));
|
|
652
|
+
}
|
|
322
653
|
process.stdout.write(lines.join('\n') + '\n');
|
|
323
654
|
}
|
|
324
655
|
function pad(s, n) {
|
package/dist/scanner.js
CHANGED
|
@@ -3,6 +3,9 @@ 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;
|
|
8
|
+
exports.findDuplicates = findDuplicates;
|
|
6
9
|
exports.countDeprecated = countDeprecated;
|
|
7
10
|
exports.findSettingsFiles = findSettingsFiles;
|
|
8
11
|
exports.scanFile = scanFile;
|
|
@@ -11,6 +14,104 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
11
14
|
const node_os_1 = __importDefault(require("node:os"));
|
|
12
15
|
const PERM_RE = /"(Bash|Write|Edit|Read|Glob|Grep|WebSearch|WebFetch|mcp_)[^"]*"/g;
|
|
13
16
|
const DEPRECATED_RE = /:\*\)|:\*"/g;
|
|
17
|
+
const AUDIT_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.ccperm', 'audit');
|
|
18
|
+
function writeAudit(action, filePath, perm, before, after) {
|
|
19
|
+
try {
|
|
20
|
+
node_fs_1.default.mkdirSync(AUDIT_DIR, { recursive: true });
|
|
21
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
22
|
+
const entry = { action, file: filePath, perm, before, after, timestamp: new Date().toISOString() };
|
|
23
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(AUDIT_DIR, `${ts}_${action}.json`), JSON.stringify(entry, null, 2) + '\n');
|
|
24
|
+
}
|
|
25
|
+
catch { /* audit is best-effort */ }
|
|
26
|
+
}
|
|
27
|
+
function removePerm(filePath, rawPerm) {
|
|
28
|
+
let content;
|
|
29
|
+
try {
|
|
30
|
+
content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
let json;
|
|
36
|
+
try {
|
|
37
|
+
json = JSON.parse(content);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const allow = json?.permissions?.allow;
|
|
43
|
+
if (!Array.isArray(allow))
|
|
44
|
+
return false;
|
|
45
|
+
const idx = allow.indexOf(rawPerm);
|
|
46
|
+
if (idx === -1)
|
|
47
|
+
return false;
|
|
48
|
+
const before = [...allow];
|
|
49
|
+
allow.splice(idx, 1);
|
|
50
|
+
node_fs_1.default.writeFileSync(filePath, JSON.stringify(json, null, 2) + '\n', 'utf8');
|
|
51
|
+
writeAudit('DELETE', filePath, rawPerm, before, allow);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function addPermToGlobal(rawPerm) {
|
|
55
|
+
const globalPath = node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'settings.json');
|
|
56
|
+
let content;
|
|
57
|
+
try {
|
|
58
|
+
content = node_fs_1.default.readFileSync(globalPath, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
content = '{}';
|
|
62
|
+
}
|
|
63
|
+
let json;
|
|
64
|
+
try {
|
|
65
|
+
json = JSON.parse(content);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (!json.permissions)
|
|
71
|
+
json.permissions = {};
|
|
72
|
+
if (!Array.isArray(json.permissions.allow))
|
|
73
|
+
json.permissions.allow = [];
|
|
74
|
+
const allow = json.permissions.allow;
|
|
75
|
+
if (allow.includes(rawPerm))
|
|
76
|
+
return false; // already exists
|
|
77
|
+
const before = [...allow];
|
|
78
|
+
allow.push(rawPerm);
|
|
79
|
+
node_fs_1.default.writeFileSync(globalPath, JSON.stringify(json, null, 2) + '\n', 'utf8');
|
|
80
|
+
writeAudit('COPY_TO_GLOBAL', globalPath, rawPerm, before, allow);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
function findDuplicates(filePath, globalPerms) {
|
|
84
|
+
let content;
|
|
85
|
+
try {
|
|
86
|
+
content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return { exact: [], globalDup: [] };
|
|
90
|
+
}
|
|
91
|
+
let json;
|
|
92
|
+
try {
|
|
93
|
+
json = JSON.parse(content);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { exact: [], globalDup: [] };
|
|
97
|
+
}
|
|
98
|
+
const allow = json?.permissions?.allow;
|
|
99
|
+
if (!Array.isArray(allow))
|
|
100
|
+
return { exact: [], globalDup: [] };
|
|
101
|
+
const exact = [];
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
for (const p of allow) {
|
|
104
|
+
if (seen.has(p)) {
|
|
105
|
+
if (!exact.includes(p))
|
|
106
|
+
exact.push(p);
|
|
107
|
+
}
|
|
108
|
+
else
|
|
109
|
+
seen.add(p);
|
|
110
|
+
}
|
|
111
|
+
const globalSet = new Set(globalPerms);
|
|
112
|
+
const globalDup = [...new Set(allow)].filter((p) => globalSet.has(p));
|
|
113
|
+
return { exact, globalDup };
|
|
114
|
+
}
|
|
14
115
|
function countDeprecated(results) {
|
|
15
116
|
const out = [];
|
|
16
117
|
for (const r of results) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccperm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "Audit Claude Code permissions across all your projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ccperm": "bin/ccperm.js"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"bin/ccperm.js",
|
|
26
26
|
"dist",
|
|
27
|
-
"
|
|
27
|
+
"demo.gif"
|
|
28
28
|
],
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^25.3.0",
|
package/screenshot.png
DELETED
|
Binary file
|