ccperm 1.4.2 → 1.5.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.
@@ -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();
@@ -128,6 +135,7 @@ function renderList(state, withPerms, emptyCount) {
128
135
  }
129
136
  function renderDetail(state, withPerms, results) {
130
137
  const rows = process.stdout.rows || 24;
138
+ const cols = process.stdout.columns || 80;
131
139
  const project = withPerms[state.selectedProject];
132
140
  if (!project)
133
141
  return;
@@ -138,37 +146,69 @@ function renderDetail(state, withPerms, results) {
138
146
  const projDir = projIdx >= 0 ? project.display.slice(0, projIdx) : project.display;
139
147
  return dir === projDir;
140
148
  });
141
- // build content lines
142
- const content = [];
149
+ // build navigable rows
150
+ const navRows = [];
143
151
  for (const result of projectResults) {
144
152
  if (result.totalCount === 0)
145
153
  continue;
146
- // short filename: settings.local.json
147
154
  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}`);
155
+ navRows.push({ line: ` ${colors_js_1.CYAN}${fileName}${colors_js_1.NC} ${colors_js_1.DIM}(${result.totalCount})${colors_js_1.NC}` });
149
156
  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}`);
157
+ const key = `${result.path}:${group.category}`;
158
+ const isOpen = state.expanded.has(key);
159
+ 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 });
161
+ if (isOpen) {
162
+ const maxLen = cols - 10;
163
+ for (const item of group.items) {
164
+ 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}` });
166
+ }
153
167
  }
154
168
  }
155
- content.push(` ${colors_js_1.DIM}${'─'.repeat(40)}${colors_js_1.NC}`);
156
169
  }
157
- // header + footer take space
170
+ // handle toggle
171
+ if (state._toggle) {
172
+ delete state._toggle;
173
+ const row = navRows[state.detailCursor];
174
+ if (row?.key) {
175
+ if (state.expanded.has(row.key))
176
+ state.expanded.delete(row.key);
177
+ else
178
+ state.expanded.add(row.key);
179
+ // re-render needed — will happen on next render() call
180
+ renderDetail(state, withPerms, results);
181
+ return;
182
+ }
183
+ }
184
+ // clamp cursor
185
+ if (state.detailCursor >= navRows.length)
186
+ state.detailCursor = Math.max(0, navRows.length - 1);
187
+ // scroll
158
188
  const headerLines = 3;
159
189
  const footerLines = 2;
160
190
  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}`;
191
+ if (state.detailCursor < state.detailScroll)
192
+ state.detailScroll = state.detailCursor;
193
+ if (state.detailCursor >= state.detailScroll + visibleRows)
194
+ state.detailScroll = state.detailCursor - visibleRows + 1;
195
+ const visible = navRows.slice(state.detailScroll, state.detailScroll + visibleRows);
166
196
  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}`);
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}`);
168
198
  lines.push('');
169
- lines.push(...visible);
199
+ for (let i = 0; i < visible.length; i++) {
200
+ const globalIdx = state.detailScroll + i;
201
+ const isCursor = globalIdx === state.detailCursor;
202
+ 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
+ }
209
+ }
170
210
  lines.push('');
171
- lines.push(` ${colors_js_1.DIM}[↑↓] scroll [Esc] back [q] quit${colors_js_1.NC}`);
211
+ lines.push(` ${colors_js_1.DIM}[↑↓] navigate [Enter] expand/collapse [Esc] back [q] quit${colors_js_1.NC}`);
172
212
  process.stdout.write(lines.join('\n') + '\n');
173
213
  }
174
214
  function pad(s, n) {
package/dist/scanner.js CHANGED
@@ -59,7 +59,7 @@ async function findSettingsFiles(searchDir, onProgress, debug = false) {
59
59
  continue;
60
60
  }
61
61
  for (const f of inner) {
62
- if (f.startsWith('settings') && f.endsWith('.json')) {
62
+ if (f === 'settings.json' || f === 'settings.local.json') {
63
63
  const full = node_path_1.default.join(claudeDir, f);
64
64
  try {
65
65
  await node_fs_1.default.promises.access(full, node_fs_1.default.constants.W_OK);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"