ccperm 1.6.0 → 1.8.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 +11 -16
- package/dist/cli.js +3 -3
- package/dist/interactive.js +33 -31
- package/dist/renderer.js +20 -9
- package/dist/scanner.js +2 -1
- package/package.json +1 -1
package/dist/aggregator.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.shortPath = shortPath;
|
|
4
4
|
exports.projectDir = projectDir;
|
|
5
|
-
exports.
|
|
5
|
+
exports.toFileEntries = toFileEntries;
|
|
6
6
|
exports.summarize = summarize;
|
|
7
7
|
function shortPath(display) {
|
|
8
8
|
const m = display.match(/\/([^/]+)\/\.claude\//);
|
|
@@ -12,24 +12,19 @@ function projectDir(display) {
|
|
|
12
12
|
const idx = display.indexOf('/.claude/');
|
|
13
13
|
return idx >= 0 ? display.slice(0, idx) : display;
|
|
14
14
|
}
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const dir = projectDir(r.display);
|
|
19
|
-
let merged = map.get(dir);
|
|
20
|
-
if (!merged) {
|
|
21
|
-
merged = { display: r.display, shortName: shortPath(r.display), totalCount: 0, groups: new Map() };
|
|
22
|
-
map.set(dir, merged);
|
|
23
|
-
}
|
|
24
|
-
merged.totalCount += r.totalCount;
|
|
15
|
+
function toFileEntries(results) {
|
|
16
|
+
return results.map((r) => {
|
|
17
|
+
const groups = new Map();
|
|
25
18
|
for (const g of r.groups) {
|
|
26
|
-
|
|
19
|
+
groups.set(g.category, g.items.length);
|
|
27
20
|
}
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
const fileType = r.isGlobal ? 'global' : r.display.includes('settings.local.json') ? 'local' : 'shared';
|
|
22
|
+
const name = r.isGlobal ? 'GLOBAL' : shortPath(r.display);
|
|
23
|
+
return { display: r.display, shortName: name, totalCount: r.totalCount, groups, isGlobal: r.isGlobal, fileType };
|
|
24
|
+
});
|
|
30
25
|
}
|
|
31
26
|
function summarize(results) {
|
|
32
|
-
const
|
|
27
|
+
const dirs = new Set(results.map((r) => projectDir(r.display)));
|
|
33
28
|
const categoryTotals = new Map();
|
|
34
29
|
for (const r of results) {
|
|
35
30
|
for (const group of r.groups) {
|
|
@@ -40,7 +35,7 @@ function summarize(results) {
|
|
|
40
35
|
const projectsEmpty = results.filter((r) => r.totalCount === 0).length;
|
|
41
36
|
const totalPerms = [...categoryTotals.values()].reduce((a, b) => a + b, 0);
|
|
42
37
|
return {
|
|
43
|
-
totalProjects:
|
|
38
|
+
totalProjects: dirs.size,
|
|
44
39
|
projectsWithPerms,
|
|
45
40
|
projectsEmpty,
|
|
46
41
|
totalPerms,
|
package/dist/cli.js
CHANGED
|
@@ -88,17 +88,17 @@ async function main() {
|
|
|
88
88
|
}
|
|
89
89
|
console.log(` ${colors_js_1.GREEN}✔${colors_js_1.NC} Found ${colors_js_1.CYAN}${files.length}${colors_js_1.NC} settings files\n`);
|
|
90
90
|
const results = files.map(scanner_js_1.scanFile).filter((r) => r !== null);
|
|
91
|
-
const
|
|
91
|
+
const entries = (0, aggregator_js_1.toFileEntries)(results);
|
|
92
92
|
const summary = (0, aggregator_js_1.summarize)(results);
|
|
93
93
|
if (!isStatic) {
|
|
94
|
-
await (0, interactive_js_1.startInteractive)(
|
|
94
|
+
await (0, interactive_js_1.startInteractive)(entries, results);
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
if (isVerbose) {
|
|
98
98
|
(0, renderer_js_1.printVerbose)(results, summary);
|
|
99
99
|
}
|
|
100
100
|
else {
|
|
101
|
-
(0, renderer_js_1.printCompact)(
|
|
101
|
+
(0, renderer_js_1.printCompact)(entries, summary);
|
|
102
102
|
}
|
|
103
103
|
(0, renderer_js_1.printFooter)(summary);
|
|
104
104
|
if (args.includes('--fix')) {
|
package/dist/interactive.js
CHANGED
|
@@ -33,7 +33,9 @@ function boxSep(width) {
|
|
|
33
33
|
}
|
|
34
34
|
function startInteractive(merged, results) {
|
|
35
35
|
return new Promise((resolve) => {
|
|
36
|
-
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];
|
|
37
39
|
const emptyCount = merged.filter((r) => r.totalCount === 0).length;
|
|
38
40
|
if (withPerms.length === 0) {
|
|
39
41
|
console.log(`\n ${colors_js_1.GREEN}No projects with permissions found.${colors_js_1.NC}\n`);
|
|
@@ -114,10 +116,12 @@ function renderList(state, withPerms, emptyCount) {
|
|
|
114
116
|
const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
|
|
115
117
|
const catsPresent = cats.filter((c) => withPerms.some((r) => r.groups.has(c)));
|
|
116
118
|
const catColWidth = catsPresent.length * 7;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
119
|
+
const nameWidths = withPerms.map((r) => r.shortName.length + 2);
|
|
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));
|
|
121
125
|
if (state.cursor < state.scrollOffset)
|
|
122
126
|
state.scrollOffset = state.cursor;
|
|
123
127
|
if (state.cursor >= state.scrollOffset + visibleRows)
|
|
@@ -127,13 +131,16 @@ function renderList(state, withPerms, emptyCount) {
|
|
|
127
131
|
lines.push(boxTop('ccperm', scrollInfo, w));
|
|
128
132
|
lines.push(boxLine(`${colors_js_1.DIM}${pad('PROJECT', nameWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}`, w));
|
|
129
133
|
lines.push(boxSep(w));
|
|
134
|
+
const globalCount = withPerms.filter((r) => r.isGlobal).length;
|
|
130
135
|
const end = Math.min(state.scrollOffset + visibleRows, withPerms.length);
|
|
131
136
|
for (let i = state.scrollOffset; i < end; i++) {
|
|
132
137
|
const r = withPerms[i];
|
|
133
138
|
const isCursor = i === state.cursor;
|
|
134
|
-
const
|
|
139
|
+
const typeLabel = r.isGlobal ? '' : r.fileType === 'local' ? ' ˡ' : ' ˢ';
|
|
140
|
+
const displayName = r.isGlobal ? `★ ${r.shortName}` : `${r.shortName}${typeLabel}`;
|
|
141
|
+
const truncName = displayName.length > nameWidth ? displayName.slice(0, nameWidth - 1) + '…' : displayName;
|
|
135
142
|
const marker = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
|
|
136
|
-
const nameStyle = isCursor ? `${colors_js_1.BOLD}` : `${colors_js_1.DIM}`;
|
|
143
|
+
const nameStyle = isCursor ? `${colors_js_1.BOLD}` : r.isGlobal ? `${colors_js_1.YELLOW}` : `${colors_js_1.DIM}`;
|
|
137
144
|
const nameCol = `${marker}${nameStyle}${pad(truncName, nameWidth)}${colors_js_1.NC}`;
|
|
138
145
|
const catCols = catsPresent.map((c) => {
|
|
139
146
|
const count = r.groups.get(c) || 0;
|
|
@@ -143,6 +150,10 @@ function renderList(state, withPerms, emptyCount) {
|
|
|
143
150
|
}).join(' ');
|
|
144
151
|
const totalCol = isCursor ? `${colors_js_1.BOLD}${rpad(r.totalCount, 5)}${colors_js_1.NC}` : rpad(r.totalCount, 5);
|
|
145
152
|
lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}`, w));
|
|
153
|
+
// separator after global section
|
|
154
|
+
if (r.isGlobal && i + 1 < withPerms.length && !withPerms[i + 1].isGlobal) {
|
|
155
|
+
lines.push(boxSep(w));
|
|
156
|
+
}
|
|
146
157
|
}
|
|
147
158
|
if (emptyCount > 0) {
|
|
148
159
|
lines.push(boxLine(`${colors_js_1.DIM}+ ${emptyCount} projects with no permissions${colors_js_1.NC}`, w));
|
|
@@ -157,31 +168,21 @@ function renderDetail(state, withPerms, results) {
|
|
|
157
168
|
const project = withPerms[state.selectedProject];
|
|
158
169
|
if (!project)
|
|
159
170
|
return;
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const projIdx = project.display.indexOf('/.claude/');
|
|
164
|
-
const projDir = projIdx >= 0 ? project.display.slice(0, projIdx) : project.display;
|
|
165
|
-
return dir === projDir;
|
|
166
|
-
});
|
|
171
|
+
const fileResult = results.find((r) => r.display === project.display);
|
|
172
|
+
if (!fileResult || fileResult.totalCount === 0)
|
|
173
|
+
return;
|
|
167
174
|
// build navigable rows
|
|
168
175
|
const navRows = [];
|
|
169
|
-
for (const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
navRows.push({ text: `${colors_js_1.
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (isOpen) {
|
|
180
|
-
const maxLen = w - 12;
|
|
181
|
-
for (const item of group.items) {
|
|
182
|
-
const name = item.name.length > maxLen ? item.name.slice(0, maxLen - 1) + '…' : item.name;
|
|
183
|
-
navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}` });
|
|
184
|
-
}
|
|
176
|
+
for (const group of fileResult.groups) {
|
|
177
|
+
const key = `${fileResult.path}:${group.category}`;
|
|
178
|
+
const isOpen = state.expanded.has(key);
|
|
179
|
+
const arrow = isOpen ? '▾' : '▸';
|
|
180
|
+
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 });
|
|
181
|
+
if (isOpen) {
|
|
182
|
+
const maxLen = w - 12;
|
|
183
|
+
for (const item of group.items) {
|
|
184
|
+
const name = item.name.length > maxLen ? item.name.slice(0, maxLen - 1) + '…' : item.name;
|
|
185
|
+
navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}` });
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
}
|
|
@@ -209,7 +210,8 @@ function renderDetail(state, withPerms, results) {
|
|
|
209
210
|
const visible = navRows.slice(state.detailScroll, state.detailScroll + visibleRows);
|
|
210
211
|
const scrollInfo = navRows.length > visibleRows ? `${state.detailCursor + 1}/${navRows.length}` : '';
|
|
211
212
|
const lines = [];
|
|
212
|
-
|
|
213
|
+
const typeTag = project.fileType === 'global' ? 'global' : project.fileType;
|
|
214
|
+
lines.push(boxTop(`${project.shortName} (${typeTag}) ${project.totalCount} permissions`, scrollInfo, w));
|
|
213
215
|
for (let i = 0; i < visible.length; i++) {
|
|
214
216
|
const globalIdx = state.detailScroll + i;
|
|
215
217
|
const isCursor = globalIdx === state.detailCursor;
|
package/dist/renderer.js
CHANGED
|
@@ -11,26 +11,37 @@ function rpad(s, n) {
|
|
|
11
11
|
const str = String(s);
|
|
12
12
|
return str.length >= n ? str : ' '.repeat(n - str.length) + str;
|
|
13
13
|
}
|
|
14
|
-
function printCompact(
|
|
14
|
+
function printCompact(entries, summary) {
|
|
15
15
|
const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
|
|
16
|
-
const catsPresent = cats.filter((c) =>
|
|
17
|
-
const
|
|
18
|
-
const
|
|
16
|
+
const catsPresent = cats.filter((c) => entries.some((r) => r.groups.has(c)));
|
|
17
|
+
const globals = entries.filter((r) => r.totalCount > 0 && r.isGlobal).sort((a, b) => b.totalCount - a.totalCount);
|
|
18
|
+
const projects = entries.filter((r) => r.totalCount > 0 && !r.isGlobal).sort((a, b) => b.totalCount - a.totalCount);
|
|
19
|
+
const withPerms = [...globals, ...projects];
|
|
20
|
+
const emptyCount = entries.filter((r) => r.totalCount === 0).length;
|
|
19
21
|
// header
|
|
20
|
-
const
|
|
22
|
+
const nameWidths = withPerms.map((r) => r.shortName.length + 2);
|
|
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 typeLabel = result.isGlobal ? '' : result.fileType === 'local' ? ' ˡ' : ' ˢ';
|
|
31
|
+
const displayName = result.isGlobal ? `★ ${result.shortName}` : `${result.shortName}${typeLabel}`;
|
|
32
|
+
const truncName = displayName.length > nameWidth ? displayName.slice(0, nameWidth - 1) + '…' : displayName;
|
|
33
|
+
const nameStyle = result.isGlobal ? `${colors_js_1.YELLOW}` : `${colors_js_1.DIM}`;
|
|
34
|
+
const nameCol = ` ${nameStyle}${pad(truncName, nameWidth)}${colors_js_1.NC}`;
|
|
28
35
|
const catCols = catsPresent.map((c) => {
|
|
29
36
|
const count = result.groups.get(c) || 0;
|
|
30
37
|
return count > 0 ? rpad(count, 5) : `${colors_js_1.DIM}${rpad('·', 5)}${colors_js_1.NC}`;
|
|
31
38
|
}).join(' ');
|
|
32
39
|
const totalCol = rpad(result.totalCount, 5);
|
|
33
40
|
console.log(`${nameCol} ${catCols} ${colors_js_1.BOLD}${totalCol}${colors_js_1.NC}`);
|
|
41
|
+
// separator after global section
|
|
42
|
+
if (result.isGlobal && i + 1 < withPerms.length && !withPerms[i + 1].isGlobal) {
|
|
43
|
+
console.log(` ${colors_js_1.DIM}${'─'.repeat(nameWidth + catsPresent.length * 7 + 8)}${colors_js_1.NC}`);
|
|
44
|
+
}
|
|
34
45
|
}
|
|
35
46
|
if (emptyCount > 0) {
|
|
36
47
|
console.log(`\n ${colors_js_1.DIM}+ ${emptyCount} projects with no permissions${colors_js_1.NC}`);
|
|
@@ -58,5 +69,5 @@ function printVerbose(results, summary) {
|
|
|
58
69
|
function printFooter(summary) {
|
|
59
70
|
console.log(`\n ${colors_js_1.DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors_js_1.NC}`);
|
|
60
71
|
const catSummary = [...summary.categoryTotals.entries()].map(([k, v]) => `${k}: ${colors_js_1.BOLD}${v}${colors_js_1.NC}${colors_js_1.DIM}`).join(' ');
|
|
61
|
-
console.log(` ${colors_js_1.BOLD}${summary.
|
|
72
|
+
console.log(` ${colors_js_1.BOLD}${summary.totalProjects}${colors_js_1.NC} projects ${colors_js_1.BOLD}${summary.totalPerms}${colors_js_1.NC} permissions ${colors_js_1.DIM}(${catSummary})${colors_js_1.NC}\n`);
|
|
62
73
|
}
|
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')) {
|