ccperm 1.11.2 → 1.12.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.
@@ -62,6 +62,7 @@ function startInteractive(merged, results) {
62
62
  const projects = merged.filter((r) => r.totalCount > 0 && !r.isGlobal).sort((a, b) => b.totalCount - a.totalCount);
63
63
  const withPerms = [...globals, ...projects];
64
64
  const emptyCount = merged.filter((r) => r.totalCount === 0 && !r.isGlobal).length;
65
+ const riskMap = buildRiskMap(results);
65
66
  if (withPerms.length === 0) {
66
67
  console.log(`\n ${colors_js_1.GREEN}No projects with permissions found.${colors_js_1.NC}\n`);
67
68
  resolve();
@@ -85,7 +86,7 @@ function startInteractive(merged, results) {
85
86
  const render = () => {
86
87
  process.stdout.write('\x1b[2J\x1b[H\x1b[?25l');
87
88
  if (state.view === 'list')
88
- renderList(state, withPerms, emptyCount);
89
+ renderList(state, withPerms, emptyCount, riskMap);
89
90
  else
90
91
  renderDetail(state, withPerms, results);
91
92
  };
@@ -136,16 +137,39 @@ function startInteractive(merged, results) {
136
137
  render();
137
138
  });
138
139
  }
139
- function renderList(state, withPerms, emptyCount) {
140
+ function buildRiskMap(results) {
141
+ const map = new Map();
142
+ for (const r of results) {
143
+ let crit = 0, hi = 0;
144
+ for (const g of r.groups) {
145
+ for (const item of g.items) {
146
+ const info = (0, explain_js_1.explain)(g.category, item.name);
147
+ if (info.risk === 'critical')
148
+ crit++;
149
+ else if (info.risk === 'high')
150
+ hi++;
151
+ }
152
+ }
153
+ map.set(r.display, { critical: crit, high: hi });
154
+ }
155
+ return map;
156
+ }
157
+ function renderList(state, withPerms, emptyCount, riskMap) {
140
158
  const rows = process.stdout.rows || 24;
141
159
  const cols = process.stdout.columns || 80;
142
- const w = Math.min(cols, 82);
143
- const inner = w - 4; // box border + 1 space each side
144
160
  const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
145
161
  const catsPresent = cats.filter((c) => withPerms.some((r) => r.groups.has(c)));
162
+ const hasRisk = [...riskMap.values()].some((v) => v.critical > 0 || v.high > 0);
163
+ const riskColWidth = hasRisk ? 3 : 0;
146
164
  const catColWidth = catsPresent.length * 7;
165
+ const typeColWidth = 7;
147
166
  const maxName = Math.max(...withPerms.map((r) => r.shortName.length), 7);
148
- const nameWidth = Math.min(maxName, inner - catColWidth - 16);
167
+ const nameColWidth = Math.min(maxName + typeColWidth, 35);
168
+ const nameWidth = nameColWidth - typeColWidth;
169
+ // content: marker(2) + nameCol + gap(2) + catCols + gap(2) + total(5) + gap(1) + riskCol
170
+ const contentWidth = 2 + nameColWidth + 2 + catColWidth + 2 + 5 + (hasRisk ? 1 + riskColWidth : 0);
171
+ const w = Math.min(cols, contentWidth + 4);
172
+ const inner = w - 4;
149
173
  const hasGlobalSep = withPerms.some((r) => r.isGlobal) && withPerms.some((r) => !r.isGlobal);
150
174
  // box takes: top(1) + header(2) + sep(1) + content + globalSep?(1) + emptyLine?(1) + bottom(1)
151
175
  const chrome = 5 + (hasGlobalSep ? 1 : 0) + (emptyCount > 0 ? 1 : 0);
@@ -157,7 +181,8 @@ function renderList(state, withPerms, emptyCount) {
157
181
  const scrollInfo = withPerms.length > visibleRows ? `${state.cursor + 1}/${withPerms.length}` : '';
158
182
  const lines = [];
159
183
  lines.push(boxTop('ccperm', scrollInfo, w));
160
- lines.push(boxLine(`${colors_js_1.DIM}${pad('PROJECT', nameWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}`, w));
184
+ const riskHeader = hasRisk ? ` ${colors_js_1.DIM}⚠${colors_js_1.NC}` : '';
185
+ lines.push(boxLine(`${colors_js_1.DIM}${pad('PROJECT', nameColWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}${riskHeader}`, w));
161
186
  lines.push(boxSep(w));
162
187
  const globalCount = withPerms.filter((r) => r.isGlobal).length;
163
188
  const end = Math.min(state.scrollOffset + visibleRows, withPerms.length);
@@ -176,7 +201,17 @@ function renderList(state, withPerms, emptyCount) {
176
201
  return `${colors_js_1.DIM}${rpad('·', 5)}${colors_js_1.NC}`;
177
202
  }).join(' ');
178
203
  const totalCol = isCursor ? `${colors_js_1.BOLD}${rpad(r.totalCount, 5)}${colors_js_1.NC}` : rpad(r.totalCount, 5);
179
- lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}`, w));
204
+ let riskCol = '';
205
+ if (hasRisk) {
206
+ const risk = riskMap.get(r.display) || { critical: 0, high: 0 };
207
+ if (risk.critical > 0)
208
+ riskCol = ` ${colors_js_1.RED}${rpad(risk.critical, 2)}${colors_js_1.NC}`;
209
+ else if (risk.high > 0)
210
+ riskCol = ` ${colors_js_1.YELLOW}${rpad(risk.high, 2)}${colors_js_1.NC}`;
211
+ else
212
+ riskCol = ` ${colors_js_1.DIM}${rpad('·', 2)}${colors_js_1.NC}`;
213
+ }
214
+ lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}${riskCol}`, w));
180
215
  // separator after global section
181
216
  if (r.isGlobal && i + 1 < withPerms.length && !withPerms[i + 1].isGlobal) {
182
217
  lines.push(boxSep(w));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.11.2",
3
+ "version": "1.12.0",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"