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.
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.shortPath = shortPath;
4
4
  exports.projectDir = projectDir;
5
- exports.mergeByProject = mergeByProject;
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 mergeByProject(results) {
16
- const map = new Map();
17
- for (const r of results) {
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
- merged.groups.set(g.category, (merged.groups.get(g.category) || 0) + g.items.length);
19
+ groups.set(g.category, g.items.length);
27
20
  }
28
- }
29
- return [...map.values()];
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 merged = mergeByProject(results);
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: merged.length,
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 merged = (0, aggregator_js_1.mergeByProject)(results);
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)(merged, results);
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)(merged, summary);
101
+ (0, renderer_js_1.printCompact)(entries, summary);
102
102
  }
103
103
  (0, renderer_js_1.printFooter)(summary);
104
104
  if (args.includes('--fix')) {
@@ -33,7 +33,9 @@ function boxSep(width) {
33
33
  }
34
34
  function startInteractive(merged, results) {
35
35
  return new Promise((resolve) => {
36
- const withPerms = merged.filter((r) => r.totalCount > 0).sort((a, b) => b.totalCount - a.totalCount);
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 nameWidth = Math.min(Math.max(...withPerms.map((r) => r.shortName.length), 7), inner - catColWidth - 8);
118
- // box takes: top(1) + header(2) + sep(1) + content + emptyLine?(1) + sep(1) + bottom(1) = 6-7 + content
119
- const chrome = 6 + (emptyCount > 0 ? 1 : 0);
120
- const visibleRows = Math.max(1, rows - chrome);
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 truncName = r.shortName.length > nameWidth ? r.shortName.slice(0, nameWidth - 1) + '…' : r.shortName;
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 projectResults = results.filter((r) => {
161
- const idx = r.display.indexOf('/.claude/');
162
- const dir = idx >= 0 ? r.display.slice(0, idx) : r.display;
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 result of projectResults) {
170
- if (result.totalCount === 0)
171
- continue;
172
- const fileName = result.display.replace(/.*\/\.claude\//, '');
173
- navRows.push({ text: `${colors_js_1.CYAN}${fileName}${colors_js_1.NC} ${colors_js_1.DIM}(${result.totalCount})${colors_js_1.NC}` });
174
- for (const group of result.groups) {
175
- const key = `${result.path}:${group.category}`;
176
- const isOpen = state.expanded.has(key);
177
- const arrow = isOpen ? '' : '▸';
178
- 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 });
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
- lines.push(boxTop(`${project.shortName} (${project.totalCount})`, scrollInfo, w));
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(merged, summary) {
14
+ function printCompact(entries, summary) {
15
15
  const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
16
- const catsPresent = cats.filter((c) => merged.some((r) => r.groups.has(c)));
17
- const withPerms = merged.filter((r) => r.totalCount > 0).sort((a, b) => b.totalCount - a.totalCount);
18
- const emptyCount = merged.filter((r) => r.totalCount === 0).length;
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 nameWidth = Math.min(Math.max(...withPerms.map((r) => r.shortName.length), 7), 40);
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 (const result of withPerms) {
26
- const truncName = result.shortName.length > nameWidth ? result.shortName.slice(0, nameWidth - 1) + '…' : result.shortName;
27
- const nameCol = ` ${colors_js_1.DIM}${pad(truncName, nameWidth)}${colors_js_1.NC}`;
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.projectsWithPerms}${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`);
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')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"