ccperm 1.5.1 → 1.7.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/aggregator.js +2 -1
- package/dist/interactive.js +68 -50
- package/dist/renderer.js +15 -5
- package/dist/scanner.js +2 -1
- package/package.json +1 -1
package/dist/aggregator.js
CHANGED
|
@@ -18,7 +18,8 @@ function mergeByProject(results) {
|
|
|
18
18
|
const dir = projectDir(r.display);
|
|
19
19
|
let merged = map.get(dir);
|
|
20
20
|
if (!merged) {
|
|
21
|
-
|
|
21
|
+
const name = r.isGlobal ? 'GLOBAL' : shortPath(r.display);
|
|
22
|
+
merged = { display: r.display, shortName: name, totalCount: 0, groups: new Map(), isGlobal: r.isGlobal };
|
|
22
23
|
map.set(dir, merged);
|
|
23
24
|
}
|
|
24
25
|
merged.totalCount += r.totalCount;
|
package/dist/interactive.js
CHANGED
|
@@ -6,9 +6,36 @@ 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
|
+
// strip ANSI escape codes for visible length
|
|
10
|
+
function visLen(s) {
|
|
11
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
12
|
+
}
|
|
13
|
+
function boxLine(text, width) {
|
|
14
|
+
const vis = visLen(text);
|
|
15
|
+
const padRight = Math.max(0, width - vis - 1);
|
|
16
|
+
return `${colors_js_1.DIM}│${colors_js_1.NC} ${text}${' '.repeat(padRight)}${colors_js_1.DIM}│${colors_js_1.NC}`;
|
|
17
|
+
}
|
|
18
|
+
function boxTop(title, info, width) {
|
|
19
|
+
const inner = width - 2;
|
|
20
|
+
const titlePart = ` ${title} `;
|
|
21
|
+
const infoPart = info ? ` ${info} ` : '';
|
|
22
|
+
const fill = Math.max(0, inner - titlePart.length - infoPart.length);
|
|
23
|
+
return `${colors_js_1.DIM}┌${titlePart}${'─'.repeat(fill)}${infoPart}┐${colors_js_1.NC}`;
|
|
24
|
+
}
|
|
25
|
+
function boxBottom(hint, width) {
|
|
26
|
+
const inner = width - 2;
|
|
27
|
+
const hintPart = ` ${hint} `;
|
|
28
|
+
const fill = Math.max(0, inner - hintPart.length);
|
|
29
|
+
return `${colors_js_1.DIM}└${'─'.repeat(fill)}${hintPart}┘${colors_js_1.NC}`;
|
|
30
|
+
}
|
|
31
|
+
function boxSep(width) {
|
|
32
|
+
return `${colors_js_1.DIM}├${'─'.repeat(width - 2)}┤${colors_js_1.NC}`;
|
|
33
|
+
}
|
|
9
34
|
function startInteractive(merged, results) {
|
|
10
35
|
return new Promise((resolve) => {
|
|
11
|
-
const
|
|
36
|
+
const globals = merged.filter((r) => r.totalCount > 0 && r.isGlobal).sort((a, b) => b.totalCount - a.totalCount);
|
|
37
|
+
const projects = merged.filter((r) => r.totalCount > 0 && !r.isGlobal).sort((a, b) => b.totalCount - a.totalCount);
|
|
38
|
+
const withPerms = [...globals, ...projects];
|
|
12
39
|
const emptyCount = merged.filter((r) => r.totalCount === 0).length;
|
|
13
40
|
if (withPerms.length === 0) {
|
|
14
41
|
console.log(`\n ${colors_js_1.GREEN}No projects with permissions found.${colors_js_1.NC}\n`);
|
|
@@ -26,16 +53,11 @@ function startInteractive(merged, results) {
|
|
|
26
53
|
process.stdin.pause();
|
|
27
54
|
process.stdin.removeListener('keypress', onKey);
|
|
28
55
|
process.removeListener('SIGINT', onSigint);
|
|
29
|
-
// show cursor
|
|
30
56
|
process.stdout.write('\x1b[?25h');
|
|
31
57
|
};
|
|
32
|
-
const onSigint = () => {
|
|
33
|
-
cleanup();
|
|
34
|
-
process.exit(0);
|
|
35
|
-
};
|
|
58
|
+
const onSigint = () => { cleanup(); process.exit(0); };
|
|
36
59
|
process.on('SIGINT', onSigint);
|
|
37
60
|
const render = () => {
|
|
38
|
-
// clear screen + move cursor home + hide cursor
|
|
39
61
|
process.stdout.write('\x1b[2J\x1b[H\x1b[?25l');
|
|
40
62
|
if (state.view === 'list')
|
|
41
63
|
renderList(state, withPerms, emptyCount);
|
|
@@ -52,12 +74,10 @@ function startInteractive(merged, results) {
|
|
|
52
74
|
return;
|
|
53
75
|
}
|
|
54
76
|
if (state.view === 'list') {
|
|
55
|
-
if (key.name === 'up')
|
|
77
|
+
if (key.name === 'up')
|
|
56
78
|
state.cursor = Math.max(0, state.cursor - 1);
|
|
57
|
-
|
|
58
|
-
else if (key.name === 'down') {
|
|
79
|
+
else if (key.name === 'down')
|
|
59
80
|
state.cursor = Math.min(withPerms.length - 1, state.cursor + 1);
|
|
60
|
-
}
|
|
61
81
|
else if (key.name === 'return') {
|
|
62
82
|
state.selectedProject = state.cursor;
|
|
63
83
|
state.detailCursor = 0;
|
|
@@ -67,7 +87,6 @@ function startInteractive(merged, results) {
|
|
|
67
87
|
}
|
|
68
88
|
}
|
|
69
89
|
else {
|
|
70
|
-
// detail view
|
|
71
90
|
if (key.name === 'escape' || key.name === 'backspace') {
|
|
72
91
|
state.view = 'list';
|
|
73
92
|
state.detailCursor = 0;
|
|
@@ -80,7 +99,6 @@ function startInteractive(merged, results) {
|
|
|
80
99
|
state.detailCursor++;
|
|
81
100
|
}
|
|
82
101
|
else if (key.name === 'return') {
|
|
83
|
-
// toggle handled in renderDetail via detailRows
|
|
84
102
|
state._toggle = true;
|
|
85
103
|
}
|
|
86
104
|
}
|
|
@@ -93,30 +111,35 @@ function startInteractive(merged, results) {
|
|
|
93
111
|
function renderList(state, withPerms, emptyCount) {
|
|
94
112
|
const rows = process.stdout.rows || 24;
|
|
95
113
|
const cols = process.stdout.columns || 80;
|
|
114
|
+
const w = Math.min(cols, 82);
|
|
115
|
+
const inner = w - 4; // box border + 1 space each side
|
|
96
116
|
const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
|
|
97
117
|
const catsPresent = cats.filter((c) => withPerms.some((r) => r.groups.has(c)));
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
const catColWidth = catsPresent.length * 7;
|
|
119
|
+
const nameWidths = withPerms.map((r) => r.isGlobal ? r.shortName.length + 2 : r.shortName.length);
|
|
120
|
+
const nameWidth = Math.min(Math.max(...nameWidths, 7), inner - catColWidth - 8);
|
|
121
|
+
const hasGlobalSep = withPerms.some((r) => r.isGlobal) && withPerms.some((r) => !r.isGlobal);
|
|
122
|
+
// box takes: top(1) + header(2) + sep(1) + content + globalSep?(1) + emptyLine?(1) + bottom(1)
|
|
123
|
+
const chrome = 5 + (hasGlobalSep ? 1 : 0) + (emptyCount > 0 ? 1 : 0);
|
|
124
|
+
const visibleRows = Math.min(25, Math.max(1, rows - chrome));
|
|
104
125
|
if (state.cursor < state.scrollOffset)
|
|
105
126
|
state.scrollOffset = state.cursor;
|
|
106
127
|
if (state.cursor >= state.scrollOffset + visibleRows)
|
|
107
128
|
state.scrollOffset = state.cursor - visibleRows + 1;
|
|
129
|
+
const scrollInfo = withPerms.length > visibleRows ? `${state.cursor + 1}/${withPerms.length}` : '';
|
|
108
130
|
const lines = [];
|
|
109
|
-
|
|
110
|
-
lines.push(
|
|
111
|
-
lines.push(
|
|
112
|
-
|
|
131
|
+
lines.push(boxTop('ccperm', scrollInfo, w));
|
|
132
|
+
lines.push(boxLine(`${colors_js_1.DIM}${pad('PROJECT', nameWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}`, w));
|
|
133
|
+
lines.push(boxSep(w));
|
|
134
|
+
const globalCount = withPerms.filter((r) => r.isGlobal).length;
|
|
113
135
|
const end = Math.min(state.scrollOffset + visibleRows, withPerms.length);
|
|
114
136
|
for (let i = state.scrollOffset; i < end; i++) {
|
|
115
137
|
const r = withPerms[i];
|
|
116
138
|
const isCursor = i === state.cursor;
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
139
|
+
const displayName = r.isGlobal ? `★ ${r.shortName}` : r.shortName;
|
|
140
|
+
const truncName = displayName.length > nameWidth ? displayName.slice(0, nameWidth - 1) + '…' : displayName;
|
|
141
|
+
const marker = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
|
|
142
|
+
const nameStyle = isCursor ? `${colors_js_1.BOLD}` : r.isGlobal ? `${colors_js_1.YELLOW}` : `${colors_js_1.DIM}`;
|
|
120
143
|
const nameCol = `${marker}${nameStyle}${pad(truncName, nameWidth)}${colors_js_1.NC}`;
|
|
121
144
|
const catCols = catsPresent.map((c) => {
|
|
122
145
|
const count = r.groups.get(c) || 0;
|
|
@@ -125,18 +148,22 @@ function renderList(state, withPerms, emptyCount) {
|
|
|
125
148
|
return `${colors_js_1.DIM}${rpad('·', 5)}${colors_js_1.NC}`;
|
|
126
149
|
}).join(' ');
|
|
127
150
|
const totalCol = isCursor ? `${colors_js_1.BOLD}${rpad(r.totalCount, 5)}${colors_js_1.NC}` : rpad(r.totalCount, 5);
|
|
128
|
-
lines.push(`${nameCol} ${catCols} ${totalCol}
|
|
151
|
+
lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}`, w));
|
|
152
|
+
// separator after global section
|
|
153
|
+
if (r.isGlobal && i + 1 < withPerms.length && !withPerms[i + 1].isGlobal) {
|
|
154
|
+
lines.push(boxSep(w));
|
|
155
|
+
}
|
|
129
156
|
}
|
|
130
157
|
if (emptyCount > 0) {
|
|
131
|
-
lines.push(
|
|
158
|
+
lines.push(boxLine(`${colors_js_1.DIM}+ ${emptyCount} projects with no permissions${colors_js_1.NC}`, w));
|
|
132
159
|
}
|
|
133
|
-
lines.push('');
|
|
134
|
-
lines.push(` ${colors_js_1.DIM}[↑↓] navigate [Enter] detail [q] quit${colors_js_1.NC}`);
|
|
160
|
+
lines.push(boxBottom('[↑↓] navigate [Enter] detail [q] quit', w));
|
|
135
161
|
process.stdout.write(lines.join('\n') + '\n');
|
|
136
162
|
}
|
|
137
163
|
function renderDetail(state, withPerms, results) {
|
|
138
164
|
const rows = process.stdout.rows || 24;
|
|
139
165
|
const cols = process.stdout.columns || 80;
|
|
166
|
+
const w = Math.min(cols, 82);
|
|
140
167
|
const project = withPerms[state.selectedProject];
|
|
141
168
|
if (!project)
|
|
142
169
|
return;
|
|
@@ -153,17 +180,17 @@ function renderDetail(state, withPerms, results) {
|
|
|
153
180
|
if (result.totalCount === 0)
|
|
154
181
|
continue;
|
|
155
182
|
const fileName = result.display.replace(/.*\/\.claude\//, '');
|
|
156
|
-
navRows.push({
|
|
183
|
+
navRows.push({ text: `${colors_js_1.CYAN}${fileName}${colors_js_1.NC} ${colors_js_1.DIM}(${result.totalCount})${colors_js_1.NC}` });
|
|
157
184
|
for (const group of result.groups) {
|
|
158
185
|
const key = `${result.path}:${group.category}`;
|
|
159
186
|
const isOpen = state.expanded.has(key);
|
|
160
187
|
const arrow = isOpen ? '▾' : '▸';
|
|
161
|
-
navRows.push({
|
|
188
|
+
navRows.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 });
|
|
162
189
|
if (isOpen) {
|
|
163
|
-
const maxLen =
|
|
190
|
+
const maxLen = w - 12;
|
|
164
191
|
for (const item of group.items) {
|
|
165
192
|
const name = item.name.length > maxLen ? item.name.slice(0, maxLen - 1) + '…' : item.name;
|
|
166
|
-
navRows.push({
|
|
193
|
+
navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}` });
|
|
167
194
|
}
|
|
168
195
|
}
|
|
169
196
|
}
|
|
@@ -177,39 +204,30 @@ function renderDetail(state, withPerms, results) {
|
|
|
177
204
|
state.expanded.delete(row.key);
|
|
178
205
|
else
|
|
179
206
|
state.expanded.add(row.key);
|
|
180
|
-
// re-render needed — will happen on next render() call
|
|
181
207
|
renderDetail(state, withPerms, results);
|
|
182
208
|
return;
|
|
183
209
|
}
|
|
184
210
|
}
|
|
185
|
-
// clamp cursor
|
|
186
211
|
if (state.detailCursor >= navRows.length)
|
|
187
212
|
state.detailCursor = Math.max(0, navRows.length - 1);
|
|
188
|
-
//
|
|
189
|
-
const
|
|
190
|
-
const footerLines = 2;
|
|
191
|
-
const visibleRows = Math.max(1, rows - headerLines - footerLines);
|
|
213
|
+
// box chrome: top(1) + sep(1) + bottom(1) = 3
|
|
214
|
+
const visibleRows = Math.max(1, rows - 3);
|
|
192
215
|
if (state.detailCursor < state.detailScroll)
|
|
193
216
|
state.detailScroll = state.detailCursor;
|
|
194
217
|
if (state.detailCursor >= state.detailScroll + visibleRows)
|
|
195
218
|
state.detailScroll = state.detailCursor - visibleRows + 1;
|
|
196
219
|
const visible = navRows.slice(state.detailScroll, state.detailScroll + visibleRows);
|
|
220
|
+
const scrollInfo = navRows.length > visibleRows ? `${state.detailCursor + 1}/${navRows.length}` : '';
|
|
197
221
|
const lines = [];
|
|
198
|
-
lines.push(
|
|
199
|
-
lines.push('');
|
|
222
|
+
lines.push(boxTop(`${project.shortName} (${project.totalCount})`, scrollInfo, w));
|
|
200
223
|
for (let i = 0; i < visible.length; i++) {
|
|
201
224
|
const globalIdx = state.detailScroll + i;
|
|
202
225
|
const isCursor = globalIdx === state.detailCursor;
|
|
203
226
|
const row = visible[i];
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
lines.push(row.line);
|
|
209
|
-
}
|
|
227
|
+
const prefix = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
|
|
228
|
+
lines.push(boxLine(`${prefix}${row.text}`, w));
|
|
210
229
|
}
|
|
211
|
-
lines.push('');
|
|
212
|
-
lines.push(` ${colors_js_1.DIM}[↑↓] navigate [Enter] expand/collapse [Esc] back [q] quit${colors_js_1.NC}`);
|
|
230
|
+
lines.push(boxBottom('[↑↓] navigate [Enter] expand [Esc] back [q] quit', w));
|
|
213
231
|
process.stdout.write(lines.join('\n') + '\n');
|
|
214
232
|
}
|
|
215
233
|
function pad(s, n) {
|
package/dist/renderer.js
CHANGED
|
@@ -14,23 +14,33 @@ function rpad(s, n) {
|
|
|
14
14
|
function printCompact(merged, summary) {
|
|
15
15
|
const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
|
|
16
16
|
const catsPresent = cats.filter((c) => merged.some((r) => r.groups.has(c)));
|
|
17
|
-
const
|
|
17
|
+
const globals = merged.filter((r) => r.totalCount > 0 && r.isGlobal).sort((a, b) => b.totalCount - a.totalCount);
|
|
18
|
+
const projects = merged.filter((r) => r.totalCount > 0 && !r.isGlobal).sort((a, b) => b.totalCount - a.totalCount);
|
|
19
|
+
const withPerms = [...globals, ...projects];
|
|
18
20
|
const emptyCount = merged.filter((r) => r.totalCount === 0).length;
|
|
19
21
|
// header
|
|
20
|
-
const
|
|
22
|
+
const nameWidths = withPerms.map((r) => r.isGlobal ? r.shortName.length + 2 : r.shortName.length);
|
|
23
|
+
const nameWidth = Math.min(Math.max(...nameWidths, 7), 40);
|
|
21
24
|
const header = ` ${colors_js_1.DIM}${pad('PROJECT', nameWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}`;
|
|
22
25
|
console.log(header);
|
|
23
26
|
console.log(` ${colors_js_1.DIM}${'─'.repeat(nameWidth + catsPresent.length * 7 + 8)}${colors_js_1.NC}`);
|
|
24
27
|
// rows
|
|
25
|
-
for (
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
+
for (let i = 0; i < withPerms.length; i++) {
|
|
29
|
+
const result = withPerms[i];
|
|
30
|
+
const displayName = result.isGlobal ? `★ ${result.shortName}` : result.shortName;
|
|
31
|
+
const truncName = displayName.length > nameWidth ? displayName.slice(0, nameWidth - 1) + '…' : displayName;
|
|
32
|
+
const nameStyle = result.isGlobal ? `${colors_js_1.YELLOW}` : `${colors_js_1.DIM}`;
|
|
33
|
+
const nameCol = ` ${nameStyle}${pad(truncName, nameWidth)}${colors_js_1.NC}`;
|
|
28
34
|
const catCols = catsPresent.map((c) => {
|
|
29
35
|
const count = result.groups.get(c) || 0;
|
|
30
36
|
return count > 0 ? rpad(count, 5) : `${colors_js_1.DIM}${rpad('·', 5)}${colors_js_1.NC}`;
|
|
31
37
|
}).join(' ');
|
|
32
38
|
const totalCol = rpad(result.totalCount, 5);
|
|
33
39
|
console.log(`${nameCol} ${catCols} ${colors_js_1.BOLD}${totalCol}${colors_js_1.NC}`);
|
|
40
|
+
// separator after global section
|
|
41
|
+
if (result.isGlobal && i + 1 < withPerms.length && !withPerms[i + 1].isGlobal) {
|
|
42
|
+
console.log(` ${colors_js_1.DIM}${'─'.repeat(nameWidth + catsPresent.length * 7 + 8)}${colors_js_1.NC}`);
|
|
43
|
+
}
|
|
34
44
|
}
|
|
35
45
|
if (emptyCount > 0) {
|
|
36
46
|
console.log(`\n ${colors_js_1.DIM}+ ${emptyCount} projects with no permissions${colors_js_1.NC}`);
|
package/dist/scanner.js
CHANGED
|
@@ -93,6 +93,7 @@ async function findSettingsFiles(searchDir, onProgress, debug = false) {
|
|
|
93
93
|
function scanFile(filePath) {
|
|
94
94
|
const home = node_os_1.default.homedir();
|
|
95
95
|
const display = filePath.startsWith(home) ? '~' + filePath.slice(home.length) : filePath;
|
|
96
|
+
const isGlobal = node_path_1.default.dirname(node_path_1.default.dirname(filePath)) === home;
|
|
96
97
|
let content;
|
|
97
98
|
try {
|
|
98
99
|
content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
@@ -103,7 +104,7 @@ function scanFile(filePath) {
|
|
|
103
104
|
const perms = [...new Set((content.match(PERM_RE) || []).map((s) => s.slice(1, -1)))].sort();
|
|
104
105
|
const groups = groupPermissions(perms);
|
|
105
106
|
const totalCount = perms.length;
|
|
106
|
-
return { path: filePath, display, permissions: perms, groups, totalCount };
|
|
107
|
+
return { path: filePath, display, permissions: perms, groups, totalCount, isGlobal };
|
|
107
108
|
}
|
|
108
109
|
function categorize(perm) {
|
|
109
110
|
if (perm.startsWith('Bash')) {
|