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.
- package/dist/interactive.js +55 -47
- package/package.json +1 -1
package/dist/interactive.js
CHANGED
|
@@ -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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const visibleRows = Math.max(1, rows -
|
|
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
|
-
|
|
110
|
-
lines.push(
|
|
111
|
-
lines.push(
|
|
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(
|
|
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({
|
|
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({
|
|
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 =
|
|
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({
|
|
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
|
-
//
|
|
189
|
-
const
|
|
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(
|
|
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
|
-
|
|
205
|
-
|
|
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) {
|