ccperm 1.5.0 → 1.6.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.
Files changed (2) hide show
  1. package/dist/interactive.js +55 -46
  2. package/package.json +1 -1
@@ -6,6 +6,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.startInteractive = startInteractive;
7
7
  const node_readline_1 = __importDefault(require("node:readline"));
8
8
  const colors_js_1 = require("./colors.js");
9
+ // strip ANSI escape codes for visible length
10
+ function visLen(s) {
11
+ return s.replace(/\x1b\[[0-9;]*m/g, '').length;
12
+ }
13
+ function boxLine(text, width) {
14
+ const vis = visLen(text);
15
+ const padRight = Math.max(0, width - vis - 1);
16
+ return `${colors_js_1.DIM}│${colors_js_1.NC} ${text}${' '.repeat(padRight)}${colors_js_1.DIM}│${colors_js_1.NC}`;
17
+ }
18
+ function boxTop(title, info, width) {
19
+ const inner = width - 2;
20
+ const titlePart = ` ${title} `;
21
+ const infoPart = info ? ` ${info} ` : '';
22
+ const fill = Math.max(0, inner - titlePart.length - infoPart.length);
23
+ return `${colors_js_1.DIM}┌${titlePart}${'─'.repeat(fill)}${infoPart}┐${colors_js_1.NC}`;
24
+ }
25
+ function boxBottom(hint, width) {
26
+ const inner = width - 2;
27
+ const hintPart = ` ${hint} `;
28
+ const fill = Math.max(0, inner - hintPart.length);
29
+ return `${colors_js_1.DIM}└${'─'.repeat(fill)}${hintPart}┘${colors_js_1.NC}`;
30
+ }
31
+ function boxSep(width) {
32
+ return `${colors_js_1.DIM}├${'─'.repeat(width - 2)}┤${colors_js_1.NC}`;
33
+ }
9
34
  function startInteractive(merged, results) {
10
35
  return new Promise((resolve) => {
11
36
  const withPerms = merged.filter((r) => r.totalCount > 0).sort((a, b) => b.totalCount - a.totalCount);
@@ -26,16 +51,11 @@ function startInteractive(merged, results) {
26
51
  process.stdin.pause();
27
52
  process.stdin.removeListener('keypress', onKey);
28
53
  process.removeListener('SIGINT', onSigint);
29
- // show cursor
30
54
  process.stdout.write('\x1b[?25h');
31
55
  };
32
- const onSigint = () => {
33
- cleanup();
34
- process.exit(0);
35
- };
56
+ const onSigint = () => { cleanup(); process.exit(0); };
36
57
  process.on('SIGINT', onSigint);
37
58
  const render = () => {
38
- // clear screen + move cursor home + hide cursor
39
59
  process.stdout.write('\x1b[2J\x1b[H\x1b[?25l');
40
60
  if (state.view === 'list')
41
61
  renderList(state, withPerms, emptyCount);
@@ -52,12 +72,10 @@ function startInteractive(merged, results) {
52
72
  return;
53
73
  }
54
74
  if (state.view === 'list') {
55
- if (key.name === 'up') {
75
+ if (key.name === 'up')
56
76
  state.cursor = Math.max(0, state.cursor - 1);
57
- }
58
- else if (key.name === 'down') {
77
+ else if (key.name === 'down')
59
78
  state.cursor = Math.min(withPerms.length - 1, state.cursor + 1);
60
- }
61
79
  else if (key.name === 'return') {
62
80
  state.selectedProject = state.cursor;
63
81
  state.detailCursor = 0;
@@ -67,7 +85,6 @@ function startInteractive(merged, results) {
67
85
  }
68
86
  }
69
87
  else {
70
- // detail view
71
88
  if (key.name === 'escape' || key.name === 'backspace') {
72
89
  state.view = 'list';
73
90
  state.detailCursor = 0;
@@ -80,7 +97,6 @@ function startInteractive(merged, results) {
80
97
  state.detailCursor++;
81
98
  }
82
99
  else if (key.name === 'return') {
83
- // toggle handled in renderDetail via detailRows
84
100
  state._toggle = true;
85
101
  }
86
102
  }
@@ -93,28 +109,30 @@ function startInteractive(merged, results) {
93
109
  function renderList(state, withPerms, emptyCount) {
94
110
  const rows = process.stdout.rows || 24;
95
111
  const cols = process.stdout.columns || 80;
112
+ const w = Math.min(cols, 82);
113
+ const inner = w - 4; // box border + 1 space each side
96
114
  const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
97
115
  const catsPresent = cats.filter((c) => withPerms.some((r) => r.groups.has(c)));
98
- const nameWidth = Math.min(Math.max(...withPerms.map((r) => r.shortName.length), 7), 40);
99
- // header takes 4 lines, footer takes 3 lines
100
- const headerLines = 4;
101
- const footerLines = 3 + (emptyCount > 0 ? 1 : 0);
102
- const visibleRows = Math.max(1, rows - headerLines - footerLines);
103
- // adjust scroll offset
116
+ 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);
104
121
  if (state.cursor < state.scrollOffset)
105
122
  state.scrollOffset = state.cursor;
106
123
  if (state.cursor >= state.scrollOffset + visibleRows)
107
124
  state.scrollOffset = state.cursor - visibleRows + 1;
125
+ const scrollInfo = withPerms.length > visibleRows ? `${state.cursor + 1}/${withPerms.length}` : '';
108
126
  const lines = [];
109
- lines.push(` ${colors_js_1.CYAN}${colors_js_1.BOLD}ccperm${colors_js_1.NC} ${colors_js_1.DIM}interactive${colors_js_1.NC}\n`);
110
- lines.push(` ${colors_js_1.DIM}${pad('PROJECT', nameWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}`);
111
- lines.push(` ${colors_js_1.DIM}${'─'.repeat(nameWidth + catsPresent.length * 7 + 8)}${colors_js_1.NC}`);
127
+ lines.push(boxTop('ccperm', scrollInfo, w));
128
+ lines.push(boxLine(`${colors_js_1.DIM}${pad('PROJECT', nameWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}`, w));
129
+ lines.push(boxSep(w));
112
130
  const end = Math.min(state.scrollOffset + visibleRows, withPerms.length);
113
131
  for (let i = state.scrollOffset; i < end; i++) {
114
132
  const r = withPerms[i];
115
133
  const isCursor = i === state.cursor;
116
134
  const truncName = r.shortName.length > nameWidth ? r.shortName.slice(0, nameWidth - 1) + '…' : r.shortName;
117
- const marker = isCursor ? `${colors_js_1.CYAN}> ` : ' ';
135
+ const marker = isCursor ? `${colors_js_1.CYAN} ` : ' ';
118
136
  const nameStyle = isCursor ? `${colors_js_1.BOLD}` : `${colors_js_1.DIM}`;
119
137
  const nameCol = `${marker}${nameStyle}${pad(truncName, nameWidth)}${colors_js_1.NC}`;
120
138
  const catCols = catsPresent.map((c) => {
@@ -124,18 +142,18 @@ function renderList(state, withPerms, emptyCount) {
124
142
  return `${colors_js_1.DIM}${rpad('·', 5)}${colors_js_1.NC}`;
125
143
  }).join(' ');
126
144
  const totalCol = isCursor ? `${colors_js_1.BOLD}${rpad(r.totalCount, 5)}${colors_js_1.NC}` : rpad(r.totalCount, 5);
127
- lines.push(`${nameCol} ${catCols} ${totalCol}`);
145
+ lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}`, w));
128
146
  }
129
147
  if (emptyCount > 0) {
130
- lines.push(`\n ${colors_js_1.DIM}+ ${emptyCount} projects with no permissions${colors_js_1.NC}`);
148
+ lines.push(boxLine(`${colors_js_1.DIM}+ ${emptyCount} projects with no permissions${colors_js_1.NC}`, w));
131
149
  }
132
- lines.push('');
133
- lines.push(` ${colors_js_1.DIM}[↑↓] navigate [Enter] detail [q] quit${colors_js_1.NC}`);
150
+ lines.push(boxBottom('[↑↓] navigate [Enter] detail [q] quit', w));
134
151
  process.stdout.write(lines.join('\n') + '\n');
135
152
  }
136
153
  function renderDetail(state, withPerms, results) {
137
154
  const rows = process.stdout.rows || 24;
138
155
  const cols = process.stdout.columns || 80;
156
+ const w = Math.min(cols, 82);
139
157
  const project = withPerms[state.selectedProject];
140
158
  if (!project)
141
159
  return;
@@ -152,17 +170,17 @@ function renderDetail(state, withPerms, results) {
152
170
  if (result.totalCount === 0)
153
171
  continue;
154
172
  const fileName = result.display.replace(/.*\/\.claude\//, '');
155
- navRows.push({ line: ` ${colors_js_1.CYAN}${fileName}${colors_js_1.NC} ${colors_js_1.DIM}(${result.totalCount})${colors_js_1.NC}` });
173
+ navRows.push({ text: `${colors_js_1.CYAN}${fileName}${colors_js_1.NC} ${colors_js_1.DIM}(${result.totalCount})${colors_js_1.NC}` });
156
174
  for (const group of result.groups) {
157
175
  const key = `${result.path}:${group.category}`;
158
176
  const isOpen = state.expanded.has(key);
159
177
  const arrow = isOpen ? '▾' : '▸';
160
- navRows.push({ line: ` ${colors_js_1.YELLOW}${arrow} ${group.category}${colors_js_1.NC} ${colors_js_1.DIM}(${group.items.length})${colors_js_1.NC}`, key });
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 });
161
179
  if (isOpen) {
162
- const maxLen = cols - 10;
180
+ const maxLen = w - 12;
163
181
  for (const item of group.items) {
164
182
  const name = item.name.length > maxLen ? item.name.slice(0, maxLen - 1) + '…' : item.name;
165
- navRows.push({ line: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}` });
183
+ navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}` });
166
184
  }
167
185
  }
168
186
  }
@@ -176,39 +194,30 @@ function renderDetail(state, withPerms, results) {
176
194
  state.expanded.delete(row.key);
177
195
  else
178
196
  state.expanded.add(row.key);
179
- // re-render needed — will happen on next render() call
180
197
  renderDetail(state, withPerms, results);
181
198
  return;
182
199
  }
183
200
  }
184
- // clamp cursor
185
201
  if (state.detailCursor >= navRows.length)
186
202
  state.detailCursor = Math.max(0, navRows.length - 1);
187
- // scroll
188
- const headerLines = 3;
189
- const footerLines = 2;
190
- const visibleRows = Math.max(1, rows - headerLines - footerLines);
203
+ // box chrome: top(1) + sep(1) + bottom(1) = 3
204
+ const visibleRows = Math.max(1, rows - 3);
191
205
  if (state.detailCursor < state.detailScroll)
192
206
  state.detailScroll = state.detailCursor;
193
207
  if (state.detailCursor >= state.detailScroll + visibleRows)
194
208
  state.detailScroll = state.detailCursor - visibleRows + 1;
195
209
  const visible = navRows.slice(state.detailScroll, state.detailScroll + visibleRows);
210
+ const scrollInfo = navRows.length > visibleRows ? `${state.detailCursor + 1}/${navRows.length}` : '';
196
211
  const lines = [];
197
- lines.push(` ${colors_js_1.CYAN}${colors_js_1.BOLD}${project.shortName}${colors_js_1.NC} ${colors_js_1.DIM}(${project.totalCount} permissions)${colors_js_1.NC}`);
198
- lines.push('');
212
+ lines.push(boxTop(`${project.shortName} (${project.totalCount})`, scrollInfo, w));
199
213
  for (let i = 0; i < visible.length; i++) {
200
214
  const globalIdx = state.detailScroll + i;
201
215
  const isCursor = globalIdx === state.detailCursor;
202
216
  const row = visible[i];
203
- if (isCursor) {
204
- lines.push(row.line.replace(/^ /, `${colors_js_1.CYAN}> `));
205
- }
206
- else {
207
- lines.push(row.line);
208
- }
217
+ const prefix = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
218
+ lines.push(boxLine(`${prefix}${row.text}`, w));
209
219
  }
210
- lines.push('');
211
- lines.push(` ${colors_js_1.DIM}[↑↓] navigate [Enter] expand/collapse [Esc] back [q] quit${colors_js_1.NC}`);
220
+ lines.push(boxBottom('[↑↓] navigate [Enter] expand [Esc] back [q] quit', w));
212
221
  process.stdout.write(lines.join('\n') + '\n');
213
222
  }
214
223
  function pad(s, n) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"