@emeryld/manager 0.2.0 → 0.2.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/bin/manager-cli.js +1 -3
- package/dist/git.js +68 -0
- package/dist/helper-cli.js +299 -0
- package/dist/menu.js +130 -0
- package/dist/packages.js +281 -0
- package/dist/preflight.js +26 -0
- package/dist/prompts.js +85 -0
- package/dist/publish.js +142 -0
- package/dist/release.js +292 -0
- package/dist/semver.js +17 -0
- package/dist/utils/colors.js +11 -0
- package/dist/utils/log.js +19 -0
- package/dist/utils/run.js +21 -0
- package/dist/workspace.js +246 -0
- package/package.json +5 -5
- package/tsconfig.base.json +0 -1
- package/src/git.ts +0 -74
- package/src/helper-cli.ts +0 -405
- package/src/menu.ts +0 -142
- package/src/packages.ts +0 -305
- package/src/preflight.ts +0 -26
- package/src/prompts.ts +0 -93
- package/src/publish.ts +0 -183
- package/src/release.ts +0 -410
- package/src/semver.ts +0 -27
- package/src/sync-version.mjs +0 -213
- package/src/utils/colors.ts +0 -11
- package/src/utils/log.ts +0 -42
- package/src/utils/run.ts +0 -30
- package/src/workspace.ts +0 -290
package/bin/manager-cli.js
CHANGED
|
@@ -15,9 +15,7 @@ const tsNodeLoader = require.resolve('ts-node/esm.mjs')
|
|
|
15
15
|
const registerCode = [
|
|
16
16
|
'import { register } from "node:module";',
|
|
17
17
|
'import { pathToFileURL } from "node:url";',
|
|
18
|
-
|
|
19
|
-
tsNodeLoader,
|
|
20
|
-
)}, pathToFileURL(${JSON.stringify(entryPoint)}));`,
|
|
18
|
+
`register(${JSON.stringify(tsNodeLoader)}, pathToFileURL(${JSON.stringify(packageRoot + path.sep)}));`,
|
|
21
19
|
].join(' ')
|
|
22
20
|
const registerImport = `data:text/javascript,${encodeURIComponent(registerCode)}`
|
|
23
21
|
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/git.js
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { run } from './utils/run.js';
|
|
4
|
+
import { logGlobal, colors } from './utils/log.js';
|
|
5
|
+
import { rootDir } from './utils/run.js';
|
|
6
|
+
export async function collectGitStatus() {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn('git', ['status', '--porcelain'], {
|
|
9
|
+
cwd: rootDir,
|
|
10
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
11
|
+
});
|
|
12
|
+
const lines = [];
|
|
13
|
+
child.stdout.on('data', (chunk) => {
|
|
14
|
+
lines.push(...chunk
|
|
15
|
+
.toString()
|
|
16
|
+
.split('\n')
|
|
17
|
+
// Preserve leading status markers; strip only trailing whitespace/newlines
|
|
18
|
+
.map((line) => line.replace(/\s+$/, ''))
|
|
19
|
+
.filter(Boolean));
|
|
20
|
+
});
|
|
21
|
+
child.on('error', reject);
|
|
22
|
+
child.on('close', (code) => {
|
|
23
|
+
if (code !== 0)
|
|
24
|
+
return reject(new Error('Failed to read git status.'));
|
|
25
|
+
resolve(lines);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export async function gitAdd(paths) {
|
|
30
|
+
await run('git', ['add', ...paths]);
|
|
31
|
+
}
|
|
32
|
+
export async function gitCommit(message) {
|
|
33
|
+
await run('git', ['commit', '-m', message]);
|
|
34
|
+
}
|
|
35
|
+
export async function hasStagedChanges() {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const child = spawn('git', ['diff', '--cached', '--quiet'], {
|
|
38
|
+
cwd: rootDir,
|
|
39
|
+
stdio: 'ignore',
|
|
40
|
+
});
|
|
41
|
+
child.on('close', (code) => {
|
|
42
|
+
if (code === 0)
|
|
43
|
+
resolve(false);
|
|
44
|
+
else if (code === 1)
|
|
45
|
+
resolve(true);
|
|
46
|
+
else
|
|
47
|
+
reject(new Error('git diff --cached --quiet failed'));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export async function stageCommitPush(changedPaths, commitMessage) {
|
|
52
|
+
if (!changedPaths.length) {
|
|
53
|
+
logGlobal('No files changed; skipping commit.', colors.dim);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const unique = [...new Set(changedPaths)];
|
|
57
|
+
logGlobal('Staging files…', colors.cyan);
|
|
58
|
+
await gitAdd(unique);
|
|
59
|
+
const staged = await hasStagedChanges();
|
|
60
|
+
if (!staged) {
|
|
61
|
+
logGlobal('Nothing to commit; skipping commit/push.', colors.dim);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
logGlobal('Creating commit…', colors.cyan);
|
|
65
|
+
await gitCommit(commitMessage);
|
|
66
|
+
logGlobal('Pushing to origin…', colors.cyan);
|
|
67
|
+
await run('git', ['push']);
|
|
68
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import readline from 'node:readline/promises';
|
|
4
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const rootDir = path.resolve(path.dirname(__filename), '..');
|
|
9
|
+
const managerRequire = createRequire(import.meta.url);
|
|
10
|
+
let tsNodeLoaderPath;
|
|
11
|
+
function getTsNodeLoaderPath() {
|
|
12
|
+
if (!tsNodeLoaderPath) {
|
|
13
|
+
tsNodeLoaderPath = managerRequire.resolve('ts-node/esm.mjs');
|
|
14
|
+
}
|
|
15
|
+
return tsNodeLoaderPath;
|
|
16
|
+
}
|
|
17
|
+
export function buildTsNodeRegisterImport(scriptPath) {
|
|
18
|
+
const loader = getTsNodeLoaderPath();
|
|
19
|
+
const code = [
|
|
20
|
+
'import { register } from "node:module";',
|
|
21
|
+
'import { pathToFileURL } from "node:url";',
|
|
22
|
+
`register(${JSON.stringify(loader)}, pathToFileURL(${JSON.stringify(scriptPath)}));`,
|
|
23
|
+
].join(' ');
|
|
24
|
+
return `data:text/javascript,${encodeURIComponent(code)}`;
|
|
25
|
+
}
|
|
26
|
+
const ansi = (code) => (text) => `\x1b[${code}m${text}\x1b[0m`;
|
|
27
|
+
const colors = {
|
|
28
|
+
cyan: ansi(36),
|
|
29
|
+
green: ansi(32),
|
|
30
|
+
yellow: ansi(33),
|
|
31
|
+
magenta: ansi(35),
|
|
32
|
+
dim: ansi(2),
|
|
33
|
+
};
|
|
34
|
+
function normalizeScripts(entries) {
|
|
35
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
36
|
+
throw new Error('runHelperCli requires at least one script definition.');
|
|
37
|
+
}
|
|
38
|
+
return entries.map((entry, index) => {
|
|
39
|
+
if (!entry || typeof entry !== 'object') {
|
|
40
|
+
throw new Error(`Script entry at index ${index} is not an object.`);
|
|
41
|
+
}
|
|
42
|
+
if (!entry.name || typeof entry.name !== 'string') {
|
|
43
|
+
throw new Error(`Script entry at index ${index} is missing a "name".`);
|
|
44
|
+
}
|
|
45
|
+
const hasHandler = typeof entry.handler === 'function';
|
|
46
|
+
const hasScript = typeof entry.script === 'string' && entry.script.length > 0;
|
|
47
|
+
if (!hasHandler && !hasScript) {
|
|
48
|
+
throw new Error(`Script "${entry.name}" requires either a "script" path or a "handler" function.`);
|
|
49
|
+
}
|
|
50
|
+
const absoluteScript = hasScript && path.isAbsolute(entry.script)
|
|
51
|
+
? entry.script
|
|
52
|
+
: hasScript
|
|
53
|
+
? path.join(rootDir, entry.script)
|
|
54
|
+
: undefined;
|
|
55
|
+
return {
|
|
56
|
+
...entry,
|
|
57
|
+
emoji: entry.emoji ?? '🔧',
|
|
58
|
+
displayName: entry.name.trim(),
|
|
59
|
+
absoluteScript,
|
|
60
|
+
script: hasScript ? entry.script : undefined,
|
|
61
|
+
handler: hasHandler ? entry.handler : undefined,
|
|
62
|
+
metaLabel: entry.description ?? (hasScript ? entry.script : '[callback]'),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function findScriptEntry(entries, key) {
|
|
67
|
+
if (!key)
|
|
68
|
+
return undefined;
|
|
69
|
+
const normalized = key.toLowerCase();
|
|
70
|
+
return entries.find((entry) => {
|
|
71
|
+
const nameMatch = entry.displayName.toLowerCase() === normalized;
|
|
72
|
+
const aliasMatch = entry.displayName.toLowerCase().includes(normalized);
|
|
73
|
+
const scriptMatch = entry.script
|
|
74
|
+
? entry.script.toLowerCase().includes(normalized)
|
|
75
|
+
: false;
|
|
76
|
+
return nameMatch || aliasMatch || scriptMatch;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function printScriptList(entries, title) {
|
|
80
|
+
const heading = title
|
|
81
|
+
? colors.magenta(title)
|
|
82
|
+
: colors.magenta('Available scripts');
|
|
83
|
+
console.log(heading);
|
|
84
|
+
entries.forEach((entry, index) => {
|
|
85
|
+
console.log(` ${colors.cyan(String(index + 1).padStart(2, ' '))} ${entry.emoji} ${entry.displayName} ${colors.dim(entry.metaLabel)}`);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function promptWithReadline(entries, title) {
|
|
89
|
+
printScriptList(entries, title);
|
|
90
|
+
const rl = readline.createInterface({ input, output });
|
|
91
|
+
try {
|
|
92
|
+
while (true) {
|
|
93
|
+
const answer = (await rl.question(colors.cyan('\nSelect a script by number or name: '))).trim();
|
|
94
|
+
if (!answer)
|
|
95
|
+
continue;
|
|
96
|
+
const numeric = Number.parseInt(answer, 10);
|
|
97
|
+
if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= entries.length) {
|
|
98
|
+
return entries[numeric - 1];
|
|
99
|
+
}
|
|
100
|
+
const byName = findScriptEntry(entries, answer);
|
|
101
|
+
if (byName)
|
|
102
|
+
return byName;
|
|
103
|
+
console.log(colors.yellow(`Could not find "${answer}". Try again.`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
rl.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function formatInteractiveLines(entries, selectedIndex, title) {
|
|
111
|
+
const heading = title
|
|
112
|
+
? colors.magenta(title)
|
|
113
|
+
: colors.magenta('Available scripts');
|
|
114
|
+
const lines = [heading];
|
|
115
|
+
entries.forEach((entry, index) => {
|
|
116
|
+
const isSelected = index === selectedIndex;
|
|
117
|
+
const pointer = isSelected ? `${colors.green('➤')} ` : '';
|
|
118
|
+
const numberLabel = colors.cyan(`${index + 1}`.padStart(2, ' '));
|
|
119
|
+
const label = isSelected
|
|
120
|
+
? colors.green(entry.displayName)
|
|
121
|
+
: entry.displayName;
|
|
122
|
+
lines.push(`${pointer}${numberLabel}. ${entry.emoji} ${label} ${colors.dim(entry.metaLabel)}`);
|
|
123
|
+
});
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(colors.dim('Use ↑/↓ (or j/k) to move, digits (1-9,0 for 10) to run instantly, Enter to confirm, Esc/Ctrl+C to exit.'));
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
function renderInteractiveList(lines, previousLineCount) {
|
|
129
|
+
if (previousLineCount > 0) {
|
|
130
|
+
process.stdout.write(`\x1b[${previousLineCount}A`);
|
|
131
|
+
process.stdout.write('\x1b[0J');
|
|
132
|
+
}
|
|
133
|
+
lines.forEach((line) => console.log(line));
|
|
134
|
+
return lines.length;
|
|
135
|
+
}
|
|
136
|
+
async function promptForScript(entries, title) {
|
|
137
|
+
const supportsRawMode = typeof input.setRawMode === 'function' && input.isTTY;
|
|
138
|
+
if (!supportsRawMode) {
|
|
139
|
+
return promptWithReadline(entries, title);
|
|
140
|
+
}
|
|
141
|
+
const wasRaw = input.isRaw;
|
|
142
|
+
if (!wasRaw) {
|
|
143
|
+
input.setRawMode(true);
|
|
144
|
+
input.resume();
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write('\x1b[?25l');
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
let selectedIndex = 0;
|
|
149
|
+
let renderedLines = 0;
|
|
150
|
+
const cleanup = () => {
|
|
151
|
+
if (renderedLines > 0) {
|
|
152
|
+
process.stdout.write(`\x1b[${renderedLines}A`);
|
|
153
|
+
process.stdout.write('\x1b[0J');
|
|
154
|
+
renderedLines = 0;
|
|
155
|
+
}
|
|
156
|
+
process.stdout.write('\x1b[?25h');
|
|
157
|
+
if (!wasRaw) {
|
|
158
|
+
input.setRawMode(false);
|
|
159
|
+
input.pause();
|
|
160
|
+
}
|
|
161
|
+
input.removeListener('data', onData);
|
|
162
|
+
};
|
|
163
|
+
const commitSelection = (entry) => {
|
|
164
|
+
cleanup();
|
|
165
|
+
console.log();
|
|
166
|
+
resolve(entry);
|
|
167
|
+
};
|
|
168
|
+
const render = () => {
|
|
169
|
+
const lines = formatInteractiveLines(entries, selectedIndex, title);
|
|
170
|
+
renderedLines = renderInteractiveList(lines, renderedLines);
|
|
171
|
+
};
|
|
172
|
+
const onData = (buffer) => {
|
|
173
|
+
const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
|
|
174
|
+
const isArrowDown = buffer.equals(Buffer.from([0x1b, 0x5b, 0x42]));
|
|
175
|
+
const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
|
|
176
|
+
const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
|
|
177
|
+
const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
|
|
178
|
+
if (isCtrlC || isEscape) {
|
|
179
|
+
cleanup();
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
if (isArrowUp ||
|
|
183
|
+
(buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
|
|
184
|
+
selectedIndex = (selectedIndex - 1 + entries.length) % entries.length;
|
|
185
|
+
render();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (isArrowDown ||
|
|
189
|
+
(buffer.length === 1 && (buffer[0] === 0x6a || buffer[0] === 0x4a))) {
|
|
190
|
+
selectedIndex = (selectedIndex + 1) % entries.length;
|
|
191
|
+
render();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (isEnter) {
|
|
195
|
+
commitSelection(entries[selectedIndex]);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (buffer.length === 1 && buffer[0] >= 0x30 && buffer[0] <= 0x39) {
|
|
199
|
+
const numericValue = buffer[0] === 0x30 ? 10 : buffer[0] - 0x30;
|
|
200
|
+
const selected = numericValue - 1;
|
|
201
|
+
if (selected >= 0 && selected < entries.length) {
|
|
202
|
+
commitSelection(entries[selected]);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
process.stdout.write('\x07');
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (buffer.length === 1 &&
|
|
210
|
+
((buffer[0] >= 0x41 && buffer[0] <= 0x5a) ||
|
|
211
|
+
(buffer[0] >= 0x61 && buffer[0] <= 0x7a))) {
|
|
212
|
+
const char = String.fromCharCode(buffer[0]).toLowerCase();
|
|
213
|
+
const foundIndex = entries.findIndex((entry) => entry.displayName.toLowerCase().startsWith(char));
|
|
214
|
+
if (foundIndex !== -1) {
|
|
215
|
+
selectedIndex = foundIndex;
|
|
216
|
+
render();
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
process.stdout.write('\x07');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
input.on('data', onData);
|
|
224
|
+
render();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function runEntry(entry, forwardedArgs) {
|
|
228
|
+
const detail = entry.script
|
|
229
|
+
? path.relative(rootDir, entry.absoluteScript ?? entry.script)
|
|
230
|
+
: (entry.metaLabel ?? '[callback]');
|
|
231
|
+
console.log(`${entry.emoji} ${colors.green(`Running "${entry.displayName}"`)} ${colors.dim(detail)}`);
|
|
232
|
+
if (entry.handler) {
|
|
233
|
+
return Promise.resolve(entry.handler({ args: forwardedArgs, entry, rootDir }));
|
|
234
|
+
}
|
|
235
|
+
if (!entry.absoluteScript) {
|
|
236
|
+
throw new Error(`Script "${entry.displayName}" is missing a resolved path.`);
|
|
237
|
+
}
|
|
238
|
+
const scriptPath = entry.absoluteScript;
|
|
239
|
+
const tsConfigPath = path.join(rootDir, 'tsconfig.base.json');
|
|
240
|
+
const extension = path.extname(scriptPath).toLowerCase();
|
|
241
|
+
const isTypeScript = extension === '.js' || extension === '.mts' || extension === '.cts';
|
|
242
|
+
const command = process.execPath;
|
|
243
|
+
const execArgs = isTypeScript
|
|
244
|
+
? [
|
|
245
|
+
'--import',
|
|
246
|
+
buildTsNodeRegisterImport(scriptPath),
|
|
247
|
+
scriptPath,
|
|
248
|
+
...forwardedArgs,
|
|
249
|
+
]
|
|
250
|
+
: [scriptPath, ...forwardedArgs];
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
const child = spawn(command, execArgs, {
|
|
253
|
+
cwd: rootDir,
|
|
254
|
+
stdio: 'inherit',
|
|
255
|
+
env: isTypeScript
|
|
256
|
+
? { ...process.env, TS_NODE_PROJECT: tsConfigPath }
|
|
257
|
+
: process.env,
|
|
258
|
+
shell: process.platform === 'win32' && isTypeScript,
|
|
259
|
+
});
|
|
260
|
+
child.on('close', (code) => {
|
|
261
|
+
if (code === 0)
|
|
262
|
+
resolve();
|
|
263
|
+
else
|
|
264
|
+
reject(new Error(`Script "${entry.displayName}" exited with code ${code}`));
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
export async function runHelperCli({ scripts, title = 'Helper CLI', argv = process.argv.slice(2), }) {
|
|
269
|
+
const normalized = normalizeScripts(scripts);
|
|
270
|
+
let args = Array.isArray(argv) ? [...argv] : [];
|
|
271
|
+
const separatorIndex = args.indexOf('--');
|
|
272
|
+
if (separatorIndex !== -1) {
|
|
273
|
+
args = [...args.slice(0, separatorIndex), ...args.slice(separatorIndex + 1)];
|
|
274
|
+
}
|
|
275
|
+
const [firstArg, ...restArgs] = args;
|
|
276
|
+
if (firstArg === '--list' || firstArg === '-l') {
|
|
277
|
+
printScriptList(normalized, title);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (firstArg === '--help' || firstArg === '-h') {
|
|
281
|
+
console.log(colors.magenta('Usage:'));
|
|
282
|
+
console.log(' pnpm run <cli> [script] [...args]');
|
|
283
|
+
console.log('\nFlags:');
|
|
284
|
+
console.log(' --list Show available scripts');
|
|
285
|
+
console.log(' --help Show this information');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const argLooksLikeScript = firstArg && !firstArg.startsWith('-');
|
|
289
|
+
if (argLooksLikeScript) {
|
|
290
|
+
const requested = findScriptEntry(normalized, firstArg);
|
|
291
|
+
if (requested) {
|
|
292
|
+
await runEntry(requested, restArgs);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
console.log(colors.yellow(`Unknown script "${firstArg}". Falling back to interactive selection…`));
|
|
296
|
+
}
|
|
297
|
+
const selection = await promptForScript(normalized, title);
|
|
298
|
+
await runEntry(selection, []);
|
|
299
|
+
}
|
package/dist/menu.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { colors, globalEmoji } from './utils/log.js';
|
|
2
|
+
import { updateDependencies, testAll, testSingle, buildAll, buildSingle, } from './workspace.js';
|
|
3
|
+
import { releaseMultiple, releaseSingle } from './release.js';
|
|
4
|
+
import { getOrderedPackages } from './packages.js';
|
|
5
|
+
import { runHelperCli } from './helper-cli.js';
|
|
6
|
+
import { ensureWorkingTreeCommitted } from './preflight.js';
|
|
7
|
+
export function makeStepEntries(targets, packages, state) {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
name: 'update dependencies',
|
|
11
|
+
emoji: '♻️',
|
|
12
|
+
description: 'Update dependencies for the selection',
|
|
13
|
+
handler: async () => {
|
|
14
|
+
await updateDependencies(targets);
|
|
15
|
+
state.lastStep = 'update';
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'test',
|
|
20
|
+
emoji: '🧪',
|
|
21
|
+
description: 'Run tests',
|
|
22
|
+
handler: async () => {
|
|
23
|
+
if (targets.length === 1)
|
|
24
|
+
await testSingle(targets[0]);
|
|
25
|
+
else
|
|
26
|
+
await testAll();
|
|
27
|
+
state.lastStep = 'test';
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'build',
|
|
32
|
+
emoji: '🏗️',
|
|
33
|
+
description: 'Build packages',
|
|
34
|
+
handler: async () => {
|
|
35
|
+
if (targets.length === 1)
|
|
36
|
+
await buildSingle(targets[0]);
|
|
37
|
+
else
|
|
38
|
+
await buildAll();
|
|
39
|
+
state.lastStep = 'build';
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'publish',
|
|
44
|
+
emoji: '🚀',
|
|
45
|
+
description: 'Version, commit, and publish',
|
|
46
|
+
handler: async () => {
|
|
47
|
+
await ensureWorkingTreeCommitted();
|
|
48
|
+
if (targets.length > 1)
|
|
49
|
+
await releaseMultiple(targets, packages);
|
|
50
|
+
else
|
|
51
|
+
await releaseSingle(targets[0], packages);
|
|
52
|
+
state.lastStep = 'publish';
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'full',
|
|
57
|
+
emoji: '✅',
|
|
58
|
+
description: 'update → test → build → publish',
|
|
59
|
+
handler: async () => {
|
|
60
|
+
await updateDependencies(targets);
|
|
61
|
+
if (targets.length === 1)
|
|
62
|
+
await testSingle(targets[0]);
|
|
63
|
+
else
|
|
64
|
+
await testAll();
|
|
65
|
+
if (targets.length === 1)
|
|
66
|
+
await buildSingle(targets[0]);
|
|
67
|
+
else
|
|
68
|
+
await buildAll();
|
|
69
|
+
await ensureWorkingTreeCommitted();
|
|
70
|
+
if (targets.length > 1)
|
|
71
|
+
await releaseMultiple(targets, packages);
|
|
72
|
+
else
|
|
73
|
+
await releaseSingle(targets[0], packages);
|
|
74
|
+
state.lastStep = 'full';
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'back',
|
|
79
|
+
emoji: '↩️',
|
|
80
|
+
description: 'Pick packages again',
|
|
81
|
+
handler: async () => {
|
|
82
|
+
state.lastStep = 'back';
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
export function buildPackageSelectionMenu(packages, onStepComplete) {
|
|
88
|
+
const ordered = getOrderedPackages(packages);
|
|
89
|
+
const entries = ordered.map((pkg) => ({
|
|
90
|
+
name: pkg.substitute,
|
|
91
|
+
emoji: colors[pkg.color]('●'),
|
|
92
|
+
description: pkg.name ?? pkg.dirName,
|
|
93
|
+
handler: async () => {
|
|
94
|
+
const step = await runStepLoop([pkg], packages);
|
|
95
|
+
onStepComplete?.(step);
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
98
|
+
entries.push({
|
|
99
|
+
name: 'All packages',
|
|
100
|
+
emoji: globalEmoji,
|
|
101
|
+
description: 'Select all packages',
|
|
102
|
+
handler: async () => {
|
|
103
|
+
const step = await runStepLoop(ordered, packages);
|
|
104
|
+
onStepComplete?.(step);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
return entries;
|
|
108
|
+
}
|
|
109
|
+
// IMPORTANT FIX: do not auto-pass argv derived from lastStep.
|
|
110
|
+
// This removes the infinite loop and returns to the step menu after each run.
|
|
111
|
+
export async function runStepLoop(targets, packages) {
|
|
112
|
+
const state = {};
|
|
113
|
+
// Loop shows the step menu and executes a chosen action once.
|
|
114
|
+
// After the handler completes, show the menu again.
|
|
115
|
+
// Selecting "back" breaks and returns control to the package picker.
|
|
116
|
+
// No argv auto-execution here.
|
|
117
|
+
// eslint-disable-next-line no-constant-condition
|
|
118
|
+
while (true) {
|
|
119
|
+
state.lastStep = undefined;
|
|
120
|
+
const entries = makeStepEntries(targets, packages, state);
|
|
121
|
+
await runHelperCli({
|
|
122
|
+
title: `Actions for ${targets.length === 1 ? targets[0].name : 'selected packages'}`,
|
|
123
|
+
scripts: entries,
|
|
124
|
+
argv: [], // <- key change
|
|
125
|
+
});
|
|
126
|
+
if (state.lastStep === 'back')
|
|
127
|
+
return state.lastStep;
|
|
128
|
+
// keep looping to show menu again
|
|
129
|
+
}
|
|
130
|
+
}
|