ccperm 1.11.3 → 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,19 +137,38 @@ 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
160
  const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
143
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;
144
164
  const catColWidth = catsPresent.length * 7;
145
- const typeColWidth = 7; // " local " or " shared"
165
+ const typeColWidth = 7;
146
166
  const maxName = Math.max(...withPerms.map((r) => r.shortName.length), 7);
147
- const nameColWidth = Math.min(maxName + typeColWidth, 35); // name + type combined
167
+ const nameColWidth = Math.min(maxName + typeColWidth, 35);
148
168
  const nameWidth = nameColWidth - typeColWidth;
149
- // content: marker(2) + nameCol(nameColWidth) + gap(2) + catCols + gap(2) + total(5)
150
- const contentWidth = 2 + nameColWidth + 2 + catColWidth + 2 + 5;
151
- const w = Math.min(cols, contentWidth + 4); // +4 for box borders + padding
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);
152
172
  const inner = w - 4;
153
173
  const hasGlobalSep = withPerms.some((r) => r.isGlobal) && withPerms.some((r) => !r.isGlobal);
154
174
  // box takes: top(1) + header(2) + sep(1) + content + globalSep?(1) + emptyLine?(1) + bottom(1)
@@ -161,7 +181,8 @@ function renderList(state, withPerms, emptyCount) {
161
181
  const scrollInfo = withPerms.length > visibleRows ? `${state.cursor + 1}/${withPerms.length}` : '';
162
182
  const lines = [];
163
183
  lines.push(boxTop('ccperm', scrollInfo, w));
164
- lines.push(boxLine(`${colors_js_1.DIM}${pad('PROJECT', nameColWidth)} ${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));
165
186
  lines.push(boxSep(w));
166
187
  const globalCount = withPerms.filter((r) => r.isGlobal).length;
167
188
  const end = Math.min(state.scrollOffset + visibleRows, withPerms.length);
@@ -180,7 +201,17 @@ function renderList(state, withPerms, emptyCount) {
180
201
  return `${colors_js_1.DIM}${rpad('·', 5)}${colors_js_1.NC}`;
181
202
  }).join(' ');
182
203
  const totalCol = isCursor ? `${colors_js_1.BOLD}${rpad(r.totalCount, 5)}${colors_js_1.NC}` : rpad(r.totalCount, 5);
183
- 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));
184
215
  // separator after global section
185
216
  if (r.isGlobal && i + 1 < withPerms.length && !withPerms[i + 1].isGlobal) {
186
217
  lines.push(boxSep(w));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.11.3",
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"