@shnitzel/plugscout 0.3.22 → 0.3.24

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.
@@ -1143,79 +1143,106 @@ function describeRiskPosture(posture) {
1143
1143
  async function promptResultBrowser(entries) {
1144
1144
  if (!process.stdout.isTTY || entries.length === 0)
1145
1145
  return;
1146
- const WINDOW = 8;
1147
1146
  let selected = 0;
1148
- let linesDrawn = 0;
1149
1147
  const ENTER = '\r';
1150
1148
  const CTRL_C = '';
1151
1149
  const Q = 'q';
1152
- function isUp(key) { return key === '[A' || key === '\x1b[A'; }
1153
- function isDown(key) { return key === '[B' || key === '\x1b[B'; }
1154
- function visibleStart() {
1155
- return Math.max(0, Math.min(selected - Math.floor(WINDOW / 2), entries.length - WINDOW));
1156
- }
1157
- function renderBrowser(firstRender) {
1150
+ // Single-line navigator no second table. The rendered results above are the reference.
1151
+ function renderNavigator(firstRender) {
1158
1152
  if (!firstRender) {
1159
- moveCursor(process.stdout, 0, -linesDrawn);
1153
+ moveCursor(process.stdout, 0, -1);
1160
1154
  clearScreenDown(process.stdout);
1161
1155
  }
1162
- let drawn = 0;
1163
- const start = visibleStart();
1164
- const end = Math.min(start + WINDOW, entries.length);
1165
- for (let i = start; i < end; i++) {
1166
- const prefix = i === selected ? ' ❯ ' : ' ';
1167
- const nameSuffix = entries[i].name ? ` ${entries[i].name}` : '';
1168
- const line = `${prefix}${entries[i].id}${nameSuffix}`;
1169
- process.stdout.write(`${line}\n`);
1170
- drawn += 1;
1171
- }
1172
- if (entries.length > WINDOW) {
1173
- process.stdout.write(`\x1b[2m (${selected + 1}/${entries.length})\x1b[0m\n`);
1174
- drawn += 1;
1175
- }
1176
- linesDrawn = drawn;
1156
+ const entry = entries[selected];
1157
+ const label = entry.name ? `${entry.id} \x1b[90m${entry.name}\x1b[0m` : entry.id;
1158
+ process.stdout.write(` \x1b[36m❯\x1b[0m ${label} \x1b[90m(${selected + 1}/${entries.length})\x1b[0m\n`);
1177
1159
  }
1178
1160
  // eslint-disable-next-line prefer-const
1179
1161
  let running = true;
1180
1162
  let firstLoop = true;
1181
1163
  while (running) {
1182
1164
  if (firstLoop) {
1183
- process.stdout.write('\x1b[2m Inspect results: ↑↓ navigate ⏎ inspect q skip\x1b[0m\n\n');
1165
+ process.stdout.write('\x1b[90m ↑↓ navigate ⏎ inspect q/Esc skip\x1b[0m\n');
1184
1166
  firstLoop = false;
1185
1167
  }
1186
1168
  else {
1187
- process.stdout.write('\n\x1b[2m Inspect another: ↑↓ navigate ⏎ inspect q skip\x1b[0m\n\n');
1169
+ process.stdout.write('\x1b[90m ↑↓ navigate ⏎ inspect another q/Esc done\x1b[0m\n');
1188
1170
  }
1189
- linesDrawn = 0;
1190
1171
  process.stdin.setRawMode(true);
1191
1172
  process.stdin.resume();
1192
1173
  process.stdin.setEncoding('utf8');
1193
- renderBrowser(true);
1174
+ renderNavigator(true);
1194
1175
  const action = await new Promise((resolve) => {
1195
- process.stdin.on('data', function onKey(key) {
1176
+ let escTimer = null;
1177
+ let pendingEsc = false;
1178
+ function doExit() {
1179
+ if (escTimer) {
1180
+ clearTimeout(escTimer);
1181
+ escTimer = null;
1182
+ }
1183
+ process.stdin.removeListener('data', onKey);
1184
+ process.stdin.setRawMode(false);
1185
+ process.stdin.pause();
1186
+ process.stdout.write('\n');
1187
+ resolve({ exit: true });
1188
+ }
1189
+ function onKey(key) {
1190
+ // Handle second byte of a split escape sequence (e.g. \x1b then [A)
1191
+ if (pendingEsc) {
1192
+ pendingEsc = false;
1193
+ if (escTimer) {
1194
+ clearTimeout(escTimer);
1195
+ escTimer = null;
1196
+ }
1197
+ if (key === '[A') {
1198
+ selected = (selected - 1 + entries.length) % entries.length;
1199
+ renderNavigator(false);
1200
+ return;
1201
+ }
1202
+ if (key === '[B') {
1203
+ selected = (selected + 1) % entries.length;
1204
+ renderNavigator(false);
1205
+ return;
1206
+ }
1207
+ // Standalone Esc followed by something unexpected — still exit
1208
+ doExit();
1209
+ return;
1210
+ }
1196
1211
  if (key === CTRL_C || key === Q) {
1197
- process.stdin.removeListener('data', onKey);
1198
- process.stdin.setRawMode(false);
1199
- process.stdin.pause();
1200
- process.stdout.write('\n');
1201
- resolve({ exit: true });
1212
+ doExit();
1202
1213
  }
1203
- else if (isUp(key)) {
1214
+ else if (key === '\x1b[A' || key === '\x1bOA') {
1215
+ // Single-chunk arrow up (most terminals)
1204
1216
  selected = (selected - 1 + entries.length) % entries.length;
1205
- renderBrowser(false);
1217
+ renderNavigator(false);
1206
1218
  }
1207
- else if (isDown(key)) {
1219
+ else if (key === '\x1b[B' || key === '\x1bOB') {
1220
+ // Single-chunk arrow down (most terminals)
1208
1221
  selected = (selected + 1) % entries.length;
1209
- renderBrowser(false);
1222
+ renderNavigator(false);
1223
+ }
1224
+ else if (key === '\x1b') {
1225
+ // Could be standalone Esc or first byte of split arrow sequence
1226
+ pendingEsc = true;
1227
+ escTimer = setTimeout(() => {
1228
+ pendingEsc = false;
1229
+ escTimer = null;
1230
+ doExit();
1231
+ }, 40);
1210
1232
  }
1211
1233
  else if (key === ENTER) {
1234
+ if (escTimer) {
1235
+ clearTimeout(escTimer);
1236
+ escTimer = null;
1237
+ }
1212
1238
  process.stdin.removeListener('data', onKey);
1213
1239
  process.stdin.setRawMode(false);
1214
1240
  process.stdin.pause();
1215
1241
  process.stdout.write('\n');
1216
1242
  resolve({ exit: false, id: entries[selected].id });
1217
1243
  }
1218
- });
1244
+ }
1245
+ process.stdin.on('data', onKey);
1219
1246
  });
1220
1247
  if (action.exit) {
1221
1248
  running = false;
@@ -39,7 +39,7 @@ export async function renderHomeScreen() {
39
39
  'plugscout sync --dry-run',
40
40
  'plugscout help',
41
41
  ]) {
42
- lines.push(` ${colorIfTty(cmd, colors.green)}`);
42
+ lines.push(` ${colorIfTty(cmd, colors.cyan)}`);
43
43
  }
44
44
  lines.push('');
45
45
  lines.push(colorIfTty('Examples', colors.bold));
@@ -49,7 +49,7 @@ export async function renderHomeScreen() {
49
49
  'plugscout search github',
50
50
  'plugscout show --id claude-connector:asana',
51
51
  ]) {
52
- lines.push(` ${colorIfTty(cmd, colors.green)}`);
52
+ lines.push(` ${colorIfTty(cmd, colors.cyan)}`);
53
53
  }
54
54
  lines.push('');
55
55
  lines.push(colorIfTty('Kind aliases', colors.bold));
@@ -279,7 +279,7 @@ export async function renderInteractiveHome() {
279
279
  const rl = createInterface({ input: process.stdin, output: process.stdout });
280
280
  process.stdin.resume();
281
281
  const id = await new Promise((res) => {
282
- rl.question(' Enter catalog ID: ', (answer) => {
282
+ rl.question(' Catalog ID (e.g. mcp:github, skill:code-review, cursor-extension:gitlens): ', (answer) => {
283
283
  rl.close();
284
284
  res(answer.trim());
285
285
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shnitzel/plugscout",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "Claude plugins + Claude connectors + Copilot extensions + Skills + MCP security intelligence framework",
5
5
  "private": false,
6
6
  "type": "module",