ccperm 1.5.1 → 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 -47
  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,29 +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
- const scrollInfo = withPerms.length > visibleRows ? ` ${colors_js_1.DIM}${state.cursor + 1}/${withPerms.length}${colors_js_1.NC}` : '';
110
- lines.push(` ${colors_js_1.CYAN}${colors_js_1.BOLD}ccperm${colors_js_1.NC} ${colors_js_1.DIM}interactive${colors_js_1.NC}${scrollInfo}\n`);
111
- lines.push(` ${colors_js_1.DIM}${pad('PROJECT', nameWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}`);
112
- 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));
113
130
  const end = Math.min(state.scrollOffset + visibleRows, withPerms.length);
114
131
  for (let i = state.scrollOffset; i < end; i++) {
115
132
  const r = withPerms[i];
116
133
  const isCursor = i === state.cursor;
117
134
  const truncName = r.shortName.length > nameWidth ? r.shortName.slice(0, nameWidth - 1) + '…' : r.shortName;
118
- const marker = isCursor ? `${colors_js_1.CYAN}> ` : ' ';
135
+ const marker = isCursor ? `${colors_js_1.CYAN} ` : ' ';
119
136
  const nameStyle = isCursor ? `${colors_js_1.BOLD}` : `${colors_js_1.DIM}`;
120
137
  const nameCol = `${marker}${nameStyle}${pad(truncName, nameWidth)}${colors_js_1.NC}`;
121
138
  const catCols = catsPresent.map((c) => {
@@ -125,18 +142,18 @@ function renderList(state, withPerms, emptyCount) {
125
142
  return `${colors_js_1.DIM}${rpad('·', 5)}${colors_js_1.NC}`;
126
143
  }).join(' ');
127
144
  const totalCol = isCursor ? `${colors_js_1.BOLD}${rpad(r.totalCount, 5)}${colors_js_1.NC}` : rpad(r.totalCount, 5);
128
- lines.push(`${nameCol} ${catCols} ${totalCol}`);
145
+ lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}`, w));
129
146
  }
130
147
  if (emptyCount > 0) {
131
- 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));
132
149
  }
133
- lines.push('');
134
- 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));
135
151
  process.stdout.write(lines.join('\n') + '\n');
136
152
  }
137
153
  function renderDetail(state, withPerms, results) {
138
154
  const rows = process.stdout.rows || 24;
139
155
  const cols = process.stdout.columns || 80;
156
+ const w = Math.min(cols, 82);
140
157
  const project = withPerms[state.selectedProject];
141
158
  if (!project)
142
159
  return;
@@ -153,17 +170,17 @@ function renderDetail(state, withPerms, results) {
153
170
  if (result.totalCount === 0)
154
171
  continue;
155
172
  const fileName = result.display.replace(/.*\/\.claude\//, '');
156
- 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}` });
157
174
  for (const group of result.groups) {
158
175
  const key = `${result.path}:${group.category}`;
159
176
  const isOpen = state.expanded.has(key);
160
177
  const arrow = isOpen ? '▾' : '▸';
161
- 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 });
162
179
  if (isOpen) {
163
- const maxLen = cols - 10;
180
+ const maxLen = w - 12;
164
181
  for (const item of group.items) {
165
182
  const name = item.name.length > maxLen ? item.name.slice(0, maxLen - 1) + '…' : item.name;
166
- 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}` });
167
184
  }
168
185
  }
169
186
  }
@@ -177,39 +194,30 @@ function renderDetail(state, withPerms, results) {
177
194
  state.expanded.delete(row.key);
178
195
  else
179
196
  state.expanded.add(row.key);
180
- // re-render needed — will happen on next render() call
181
197
  renderDetail(state, withPerms, results);
182
198
  return;
183
199
  }
184
200
  }
185
- // clamp cursor
186
201
  if (state.detailCursor >= navRows.length)
187
202
  state.detailCursor = Math.max(0, navRows.length - 1);
188
- // scroll
189
- const headerLines = 3;
190
- const footerLines = 2;
191
- 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);
192
205
  if (state.detailCursor < state.detailScroll)
193
206
  state.detailScroll = state.detailCursor;
194
207
  if (state.detailCursor >= state.detailScroll + visibleRows)
195
208
  state.detailScroll = state.detailCursor - visibleRows + 1;
196
209
  const visible = navRows.slice(state.detailScroll, state.detailScroll + visibleRows);
210
+ const scrollInfo = navRows.length > visibleRows ? `${state.detailCursor + 1}/${navRows.length}` : '';
197
211
  const lines = [];
198
- 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}`);
199
- lines.push('');
212
+ lines.push(boxTop(`${project.shortName} (${project.totalCount})`, scrollInfo, w));
200
213
  for (let i = 0; i < visible.length; i++) {
201
214
  const globalIdx = state.detailScroll + i;
202
215
  const isCursor = globalIdx === state.detailCursor;
203
216
  const row = visible[i];
204
- if (isCursor) {
205
- lines.push(row.line.replace(/^ /, `${colors_js_1.CYAN}> `));
206
- }
207
- else {
208
- lines.push(row.line);
209
- }
217
+ const prefix = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
218
+ lines.push(boxLine(`${prefix}${row.text}`, w));
210
219
  }
211
- lines.push('');
212
- 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));
213
221
  process.stdout.write(lines.join('\n') + '\n');
214
222
  }
215
223
  function pad(s, n) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.5.1",
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"