@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.
- package/dist/helper-cli/prompts.js +13 -27
- package/dist/menu.js +13 -0
- package/dist/utils/package-filter.js +7 -0
- package/dist/version-control.js +249 -0
- package/dist/workspace.js +1 -7
- package/package.json +1 -1
|
@@ -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,
|
|
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('
|
|
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
|
|
260
|
-
const
|
|
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 +=
|
|
308
|
+
searchQuery += typedChar;
|
|
311
309
|
applySearch();
|
|
312
310
|
return;
|
|
313
311
|
}
|
|
314
312
|
return;
|
|
315
313
|
}
|
|
316
|
-
if (
|
|
317
|
-
const numericValue =
|
|
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 (
|
|
326
|
-
(
|
|
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,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);
|