ccperm 1.6.0 → 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.
@@ -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
- merged = { display: r.display, shortName: shortPath(r.display), totalCount: 0, groups: new Map() };
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;
@@ -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.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));
121
125
  if (state.cursor < state.scrollOffset)
122
126
  state.scrollOffset = state.cursor;
123
127
  if (state.cursor >= state.scrollOffset + visibleRows)
@@ -127,13 +131,15 @@ 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 displayName = r.isGlobal ? `★ ${r.shortName}` : r.shortName;
140
+ const truncName = displayName.length > nameWidth ? displayName.slice(0, nameWidth - 1) + '…' : displayName;
135
141
  const marker = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
136
- const nameStyle = isCursor ? `${colors_js_1.BOLD}` : `${colors_js_1.DIM}`;
142
+ const nameStyle = isCursor ? `${colors_js_1.BOLD}` : r.isGlobal ? `${colors_js_1.YELLOW}` : `${colors_js_1.DIM}`;
137
143
  const nameCol = `${marker}${nameStyle}${pad(truncName, nameWidth)}${colors_js_1.NC}`;
138
144
  const catCols = catsPresent.map((c) => {
139
145
  const count = r.groups.get(c) || 0;
@@ -143,6 +149,10 @@ function renderList(state, withPerms, emptyCount) {
143
149
  }).join(' ');
144
150
  const totalCol = isCursor ? `${colors_js_1.BOLD}${rpad(r.totalCount, 5)}${colors_js_1.NC}` : rpad(r.totalCount, 5);
145
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
+ }
146
156
  }
147
157
  if (emptyCount > 0) {
148
158
  lines.push(boxLine(`${colors_js_1.DIM}+ ${emptyCount} projects with no permissions${colors_js_1.NC}`, w));
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 withPerms = merged.filter((r) => r.totalCount > 0).sort((a, b) => b.totalCount - a.totalCount);
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 nameWidth = Math.min(Math.max(...withPerms.map((r) => r.shortName.length), 7), 40);
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 (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 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')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"