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.
- package/dist/interactive.js +55 -46
- 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,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
|
|
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
|
-
lines.push(
|
|
110
|
-
lines.push(
|
|
111
|
-
lines.push(
|
|
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(
|
|
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({
|
|
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({
|
|
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 =
|
|
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({
|
|
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
|
-
//
|
|
188
|
-
const
|
|
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(
|
|
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
|
-
|
|
204
|
-
|
|
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) {
|