@emeryld/manager 0.6.6 → 0.7.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.
@@ -53,9 +53,16 @@ async function promptWithReadline(entries, title) {
53
53
  rl.close();
54
54
  }
55
55
  }
56
- function formatInteractiveLines(state, selectedIndex, title) {
56
+ function formatInteractiveLines(state, selectedIndex, title, searchState) {
57
57
  const heading = pageHeading(title, state.page, state.pageCount);
58
58
  const lines = [heading];
59
+ if (searchState?.active) {
60
+ const queryLabel = searchState.query || '(type to filter)';
61
+ const statusLabel = searchState.hasResults
62
+ ? '(enter to run top match)'
63
+ : '(no matches yet)';
64
+ lines.push(colors.dim(`Search: ${queryLabel} ${statusLabel}`));
65
+ }
59
66
  state.options.forEach((option, index) => {
60
67
  const isSelected = index === selectedIndex;
61
68
  const pointer = isSelected ? `${colors.green('➤')} ` : '';
@@ -69,7 +76,12 @@ function formatInteractiveLines(state, selectedIndex, title) {
69
76
  : isSelected
70
77
  ? colors.green(option.entry.displayName)
71
78
  : option.entry.displayName;
72
- lines.push(`${pointer}${numberLabel}. ${option.entry.emoji} ${label} ${colors.dim(option.entry.metaLabel)}`);
79
+ const runHint = searchState?.active &&
80
+ searchState.hasResults &&
81
+ index === 0
82
+ ? colors.dim(' (enter to run)')
83
+ : '';
84
+ lines.push(`${pointer}${numberLabel}. ${option.entry.emoji} ${label} ${colors.dim(option.entry.metaLabel)}${runHint}`);
73
85
  return;
74
86
  }
75
87
  const icon = option.action === 'back'
@@ -85,8 +97,17 @@ function formatInteractiveLines(state, selectedIndex, title) {
85
97
  const navLabel = option.enabled ? baseLabel : colors.dim(baseLabel);
86
98
  lines.push(`${pointer}${numberLabel}. ${icon} ${navLabel}`);
87
99
  });
100
+ if (searchState?.active && !searchState.hasResults) {
101
+ lines.push(colors.yellow('No scripts match your search.'));
102
+ }
88
103
  lines.push('');
89
- lines.push(colors.dim(`Use ↑/↓ (or j/k) to move, 1-${PAGE_SIZE} to run, ${PREVIOUS_KEY} prev page (when shown), ${NEXT_KEY} next page (when shown), ${BACK_KEY} back, Enter to confirm, Esc/Ctrl+C to exit.`));
104
+ lines.push(colors.dim(`Use ↑/↓ (or j/k) to move, 1-${PAGE_SIZE} to run, ${PREVIOUS_KEY} prev page (when shown), ${NEXT_KEY} next page (when shown), ${BACK_KEY} back, Enter to run, Space to filter, Esc/Ctrl+C to exit.`));
105
+ if (searchState?.active) {
106
+ lines.push(colors.dim('Search mode: type to filter, Backspace edits the query, Esc returns to navigation, Enter runs the highlighted entry.'));
107
+ }
108
+ else {
109
+ lines.push(colors.dim('Press Space to start typing a filter.'));
110
+ }
90
111
  return lines;
91
112
  }
92
113
  function renderInteractiveList(lines, previousLineCount) {
@@ -109,9 +130,15 @@ export async function promptForScript(entries, title) {
109
130
  }
110
131
  process.stdout.write('\x1b[?25l');
111
132
  return new Promise((resolve) => {
133
+ const baseEntries = entries;
134
+ let filteredEntries = baseEntries;
112
135
  let selectedIndex = 0;
113
136
  let renderedLines = 0;
114
- let state = buildVisibleOptions(entries, 0);
137
+ let state = buildVisibleOptions(filteredEntries, 0);
138
+ let searchActive = false;
139
+ let searchQuery = '';
140
+ let pageBeforeSearch = 0;
141
+ let selectionBeforeSearch = 0;
115
142
  const cleanup = () => {
116
143
  if (renderedLines > 0) {
117
144
  process.stdout.write(`\x1b[${renderedLines}A`);
@@ -135,10 +162,24 @@ export async function promptForScript(entries, title) {
135
162
  console.log();
136
163
  resolve(undefined);
137
164
  };
138
- const setPage = (page) => {
139
- state = buildVisibleOptions(entries, page);
165
+ const rebuildState = (page) => {
166
+ state = buildVisibleOptions(filteredEntries, page);
140
167
  selectedIndex = Math.min(selectedIndex, state.options.length - 1);
141
168
  selectedIndex = Math.max(0, selectedIndex);
169
+ };
170
+ const render = () => {
171
+ const searchState = searchActive
172
+ ? {
173
+ active: true,
174
+ query: searchQuery.trim(),
175
+ hasResults: filteredEntries.length > 0,
176
+ }
177
+ : undefined;
178
+ const lines = formatInteractiveLines(state, selectedIndex, title, searchState);
179
+ renderedLines = renderInteractiveList(lines, renderedLines);
180
+ };
181
+ const setPage = (page) => {
182
+ rebuildState(page);
142
183
  render();
143
184
  };
144
185
  const handleNav = (option) => {
@@ -167,9 +208,47 @@ export async function promptForScript(entries, title) {
167
208
  }
168
209
  handleNav(option);
169
210
  };
170
- const render = () => {
171
- const lines = formatInteractiveLines(state, selectedIndex, title);
172
- renderedLines = renderInteractiveList(lines, renderedLines);
211
+ const matchesSearchTerm = (entry, term) => {
212
+ const haystack = [
213
+ entry.displayName,
214
+ entry.metaLabel,
215
+ entry.script ?? '',
216
+ entry.description ?? '',
217
+ ]
218
+ .join(' ')
219
+ .toLowerCase();
220
+ return haystack.includes(term);
221
+ };
222
+ const applySearch = () => {
223
+ const normalized = searchQuery.trim().toLowerCase();
224
+ if (normalized.length > 0) {
225
+ filteredEntries = baseEntries.filter((entry) => matchesSearchTerm(entry, normalized));
226
+ }
227
+ else {
228
+ filteredEntries = baseEntries;
229
+ }
230
+ rebuildState(0);
231
+ selectedIndex = 0;
232
+ render();
233
+ };
234
+ const enterSearchMode = () => {
235
+ if (searchActive)
236
+ return;
237
+ pageBeforeSearch = state.page;
238
+ selectionBeforeSearch = selectedIndex;
239
+ searchActive = true;
240
+ searchQuery = '';
241
+ applySearch();
242
+ };
243
+ const exitSearchMode = () => {
244
+ if (!searchActive)
245
+ return;
246
+ searchActive = false;
247
+ searchQuery = '';
248
+ filteredEntries = baseEntries;
249
+ state = buildVisibleOptions(filteredEntries, pageBeforeSearch);
250
+ selectedIndex = Math.min(selectionBeforeSearch, state.options.length - 1);
251
+ render();
173
252
  };
174
253
  const onData = (buffer) => {
175
254
  const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
@@ -177,10 +256,25 @@ export async function promptForScript(entries, title) {
177
256
  const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
178
257
  const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
179
258
  const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
180
- if (isCtrlC || isEscape) {
259
+ const isBackspace = buffer.length === 1 && (buffer[0] === 0x7f || buffer[0] === 0x08);
260
+ const isPrintable = buffer.length === 1 && buffer[0] >= 0x20 && buffer[0] <= 0x7e;
261
+ const isSpace = buffer.length === 1 && buffer[0] === 0x20;
262
+ if (isCtrlC) {
181
263
  cleanup();
182
264
  process.exit(1);
183
265
  }
266
+ if (isEscape) {
267
+ if (searchActive) {
268
+ exitSearchMode();
269
+ return;
270
+ }
271
+ cleanup();
272
+ process.exit(1);
273
+ }
274
+ if (!searchActive && isSpace) {
275
+ enterSearchMode();
276
+ return;
277
+ }
184
278
  if (isArrowUp ||
185
279
  (buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
186
280
  selectedIndex =
@@ -195,7 +289,29 @@ export async function promptForScript(entries, title) {
195
289
  return;
196
290
  }
197
291
  if (isEnter) {
198
- activateOption(state.options[selectedIndex]);
292
+ const option = state.options[selectedIndex];
293
+ if (option)
294
+ activateOption(option);
295
+ else
296
+ process.stdout.write('\x07');
297
+ return;
298
+ }
299
+ if (searchActive) {
300
+ if (isBackspace) {
301
+ if (searchQuery.length > 0) {
302
+ searchQuery = searchQuery.slice(0, -1);
303
+ applySearch();
304
+ }
305
+ else {
306
+ //exitSearchMode()
307
+ }
308
+ return;
309
+ }
310
+ if (isPrintable) {
311
+ searchQuery += String.fromCharCode(buffer[0]);
312
+ applySearch();
313
+ return;
314
+ }
199
315
  return;
200
316
  }
201
317
  if (buffer.length === 1 && buffer[0] >= 0x30 && buffer[0] <= 0x39) {
@@ -211,17 +327,19 @@ export async function promptForScript(entries, title) {
211
327
  ((buffer[0] >= 0x41 && buffer[0] <= 0x5a) ||
212
328
  (buffer[0] >= 0x61 && buffer[0] <= 0x7a))) {
213
329
  const char = String.fromCharCode(buffer[0]).toLowerCase();
214
- const foundIndex = entries.findIndex((entry) => entry.displayName.toLowerCase().startsWith(char));
330
+ const foundIndex = baseEntries.findIndex((entry) => entry.displayName.toLowerCase().startsWith(char));
215
331
  if (foundIndex !== -1) {
216
332
  const page = Math.floor(foundIndex / PAGE_SIZE);
217
- state = buildVisibleOptions(entries, page);
333
+ state = buildVisibleOptions(filteredEntries, page);
218
334
  selectedIndex = foundIndex % PAGE_SIZE;
219
335
  render();
220
336
  }
221
337
  else {
222
338
  process.stdout.write('\x07');
223
339
  }
340
+ return;
224
341
  }
342
+ process.stdout.write('\x07');
225
343
  };
226
344
  input.on('data', onData);
227
345
  render();
@@ -14,7 +14,7 @@ export async function runHelperCli({ scripts, title = 'Helper CLI', argv = proce
14
14
  const [firstArg, ...restArgs] = args;
15
15
  if (firstArg === '--list' || firstArg === '-l') {
16
16
  printScriptList(normalized, title);
17
- return;
17
+ return false;
18
18
  }
19
19
  if (firstArg === '--help' || firstArg === '-h') {
20
20
  console.log(colors.magenta('Usage:'));
@@ -22,19 +22,21 @@ export async function runHelperCli({ scripts, title = 'Helper CLI', argv = proce
22
22
  console.log('\nFlags:');
23
23
  console.log(' --list Show available scripts');
24
24
  console.log(' --help Show this information');
25
- return;
25
+ return false;
26
26
  }
27
27
  const argLooksLikeScript = firstArg && !firstArg.startsWith('-');
28
28
  if (argLooksLikeScript) {
29
29
  const requested = findScriptEntry(normalized, firstArg);
30
30
  if (requested) {
31
31
  await runEntry(requested, restArgs);
32
- return;
32
+ return true;
33
33
  }
34
34
  console.log(colors.yellow(`Unknown script "${firstArg}". Falling back to interactive selection…`));
35
35
  }
36
36
  const selection = await promptForScript(normalized, title);
37
37
  if (selection) {
38
38
  await runEntry(selection, []);
39
+ return true;
39
40
  }
41
+ return false;
40
42
  }
@@ -191,26 +191,24 @@ export function makeBaseScriptEntries(pkg) {
191
191
  const scripts = pkg.json?.scripts ?? {};
192
192
  const dependencies = collectDependencies(pkg);
193
193
  const marker = getPackageMarker(pkg, dependencies);
194
- return BASE_SCRIPT_KEYS.map((key) => {
194
+ return BASE_SCRIPT_KEYS.reduce((entries, key) => {
195
195
  const resolution = resolveBaseScript(pkg, key, scripts, dependencies, marker.kind);
196
- const description = resolution.available
197
- ? resolution.isDefault
198
- ? `${resolution.description} (default)`
199
- : resolution.description
200
- : `No ${key} command detected`;
201
- return {
196
+ const args = resolution.args;
197
+ if (!resolution.available || !args)
198
+ return entries;
199
+ const description = resolution.isDefault
200
+ ? `${resolution.description} (default)`
201
+ : resolution.description;
202
+ entries.push({
202
203
  name: key,
203
204
  emoji: '⚡️',
204
205
  description,
205
206
  handler: async () => {
206
- if (!resolution.available || !resolution.args) {
207
- console.log(colors.yellow(`No ${key} command found for ${pkg.name}.`));
208
- return;
209
- }
210
- await run('pnpm', resolution.args, { cwd: pkg.path });
207
+ await run('pnpm', args, { cwd: pkg.path });
211
208
  },
212
- };
213
- });
209
+ });
210
+ return entries;
211
+ }, []);
214
212
  }
215
213
  export function getPackageMarker(pkg, dependencies = collectDependencies(pkg)) {
216
214
  const normalizedDeps = new Set([...dependencies].map((dep) => String(dep).toLowerCase().trim()));
package/dist/menu.js CHANGED
@@ -14,8 +14,7 @@ function formatKindLabel(kind) {
14
14
  return 'CLI';
15
15
  return `${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
16
16
  }
17
- function makeManagerStepEntries(targets, packages, state, options) {
18
- const includeBack = options?.includeBack ?? true;
17
+ function makeManagerStepEntries(targets, packages, state) {
19
18
  return [
20
19
  {
21
20
  name: 'update dependencies',
@@ -103,18 +102,6 @@ function makeManagerStepEntries(targets, packages, state, options) {
103
102
  state.lastStep = 'full';
104
103
  },
105
104
  },
106
- ...(includeBack
107
- ? [
108
- {
109
- name: 'back',
110
- emoji: '↩️',
111
- description: 'Pick packages again',
112
- handler: async () => {
113
- state.lastStep = 'back';
114
- },
115
- },
116
- ]
117
- : []),
118
105
  ];
119
106
  }
120
107
  function makeDockerEntry(pkg) {
@@ -158,17 +145,37 @@ export function buildPackageSelectionMenu(packages, onStepComplete) {
158
145
  const ordered = getOrderedPackages(packages);
159
146
  const entries = ordered.map((pkg) => {
160
147
  const marker = getPackageMarker(pkg);
161
- const pkgColor = pkg.color ?? 'cyan';
162
- const descriptionMeta = colors.dim(pkg.relativeDir ?? pkg.dirName);
163
- const labelBadge = marker.label ? marker.colorize(marker.label) : '';
148
+ const primaryName = pkg.name ?? pkg.substitute ?? pkg.dirName;
149
+ const packageColorizer = colors[pkg.color ?? 'cyan'] ?? colors.cyan;
150
+ const highlightedName = packageColorizer(primaryName);
151
+ const tagParts = [];
152
+ const addTag = (value, colorizer) => {
153
+ if (!value)
154
+ return;
155
+ tagParts.push(colorizer(`[${value}]`));
156
+ };
157
+ const relativePath = pkg.relativeDir ?? pkg.dirName;
158
+ if (relativePath && relativePath !== '.') {
159
+ addTag(relativePath, colors.gray);
160
+ }
161
+ if (pkg.version) {
162
+ addTag(`v${pkg.version}`, colors.yellow);
163
+ }
164
+ if (pkg.substitute && pkg.substitute !== primaryName) {
165
+ const aliasTag = pkg.name ? `alias:${pkg.substitute}` : pkg.substitute;
166
+ addTag(aliasTag, colors.magenta);
167
+ }
168
+ if (pkg.dockerfilePath) {
169
+ addTag('docker', colors.green);
170
+ }
164
171
  const kindLabel = formatKindLabel(marker.kind);
165
- const kindBadge = marker.kindColorize(kindLabel);
166
- const badgeMeta = [labelBadge, kindBadge].filter(Boolean).join(' ');
172
+ addTag(kindLabel, marker.kindColorize);
173
+ const labelWord = marker.label ? ` ${marker.colorize(marker.label)}` : '';
174
+ const tags = tagParts.length ? ` ${tagParts.join(' ')}` : '';
167
175
  return {
168
- name: pkg.name ?? pkg.substitute ?? pkg.dirName,
169
- emoji: marker.colorize(''),
170
- description: [descriptionMeta, badgeMeta].filter(Boolean).join(' '),
171
- color: pkgColor,
176
+ name: `${highlightedName}${tags}${labelWord}`,
177
+ emoji: '',
178
+ description: pkg.json?.description ?? pkg.relativeDir ?? pkg.dirName,
172
179
  handler: async () => {
173
180
  const step = await runStepLoop([pkg], packages);
174
181
  onStepComplete?.(step);
@@ -179,7 +186,6 @@ export function buildPackageSelectionMenu(packages, onStepComplete) {
179
186
  return entries;
180
187
  entries.push({
181
188
  name: 'All packages',
182
- color: 'gray',
183
189
  emoji: globalEmoji,
184
190
  description: 'Select all packages',
185
191
  handler: async () => {
@@ -207,19 +213,7 @@ export async function runStepLoop(targets, packages) {
207
213
  emoji: globalEmoji,
208
214
  description: 'update/test/build/publish',
209
215
  handler: async () => {
210
- const managerEntries = [
211
- ...makeManagerStepEntries(targets, packages, state, {
212
- includeBack: false,
213
- }),
214
- {
215
- name: 'back',
216
- emoji: '↩️',
217
- description: 'Return to package menu',
218
- handler: () => {
219
- state.lastStep = undefined;
220
- },
221
- },
222
- ];
216
+ const managerEntries = makeManagerStepEntries(targets, packages, state);
223
217
  await runHelperCli({
224
218
  title: `Manager actions for ${pkg.name}`,
225
219
  scripts: managerEntries,
@@ -236,38 +230,23 @@ export async function runStepLoop(targets, packages) {
236
230
  console.log(colors.yellow(`No package.json scripts found for ${pkg.name}.`));
237
231
  return;
238
232
  }
239
- const scriptsMenu = [
240
- ...scriptEntries,
241
- {
242
- name: 'back',
243
- emoji: '↩️',
244
- description: 'Return to package menu',
245
- handler: () => { },
246
- },
247
- ];
248
233
  await runHelperCli({
249
234
  title: `${pkg.name} scripts`,
250
- scripts: scriptsMenu,
235
+ scripts: scriptEntries,
251
236
  argv: [],
252
237
  });
253
238
  },
254
239
  },
255
- {
256
- name: 'back',
257
- emoji: '↩️',
258
- description: 'Pick packages again',
259
- handler: () => {
260
- state.lastStep = 'back';
261
- },
262
- },
263
240
  ];
264
- await runHelperCli({
241
+ const ranScript = await runHelperCli({
265
242
  title: `Actions for ${pkg.name}`,
266
243
  scripts: entries,
267
244
  argv: [],
268
245
  });
269
- if (state.lastStep === 'back')
246
+ if (!ranScript) {
247
+ state.lastStep = 'back';
270
248
  return state.lastStep;
249
+ }
271
250
  // loop to keep showing menu
272
251
  }
273
252
  }
@@ -279,11 +258,15 @@ export async function runStepLoop(targets, packages) {
279
258
  while (true) {
280
259
  state.lastStep = undefined;
281
260
  const entries = makeManagerStepEntries(targets, packages, state);
282
- await runHelperCli({
261
+ const ranScript = await runHelperCli({
283
262
  title: `Actions for ${targets.length === 1 ? targets[0].name : 'selected packages'}`,
284
263
  scripts: entries,
285
264
  argv: [], // <- key change
286
265
  });
266
+ if (!ranScript) {
267
+ state.lastStep = 'back';
268
+ return state.lastStep;
269
+ }
287
270
  if (state.lastStep === 'back')
288
271
  return state.lastStep;
289
272
  // keep looping to show menu again
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",