@emeryld/manager 0.8.0 → 0.8.2

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.
@@ -100,12 +100,12 @@ function formatInteractiveLines(state, selectedIndex, title, searchState) {
100
100
  lines.push(colors.yellow('No scripts match your search.'));
101
101
  }
102
102
  lines.push('');
103
- 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.`));
103
+ 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, Type to filter (numbers run scripts), Esc/Ctrl+C to exit.`));
104
104
  if (searchState?.active) {
105
105
  lines.push(colors.dim('Search mode: type to filter, Backspace edits the query, Esc returns to navigation, Enter runs the highlighted entry.'));
106
106
  }
107
107
  else {
108
- lines.push(colors.dim('Press Space to start typing a filter.'));
108
+ lines.push(colors.dim('Type any non-number character to start filtering.'));
109
109
  }
110
110
  return lines;
111
111
  }
@@ -230,13 +230,13 @@ export async function promptForScript(entries, title) {
230
230
  selectedIndex = 0;
231
231
  render();
232
232
  };
233
- const enterSearchMode = () => {
233
+ const enterSearchMode = (initialQuery = '') => {
234
234
  if (searchActive)
235
235
  return;
236
236
  pageBeforeSearch = state.page;
237
237
  selectionBeforeSearch = selectedIndex;
238
238
  searchActive = true;
239
- searchQuery = '';
239
+ searchQuery = initialQuery;
240
240
  applySearch();
241
241
  };
242
242
  const exitSearchMode = () => {
@@ -256,8 +256,10 @@ export async function promptForScript(entries, title) {
256
256
  const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
257
257
  const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
258
258
  const isBackspace = buffer.length === 1 && (buffer[0] === 0x7f || buffer[0] === 0x08);
259
- const isPrintable = buffer.length === 1 && buffer[0] >= 0x20 && buffer[0] <= 0x7e;
260
- const isSpace = buffer.length === 1 && buffer[0] === 0x20;
259
+ const ascii = buffer.length === 1 ? buffer[0] : undefined;
260
+ const isPrintable = ascii !== undefined && ascii >= 0x20 && ascii <= 0x7e;
261
+ const isDigit = ascii !== undefined && ascii >= 0x30 && ascii <= 0x39;
262
+ const typedChar = isPrintable && ascii !== undefined ? String.fromCharCode(ascii) : '';
261
263
  if (isCtrlC) {
262
264
  cleanup();
263
265
  process.exit(1);
@@ -270,10 +272,6 @@ export async function promptForScript(entries, title) {
270
272
  cleanup();
271
273
  process.exit(1);
272
274
  }
273
- if (!searchActive && isSpace) {
274
- enterSearchMode();
275
- return;
276
- }
277
275
  if (isArrowUp ||
278
276
  (buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
279
277
  selectedIndex =
@@ -307,14 +305,14 @@ export async function promptForScript(entries, title) {
307
305
  return;
308
306
  }
309
307
  if (isPrintable) {
310
- searchQuery += String.fromCharCode(buffer[0]);
308
+ searchQuery += typedChar;
311
309
  applySearch();
312
310
  return;
313
311
  }
314
312
  return;
315
313
  }
316
- if (buffer.length === 1 && buffer[0] >= 0x30 && buffer[0] <= 0x39) {
317
- const numericValue = buffer[0] - 0x30;
314
+ if (isDigit && ascii !== undefined) {
315
+ const numericValue = ascii - 0x30;
318
316
  const option = state.options.find((opt) => opt.hotkey === numericValue);
319
317
  if (option)
320
318
  activateOption(option);
@@ -322,20 +320,8 @@ export async function promptForScript(entries, title) {
322
320
  process.stdout.write('\x07');
323
321
  return;
324
322
  }
325
- if (buffer.length === 1 &&
326
- ((buffer[0] >= 0x41 && buffer[0] <= 0x5a) ||
327
- (buffer[0] >= 0x61 && buffer[0] <= 0x7a))) {
328
- const char = String.fromCharCode(buffer[0]).toLowerCase();
329
- const foundIndex = baseEntries.findIndex((entry) => entry.displayName.toLowerCase().startsWith(char));
330
- if (foundIndex !== -1) {
331
- const page = Math.floor(foundIndex / PAGE_SIZE);
332
- state = buildVisibleOptions(filteredEntries, page);
333
- selectedIndex = foundIndex % PAGE_SIZE;
334
- render();
335
- }
336
- else {
337
- process.stdout.write('\x07');
338
- }
323
+ if (isPrintable) {
324
+ enterSearchMode(typedChar);
339
325
  return;
340
326
  }
341
327
  process.stdout.write('\x07');
package/dist/menu.js CHANGED
@@ -9,6 +9,7 @@ import { ensureWorkingTreeCommitted } from './preflight.js';
9
9
  import { openDockerHelper } from './docker.js';
10
10
  import { run } from './utils/run.js';
11
11
  import { makeBaseScriptEntries, getPackageMarker, getPackageModule } from './menu/script-helpers.js';
12
+ import { describeVersionControlScope, makeVersionControlEntries, } from './version-control.js';
12
13
  function formatKindLabel(kind) {
13
14
  if (kind === 'cli')
14
15
  return 'CLI';
@@ -43,6 +44,18 @@ function makeManagerStepEntries(targets, packages, state) {
43
44
  state.lastStep = 'rebuild';
44
45
  },
45
46
  },
47
+ {
48
+ name: 'version control',
49
+ emoji: '🧭',
50
+ description: 'Workspace version health, updates, and lockfile checks',
51
+ handler: async () => {
52
+ await runHelperCli({
53
+ title: `Version control for ${describeVersionControlScope(targets, packages)}`,
54
+ scripts: makeVersionControlEntries(targets, packages),
55
+ argv: [],
56
+ });
57
+ },
58
+ },
46
59
  {
47
60
  name: 'test',
48
61
  emoji: '🧪',
@@ -0,0 +1,7 @@
1
+ export function packageFilterArg(pkg) {
2
+ if (pkg.name)
3
+ return pkg.name;
4
+ if (!pkg.relativeDir || pkg.relativeDir === '.')
5
+ return '.';
6
+ return `./${pkg.relativeDir}`;
7
+ }
@@ -0,0 +1,249 @@
1
+ import { askLine } from './prompts.js';
2
+ import { run } from './utils/run.js';
3
+ import { colors, logGlobal } from './utils/log.js';
4
+ import { packageFilterArg } from './utils/package-filter.js';
5
+ const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies'];
6
+ const FIELD_LABELS = {
7
+ dependencies: 'deps',
8
+ devDependencies: 'dev',
9
+ peerDependencies: 'peer',
10
+ };
11
+ function packageLabel(pkg) {
12
+ return pkg.name ?? pkg.dirName;
13
+ }
14
+ function describeVersionControlScope(targets, allPackages) {
15
+ if (targets.length === 0)
16
+ return 'no packages';
17
+ if (targets.length === 1)
18
+ return packageLabel(targets[0]);
19
+ if (targets.length === allPackages.length)
20
+ return 'workspace';
21
+ return `${targets.length} packages`;
22
+ }
23
+ function collectDependencyMap(pkgs) {
24
+ const map = new Map();
25
+ for (const pkg of pkgs) {
26
+ for (const field of DEP_FIELDS) {
27
+ const deps = (pkg.json?.[field] ?? {});
28
+ for (const [name, value] of Object.entries(deps)) {
29
+ if (typeof value !== 'string')
30
+ continue;
31
+ const fieldMap = map.get(name) ?? new Map();
32
+ const entry = { pkg, range: value, field };
33
+ const list = fieldMap.get(value) ?? [];
34
+ list.push(entry);
35
+ fieldMap.set(value, list);
36
+ map.set(name, fieldMap);
37
+ }
38
+ }
39
+ }
40
+ return map;
41
+ }
42
+ function getMostCommonVersion(versions) {
43
+ let best;
44
+ let bestCount = 0;
45
+ for (const [ver, entries] of versions) {
46
+ if (entries.length > bestCount) {
47
+ best = ver;
48
+ bestCount = entries.length;
49
+ }
50
+ }
51
+ return best;
52
+ }
53
+ function ensureVersionControlTargets(targets, allPackages, feature) {
54
+ if (allPackages.length === 0) {
55
+ logGlobal('No packages found to run version control actions.', colors.yellow);
56
+ return false;
57
+ }
58
+ if (targets.length === 0) {
59
+ logGlobal(`Select at least one package before running ${feature}.`, colors.yellow);
60
+ return false;
61
+ }
62
+ return true;
63
+ }
64
+ function buildPackageFilterArgs(targets, allPackages) {
65
+ if (!targets.length || targets.length === allPackages.length) {
66
+ return [];
67
+ }
68
+ const seen = new Set();
69
+ const args = [];
70
+ for (const pkg of targets) {
71
+ const filterArg = packageFilterArg(pkg);
72
+ if (seen.has(filterArg))
73
+ continue;
74
+ seen.add(filterArg);
75
+ args.push('--filter', filterArg);
76
+ }
77
+ return args;
78
+ }
79
+ function buildRecursiveUpdateArgs(targets, allPackages, spec) {
80
+ return ['update', '-r', ...buildPackageFilterArgs(targets, allPackages), spec];
81
+ }
82
+ function formatConflictDetails(versions) {
83
+ return [...versions.entries()]
84
+ .map(([range, entries]) => {
85
+ const fields = new Set(entries.map((entry) => FIELD_LABELS[entry.field]));
86
+ const label = [...fields].join('/') || 'deps';
87
+ const packages = entries
88
+ .map((entry) => packageLabel(entry.pkg))
89
+ .join(', ');
90
+ return `${range} (${label}: ${packages})`;
91
+ })
92
+ .join(' | ');
93
+ }
94
+ async function updateByPattern(targets, allPackages) {
95
+ if (!ensureVersionControlTargets(targets, allPackages, 'Regex dependency update'))
96
+ return;
97
+ const scopeLabel = describeVersionControlScope(targets, allPackages);
98
+ const defaultPattern = '@emeryld/*';
99
+ const promptPattern = `Dependency pattern (use wildcards or scopes, default ${defaultPattern}): `;
100
+ const patternInput = await askLine(promptPattern);
101
+ const rawPattern = (patternInput ?? '').trim() || defaultPattern;
102
+ const promptVersion = 'Version or tag to pin (default "latest"): ';
103
+ const versionInput = await askLine(promptVersion);
104
+ const normalizedVersion = (versionInput ?? '').trim() || 'latest';
105
+ const lastAt = rawPattern.lastIndexOf('@');
106
+ const slashIndex = rawPattern.lastIndexOf('/');
107
+ const hasExplicitVersion = lastAt > 0 && lastAt > slashIndex;
108
+ const spec = hasExplicitVersion
109
+ ? rawPattern
110
+ : `${rawPattern}@${normalizedVersion}`;
111
+ logGlobal(`Updating dependencies using ${colors.cyan(spec)} for ${scopeLabel}…`, colors.cyan);
112
+ await run('pnpm', buildRecursiveUpdateArgs(targets, allPackages, spec));
113
+ logGlobal('Pattern update complete.', colors.green);
114
+ }
115
+ async function runVersionHealthScan(targets, allPackages) {
116
+ if (!ensureVersionControlTargets(targets, allPackages, 'Version health scan'))
117
+ return;
118
+ const scopeLabel = describeVersionControlScope(targets, allPackages);
119
+ logGlobal(`Scanning version misalignments for ${scopeLabel}…`, colors.cyan);
120
+ const dependencyMap = collectDependencyMap(targets);
121
+ const conflicts = [...dependencyMap.entries()].filter(([, versions]) => versions.size > 1);
122
+ if (conflicts.length === 0) {
123
+ logGlobal('No version conflicts detected.', colors.green);
124
+ return;
125
+ }
126
+ console.log(colors.yellow('Misaligned dependency versions:'));
127
+ conflicts.forEach(([dep, versions]) => {
128
+ console.log(` ${colors.bold(dep)} → ${formatConflictDetails(versions)}`);
129
+ });
130
+ }
131
+ async function alignDependencyVersions(targets, allPackages) {
132
+ if (!ensureVersionControlTargets(targets, allPackages, 'Dependency alignment'))
133
+ return;
134
+ const scopeLabel = describeVersionControlScope(targets, allPackages);
135
+ const dependencyMap = collectDependencyMap(targets);
136
+ if (!dependencyMap.size) {
137
+ logGlobal(`No dependencies found in ${scopeLabel}.`, colors.yellow);
138
+ return;
139
+ }
140
+ const misaligned = [...dependencyMap.entries()]
141
+ .filter(([, versions]) => versions.size > 1)
142
+ .map(([dep]) => dep);
143
+ const defaultDependency = misaligned[0] ?? dependencyMap.keys().next().value ?? '';
144
+ if (!defaultDependency) {
145
+ logGlobal('Unable to infer a dependency to align.', colors.yellow);
146
+ return;
147
+ }
148
+ const dependencyPrompt = `Dependency to align (default ${defaultDependency}): `;
149
+ const dependencyInput = (await askLine(dependencyPrompt))?.trim();
150
+ const dependency = dependencyInput || defaultDependency;
151
+ const versions = dependencyMap.get(dependency);
152
+ if (!versions) {
153
+ logGlobal(`Dependency "${dependency}" not tracked across ${scopeLabel}.`, colors.yellow);
154
+ return;
155
+ }
156
+ const defaultVersion = getMostCommonVersion(versions) ?? [...versions.keys()][0];
157
+ const versionPrompt = defaultVersion && defaultVersion.length
158
+ ? `Version to align to (default ${defaultVersion}): `
159
+ : 'Version to align to: ';
160
+ const versionInputFinal = (await askLine(versionPrompt))?.trim();
161
+ const targetVersion = versionInputFinal || defaultVersion;
162
+ if (!targetVersion) {
163
+ logGlobal('No version selected; cancelling alignment.', colors.yellow);
164
+ return;
165
+ }
166
+ const spec = `${dependency}@${targetVersion}`;
167
+ logGlobal(`Aligning ${colors.cyan(dependency)} to ${colors.cyan(targetVersion)} across ${scopeLabel}…`, colors.cyan);
168
+ await run('pnpm', buildRecursiveUpdateArgs(targets, allPackages, spec));
169
+ logGlobal('Dependency alignment complete.', colors.green);
170
+ }
171
+ async function showDriftReport(targets, allPackages) {
172
+ if (!ensureVersionControlTargets(targets, allPackages, 'Drift report'))
173
+ return;
174
+ const scopeLabel = describeVersionControlScope(targets, allPackages);
175
+ logGlobal(`Computing drift report for ${scopeLabel}…`, colors.cyan);
176
+ const dependencyMap = collectDependencyMap(targets);
177
+ const driftLines = [];
178
+ for (const [dep, versions] of dependencyMap.entries()) {
179
+ if (versions.size <= 1)
180
+ continue;
181
+ const standard = getMostCommonVersion(versions);
182
+ const deviators = [...versions.entries()].filter(([range]) => range !== standard);
183
+ if (!deviators.length)
184
+ continue;
185
+ const detail = deviators
186
+ .map(([range, entries]) => `${range}: ${entries.map((entry) => packageLabel(entry.pkg)).join(', ')}`)
187
+ .join(' | ');
188
+ driftLines.push(`${colors.bold(dep)} (standard ${standard}) → ${detail}`);
189
+ }
190
+ if (!driftLines.length) {
191
+ logGlobal('No drift detected; shared dependencies are aligned.', colors.green);
192
+ return;
193
+ }
194
+ console.log(colors.yellow('Drifting dependencies:'));
195
+ driftLines.forEach((line) => console.log(` ${line}`));
196
+ }
197
+ async function checkLockfile(targets, allPackages) {
198
+ if (!ensureVersionControlTargets(targets, allPackages, 'Lockfile check'))
199
+ return;
200
+ const scopeLabel = describeVersionControlScope(targets, allPackages);
201
+ logGlobal(`Verifying lockfile matches the ${scopeLabel} manifests…`, colors.cyan);
202
+ await run('pnpm', ['install', '--frozen-lockfile', '--ignore-scripts']);
203
+ logGlobal('Lockfile verification succeeded.', colors.green);
204
+ }
205
+ export function makeVersionControlEntries(targets, allPackages) {
206
+ return [
207
+ {
208
+ name: 'regex dependency update',
209
+ emoji: '🧷',
210
+ description: 'Update all workspace packages matching a pattern to a version/tag',
211
+ handler: async () => {
212
+ await updateByPattern(targets, allPackages);
213
+ },
214
+ },
215
+ {
216
+ name: 'version health scan',
217
+ emoji: '🩺',
218
+ description: 'Flag conflicting dependency versions across the workspace',
219
+ handler: async () => {
220
+ await runVersionHealthScan(targets, allPackages);
221
+ },
222
+ },
223
+ {
224
+ name: 'dependency alignment',
225
+ emoji: '🎯',
226
+ description: 'Pick a dependency and pin every workspace package to the same version',
227
+ handler: async () => {
228
+ await alignDependencyVersions(targets, allPackages);
229
+ },
230
+ },
231
+ {
232
+ name: 'drift & outdated report',
233
+ emoji: '📉',
234
+ description: 'Summarize distances from the standard version for shared deps',
235
+ handler: async () => {
236
+ await showDriftReport(targets, allPackages);
237
+ },
238
+ },
239
+ {
240
+ name: 'lockfile consistency check',
241
+ emoji: '🧰',
242
+ description: 'Ensure pnpm-lock.yaml stays compatible with the manifest',
243
+ handler: async () => {
244
+ await checkLockfile(targets, allPackages);
245
+ },
246
+ },
247
+ ];
248
+ }
249
+ export { describeVersionControlScope };
package/dist/workspace.js CHANGED
@@ -4,6 +4,7 @@ import { rm } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { run, rootDir } from './utils/run.js';
6
6
  import { logGlobal, logPkg, colors } from './utils/log.js';
7
+ import { packageFilterArg } from './utils/package-filter.js';
7
8
  import { collectGitStatus, gitAdd, gitCommit } from './git.js';
8
9
  import { askLine, promptSingleKey } from './prompts.js';
9
10
  const dependencyFiles = new Set([
@@ -186,13 +187,6 @@ async function promptCommitMessage(proposed) {
186
187
  }
187
188
  return custom.trim();
188
189
  }
189
- function packageFilterArg(pkg) {
190
- if (pkg.name)
191
- return pkg.name;
192
- if (!pkg.relativeDir || pkg.relativeDir === '.')
193
- return '.';
194
- return `./${pkg.relativeDir}`;
195
- }
196
190
  async function runLifecycleScenario(pkg, scenario) {
197
191
  if (pkg) {
198
192
  logPkg(pkg, scenario.singleMessage);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",