ccperm 1.4.3 → 1.5.1

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 +62 -21
  2. package/package.json +1 -1
@@ -15,7 +15,7 @@ function startInteractive(merged, results) {
15
15
  resolve();
16
16
  return;
17
17
  }
18
- const state = { view: 'list', cursor: 0, scrollOffset: 0, selectedProject: 0, detailScroll: 0 };
18
+ const state = { view: 'list', cursor: 0, scrollOffset: 0, selectedProject: 0, detailCursor: 0, detailScroll: 0, expanded: new Set() };
19
19
  node_readline_1.default.emitKeypressEvents(process.stdin);
20
20
  if (process.stdin.isTTY)
21
21
  process.stdin.setRawMode(true);
@@ -60,7 +60,9 @@ function startInteractive(merged, results) {
60
60
  }
61
61
  else if (key.name === 'return') {
62
62
  state.selectedProject = state.cursor;
63
+ state.detailCursor = 0;
63
64
  state.detailScroll = 0;
65
+ state.expanded = new Set();
64
66
  state.view = 'detail';
65
67
  }
66
68
  }
@@ -68,13 +70,18 @@ function startInteractive(merged, results) {
68
70
  // detail view
69
71
  if (key.name === 'escape' || key.name === 'backspace') {
70
72
  state.view = 'list';
73
+ state.detailCursor = 0;
71
74
  state.detailScroll = 0;
72
75
  }
73
76
  else if (key.name === 'up') {
74
- state.detailScroll = Math.max(0, state.detailScroll - 1);
77
+ state.detailCursor = Math.max(0, state.detailCursor - 1);
75
78
  }
76
79
  else if (key.name === 'down') {
77
- state.detailScroll++;
80
+ state.detailCursor++;
81
+ }
82
+ else if (key.name === 'return') {
83
+ // toggle handled in renderDetail via detailRows
84
+ state._toggle = true;
78
85
  }
79
86
  }
80
87
  render();
@@ -99,7 +106,8 @@ function renderList(state, withPerms, emptyCount) {
99
106
  if (state.cursor >= state.scrollOffset + visibleRows)
100
107
  state.scrollOffset = state.cursor - visibleRows + 1;
101
108
  const lines = [];
102
- 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`);
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`);
103
111
  lines.push(` ${colors_js_1.DIM}${pad('PROJECT', nameWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}`);
104
112
  lines.push(` ${colors_js_1.DIM}${'─'.repeat(nameWidth + catsPresent.length * 7 + 8)}${colors_js_1.NC}`);
105
113
  const end = Math.min(state.scrollOffset + visibleRows, withPerms.length);
@@ -128,6 +136,7 @@ function renderList(state, withPerms, emptyCount) {
128
136
  }
129
137
  function renderDetail(state, withPerms, results) {
130
138
  const rows = process.stdout.rows || 24;
139
+ const cols = process.stdout.columns || 80;
131
140
  const project = withPerms[state.selectedProject];
132
141
  if (!project)
133
142
  return;
@@ -138,37 +147,69 @@ function renderDetail(state, withPerms, results) {
138
147
  const projDir = projIdx >= 0 ? project.display.slice(0, projIdx) : project.display;
139
148
  return dir === projDir;
140
149
  });
141
- // build content lines
142
- const content = [];
150
+ // build navigable rows
151
+ const navRows = [];
143
152
  for (const result of projectResults) {
144
153
  if (result.totalCount === 0)
145
154
  continue;
146
- // short filename: settings.local.json
147
155
  const fileName = result.display.replace(/.*\/\.claude\//, '');
148
- content.push(` ${colors_js_1.CYAN}${fileName}${colors_js_1.NC} ${colors_js_1.DIM}(${result.totalCount})${colors_js_1.NC}`);
156
+ navRows.push({ line: ` ${colors_js_1.CYAN}${fileName}${colors_js_1.NC} ${colors_js_1.DIM}(${result.totalCount})${colors_js_1.NC}` });
149
157
  for (const group of result.groups) {
150
- content.push(` ${colors_js_1.YELLOW}${group.category}${colors_js_1.NC} ${colors_js_1.DIM}(${group.items.length})${colors_js_1.NC}`);
151
- for (const item of group.items) {
152
- content.push(` ${colors_js_1.DIM}${item.name}${colors_js_1.NC}`);
158
+ const key = `${result.path}:${group.category}`;
159
+ const isOpen = state.expanded.has(key);
160
+ 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 });
162
+ if (isOpen) {
163
+ const maxLen = cols - 10;
164
+ for (const item of group.items) {
165
+ 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}` });
167
+ }
153
168
  }
154
169
  }
155
- content.push(` ${colors_js_1.DIM}${'─'.repeat(40)}${colors_js_1.NC}`);
156
170
  }
157
- // header + footer take space
171
+ // handle toggle
172
+ if (state._toggle) {
173
+ delete state._toggle;
174
+ const row = navRows[state.detailCursor];
175
+ if (row?.key) {
176
+ if (state.expanded.has(row.key))
177
+ state.expanded.delete(row.key);
178
+ else
179
+ state.expanded.add(row.key);
180
+ // re-render needed — will happen on next render() call
181
+ renderDetail(state, withPerms, results);
182
+ return;
183
+ }
184
+ }
185
+ // clamp cursor
186
+ if (state.detailCursor >= navRows.length)
187
+ state.detailCursor = Math.max(0, navRows.length - 1);
188
+ // scroll
158
189
  const headerLines = 3;
159
190
  const footerLines = 2;
160
191
  const visibleRows = Math.max(1, rows - headerLines - footerLines);
161
- const maxScroll = Math.max(0, content.length - visibleRows);
162
- if (state.detailScroll > maxScroll)
163
- state.detailScroll = maxScroll;
164
- const visible = content.slice(state.detailScroll, state.detailScroll + visibleRows);
165
- const scrollPct = content.length <= visibleRows ? '' : ` ${colors_js_1.DIM}${state.detailScroll + visibleRows}/${content.length}${colors_js_1.NC}`;
192
+ if (state.detailCursor < state.detailScroll)
193
+ state.detailScroll = state.detailCursor;
194
+ if (state.detailCursor >= state.detailScroll + visibleRows)
195
+ state.detailScroll = state.detailCursor - visibleRows + 1;
196
+ const visible = navRows.slice(state.detailScroll, state.detailScroll + visibleRows);
166
197
  const lines = [];
167
- 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}${scrollPct}`);
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}`);
168
199
  lines.push('');
169
- lines.push(...visible);
200
+ for (let i = 0; i < visible.length; i++) {
201
+ const globalIdx = state.detailScroll + i;
202
+ const isCursor = globalIdx === state.detailCursor;
203
+ 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
+ }
210
+ }
170
211
  lines.push('');
171
- lines.push(` ${colors_js_1.DIM}[↑↓] scroll [Esc] back [q] quit${colors_js_1.NC}`);
212
+ lines.push(` ${colors_js_1.DIM}[↑↓] navigate [Enter] expand/collapse [Esc] back [q] quit${colors_js_1.NC}`);
172
213
  process.stdout.write(lines.join('\n') + '\n');
173
214
  }
174
215
  function pad(s, n) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.4.3",
3
+ "version": "1.5.1",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"