@emeryld/manager 0.6.3 → 0.6.5
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/colors-shared.js +11 -0
- package/dist/create-package/index.js +27 -131
- package/dist/helper-cli/colors.js +7 -0
- package/dist/helper-cli/display.js +49 -0
- package/dist/helper-cli/env.js +14 -0
- package/dist/helper-cli/pagination.js +41 -0
- package/dist/helper-cli/prompts.js +229 -0
- package/dist/helper-cli/runner.js +59 -0
- package/dist/helper-cli/scripts.js +47 -0
- package/dist/helper-cli/ts-node.js +10 -0
- package/dist/helper-cli/types.js +1 -0
- package/dist/helper-cli.js +9 -283
- package/dist/menu/script-helpers.js +178 -0
- package/dist/menu.js +78 -19
- package/dist/packages/manifest-utils.js +145 -0
- package/dist/packages.js +6 -150
- package/dist/sync-version.mjs +63 -0
- package/dist/utils/colors.js +1 -10
- package/dist/workspace.js +88 -14
- package/package.json +1 -1
- package/tsconfig.base.json +1 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import { askLine, promptSingleKey } from '../prompts.js';
|
|
3
|
+
import { askLine } from '../prompts.js';
|
|
6
4
|
import { colors, logGlobal } from '../utils/log.js';
|
|
5
|
+
import { run } from '../utils/run.js';
|
|
7
6
|
import { runHelperCli } from '../helper-cli.js';
|
|
8
7
|
import { loadPackages } from '../packages.js';
|
|
9
8
|
import { SCRIPT_DESCRIPTIONS, workspaceRoot, ensureWorkspaceToolingFiles, } from './shared.js';
|
|
@@ -257,133 +256,26 @@ async function ensureTargetDir(targetDir, options) {
|
|
|
257
256
|
throw error;
|
|
258
257
|
}
|
|
259
258
|
}
|
|
260
|
-
async function runCommand(cmd, args, cwd = workspaceRoot) {
|
|
261
|
-
await new Promise((resolve, reject) => {
|
|
262
|
-
const child = spawn(cmd, args, {
|
|
263
|
-
cwd,
|
|
264
|
-
stdio: 'inherit',
|
|
265
|
-
shell: process.platform === 'win32',
|
|
266
|
-
});
|
|
267
|
-
child.on('exit', (code) => {
|
|
268
|
-
if (code === 0)
|
|
269
|
-
resolve();
|
|
270
|
-
else
|
|
271
|
-
reject(new Error(`${cmd} ${args.join(' ')} exited with ${code}`));
|
|
272
|
-
});
|
|
273
|
-
child.on('error', (err) => reject(err));
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
function formatVariantLines(variants, selected) {
|
|
277
|
-
const heading = colors.magenta('Available templates');
|
|
278
|
-
const lines = [heading];
|
|
279
|
-
variants.forEach((variant, index) => {
|
|
280
|
-
const isSelected = index === selected;
|
|
281
|
-
const pointer = isSelected ? `${colors.green('➤')} ` : ' ';
|
|
282
|
-
const numberLabel = colors.cyan(String(index + 1).padStart(2, ' '));
|
|
283
|
-
const label = isSelected ? colors.green(variant.label) : variant.label;
|
|
284
|
-
const meta = colors.dim(variant.defaultDir);
|
|
285
|
-
lines.push(`${pointer}${numberLabel}. ${label} ${meta}`);
|
|
286
|
-
});
|
|
287
|
-
lines.push('');
|
|
288
|
-
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.'));
|
|
289
|
-
return lines;
|
|
290
|
-
}
|
|
291
|
-
function renderInteractiveList(lines, previousLineCount) {
|
|
292
|
-
if (previousLineCount > 0) {
|
|
293
|
-
process.stdout.write(`\x1b[${previousLineCount}A`);
|
|
294
|
-
process.stdout.write('\x1b[0J');
|
|
295
|
-
}
|
|
296
|
-
lines.forEach((line) => console.log(line));
|
|
297
|
-
return lines.length;
|
|
298
|
-
}
|
|
299
259
|
async function promptForVariant() {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
260
|
+
let selection;
|
|
261
|
+
const scripts = VARIANTS.map((variant) => ({
|
|
262
|
+
name: variant.label,
|
|
263
|
+
emoji: '✨',
|
|
264
|
+
description: variant.summary
|
|
265
|
+
? `${variant.summary} · ${variant.defaultDir}`
|
|
266
|
+
: variant.defaultDir,
|
|
267
|
+
handler: () => {
|
|
268
|
+
selection = variant;
|
|
269
|
+
},
|
|
270
|
+
}));
|
|
271
|
+
while (!selection) {
|
|
272
|
+
await runHelperCli({
|
|
273
|
+
title: 'Pick a package template',
|
|
274
|
+
scripts,
|
|
275
|
+
argv: [],
|
|
313
276
|
});
|
|
314
277
|
}
|
|
315
|
-
|
|
316
|
-
if (!wasRaw) {
|
|
317
|
-
input.setRawMode(true);
|
|
318
|
-
input.resume();
|
|
319
|
-
}
|
|
320
|
-
process.stdout.write('\x1b[?25l');
|
|
321
|
-
return new Promise((resolve) => {
|
|
322
|
-
let selectedIndex = 0;
|
|
323
|
-
let renderedLines = 0;
|
|
324
|
-
const cleanup = () => {
|
|
325
|
-
if (renderedLines > 0) {
|
|
326
|
-
process.stdout.write(`\x1b[${renderedLines}A`);
|
|
327
|
-
process.stdout.write('\x1b[0J');
|
|
328
|
-
renderedLines = 0;
|
|
329
|
-
}
|
|
330
|
-
process.stdout.write('\x1b[?25h');
|
|
331
|
-
if (!wasRaw) {
|
|
332
|
-
input.setRawMode(false);
|
|
333
|
-
input.pause();
|
|
334
|
-
}
|
|
335
|
-
input.removeListener('data', onData);
|
|
336
|
-
};
|
|
337
|
-
const commitSelection = (variant) => {
|
|
338
|
-
cleanup();
|
|
339
|
-
console.log();
|
|
340
|
-
resolve(variant);
|
|
341
|
-
};
|
|
342
|
-
const render = () => {
|
|
343
|
-
const lines = formatVariantLines(VARIANTS, selectedIndex);
|
|
344
|
-
renderedLines = renderInteractiveList(lines, renderedLines);
|
|
345
|
-
};
|
|
346
|
-
const onData = (buffer) => {
|
|
347
|
-
const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
|
|
348
|
-
const isArrowDown = buffer.equals(Buffer.from([0x1b, 0x5b, 0x42]));
|
|
349
|
-
const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
|
|
350
|
-
const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
|
|
351
|
-
const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
|
|
352
|
-
if (isCtrlC || isEscape) {
|
|
353
|
-
cleanup();
|
|
354
|
-
process.exit(1);
|
|
355
|
-
}
|
|
356
|
-
if (isArrowUp ||
|
|
357
|
-
(buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
|
|
358
|
-
selectedIndex = (selectedIndex - 1 + VARIANTS.length) % VARIANTS.length;
|
|
359
|
-
render();
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
if (isArrowDown ||
|
|
363
|
-
(buffer.length === 1 && (buffer[0] === 0x6a || buffer[0] === 0x4a))) {
|
|
364
|
-
selectedIndex = (selectedIndex + 1) % VARIANTS.length;
|
|
365
|
-
render();
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
if (buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a)) {
|
|
369
|
-
commitSelection(VARIANTS[selectedIndex]);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
if (buffer.length === 1 && buffer[0] >= 0x30 && buffer[0] <= 0x39) {
|
|
373
|
-
const numericValue = buffer[0] === 0x30 ? 10 : buffer[0] - 0x30;
|
|
374
|
-
const idx = numericValue - 1;
|
|
375
|
-
if (idx >= 0 && idx < VARIANTS.length) {
|
|
376
|
-
commitSelection(VARIANTS[idx]);
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
process.stdout.write('\x07');
|
|
380
|
-
}
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
input.on('data', onData);
|
|
385
|
-
render();
|
|
386
|
-
});
|
|
278
|
+
return selection;
|
|
387
279
|
}
|
|
388
280
|
async function promptForTargetDir(fallback) {
|
|
389
281
|
const answer = await askLine(`Path for the new package? (${fallback}): `);
|
|
@@ -401,7 +293,7 @@ async function postCreateTasks(targetDir, options) {
|
|
|
401
293
|
}
|
|
402
294
|
try {
|
|
403
295
|
logGlobal('Running pnpm install…', colors.cyan);
|
|
404
|
-
await
|
|
296
|
+
await run('pnpm', ['install'], { cwd: workspaceRoot });
|
|
405
297
|
}
|
|
406
298
|
catch (error) {
|
|
407
299
|
logGlobal(`pnpm install failed: ${error instanceof Error ? error.message : String(error)}`, colors.yellow);
|
|
@@ -415,7 +307,9 @@ async function postCreateTasks(targetDir, options) {
|
|
|
415
307
|
try {
|
|
416
308
|
if (options?.pkgName) {
|
|
417
309
|
logGlobal(`Building workspace deps for ${options.pkgName}…`, colors.cyan);
|
|
418
|
-
await
|
|
310
|
+
await run('pnpm', ['-r', '--filter', `${options.pkgName}...`, 'build'], {
|
|
311
|
+
cwd: workspaceRoot,
|
|
312
|
+
});
|
|
419
313
|
return;
|
|
420
314
|
}
|
|
421
315
|
}
|
|
@@ -425,7 +319,7 @@ async function postCreateTasks(targetDir, options) {
|
|
|
425
319
|
// Fallback: build everything
|
|
426
320
|
try {
|
|
427
321
|
logGlobal('Building full workspace…', colors.cyan);
|
|
428
|
-
await
|
|
322
|
+
await run('pnpm', ['-r', 'build'], { cwd: workspaceRoot });
|
|
429
323
|
return;
|
|
430
324
|
}
|
|
431
325
|
catch (error) {
|
|
@@ -438,7 +332,9 @@ async function postCreateTasks(targetDir, options) {
|
|
|
438
332
|
const pkg = JSON.parse(pkgRaw);
|
|
439
333
|
if (pkg.scripts?.build) {
|
|
440
334
|
logGlobal('Running pnpm run build for the new package…', colors.cyan);
|
|
441
|
-
await
|
|
335
|
+
await run('pnpm', ['-C', targetDir, 'run', 'build'], {
|
|
336
|
+
cwd: workspaceRoot,
|
|
337
|
+
});
|
|
442
338
|
}
|
|
443
339
|
}
|
|
444
340
|
catch (error) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { colors, getEntryColor } from './colors.js';
|
|
2
|
+
import { BACK_KEY, NEXT_KEY, PAGE_SIZE, PREVIOUS_KEY } from './pagination.js';
|
|
3
|
+
export function pageHeading(title, page, pageCount) {
|
|
4
|
+
const heading = title
|
|
5
|
+
? colors.magenta(title)
|
|
6
|
+
: colors.magenta('Available scripts');
|
|
7
|
+
if (page === undefined || pageCount === undefined || pageCount <= 1) {
|
|
8
|
+
return heading;
|
|
9
|
+
}
|
|
10
|
+
return `${heading} ${colors.dim(`(page ${page + 1}/${pageCount})`)}`;
|
|
11
|
+
}
|
|
12
|
+
export function printScriptList(entries, title) {
|
|
13
|
+
const heading = pageHeading(title);
|
|
14
|
+
console.log(heading);
|
|
15
|
+
entries.forEach((entry, index) => {
|
|
16
|
+
const colorizer = entry.color ? getEntryColor(entry) : colors.cyan;
|
|
17
|
+
const label = entry.color ? colorizer(entry.displayName) : entry.displayName;
|
|
18
|
+
console.log(` ${colorizer(String(index + 1).padStart(2, ' '))} ${entry.emoji} ${label} ${colors.dim(entry.metaLabel)}`);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function printPaginatedScriptList(state, title) {
|
|
22
|
+
console.log(pageHeading(title, state.page, state.pageCount));
|
|
23
|
+
state.options.forEach((option) => {
|
|
24
|
+
const colorizer = option.type === 'entry' && option.entry.color
|
|
25
|
+
? getEntryColor(option.entry)
|
|
26
|
+
: colors.cyan;
|
|
27
|
+
const numberLabel = colorizer(String(option.hotkey).padStart(2, ' '));
|
|
28
|
+
if (option.type === 'entry') {
|
|
29
|
+
const label = option.entry.color
|
|
30
|
+
? colorizer(option.entry.displayName)
|
|
31
|
+
: option.entry.displayName;
|
|
32
|
+
console.log(` ${numberLabel} ${option.entry.emoji} ${label} ${colors.dim(option.entry.metaLabel)}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const icon = option.action === 'back'
|
|
36
|
+
? '↩️'
|
|
37
|
+
: option.action === 'previous'
|
|
38
|
+
? '⬅️'
|
|
39
|
+
: '➡️';
|
|
40
|
+
const label = option.action === 'previous'
|
|
41
|
+
? 'Previous page'
|
|
42
|
+
: option.action === 'next'
|
|
43
|
+
? 'Next page'
|
|
44
|
+
: 'Back';
|
|
45
|
+
const display = option.enabled ? label : colors.dim(label);
|
|
46
|
+
console.log(` ${numberLabel} ${icon} ${display}`);
|
|
47
|
+
});
|
|
48
|
+
console.log(colors.dim(`Enter 1-${PAGE_SIZE} to run, ${PREVIOUS_KEY} for previous page, ${NEXT_KEY} for next page, ${BACK_KEY} to go back, or type a name.`));
|
|
49
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
export const managerRoot = path.resolve(path.dirname(__filename), '..', '..');
|
|
6
|
+
export const rootDir = process.cwd();
|
|
7
|
+
export const managerRequire = createRequire(import.meta.url);
|
|
8
|
+
let tsNodeLoaderPath;
|
|
9
|
+
export function getTsNodeLoaderPath() {
|
|
10
|
+
if (!tsNodeLoaderPath) {
|
|
11
|
+
tsNodeLoaderPath = managerRequire.resolve('ts-node/esm.mjs');
|
|
12
|
+
}
|
|
13
|
+
return tsNodeLoaderPath;
|
|
14
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const PAGE_SIZE = 7;
|
|
2
|
+
export const PREVIOUS_KEY = 8;
|
|
3
|
+
export const NEXT_KEY = 9;
|
|
4
|
+
export const BACK_KEY = 0;
|
|
5
|
+
export function buildVisibleOptions(entries, page) {
|
|
6
|
+
const pageCount = Math.max(1, Math.ceil(entries.length / PAGE_SIZE));
|
|
7
|
+
const safePage = Math.min(Math.max(page, 0), pageCount - 1);
|
|
8
|
+
const start = safePage * PAGE_SIZE;
|
|
9
|
+
const pageEntries = entries.slice(start, start + PAGE_SIZE);
|
|
10
|
+
const options = pageEntries.map((entry, index) => ({
|
|
11
|
+
type: 'entry',
|
|
12
|
+
entry,
|
|
13
|
+
hotkey: index + 1,
|
|
14
|
+
absoluteIndex: start + index,
|
|
15
|
+
}));
|
|
16
|
+
const hasPrevious = safePage > 0;
|
|
17
|
+
const hasNext = safePage < pageCount - 1;
|
|
18
|
+
options.push({
|
|
19
|
+
type: 'nav',
|
|
20
|
+
action: 'previous',
|
|
21
|
+
hotkey: PREVIOUS_KEY,
|
|
22
|
+
enabled: hasPrevious,
|
|
23
|
+
});
|
|
24
|
+
options.push({
|
|
25
|
+
type: 'nav',
|
|
26
|
+
action: 'next',
|
|
27
|
+
hotkey: NEXT_KEY,
|
|
28
|
+
enabled: hasNext,
|
|
29
|
+
});
|
|
30
|
+
options.push({
|
|
31
|
+
type: 'nav',
|
|
32
|
+
action: 'back',
|
|
33
|
+
hotkey: BACK_KEY,
|
|
34
|
+
enabled: true,
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
options,
|
|
38
|
+
page: safePage,
|
|
39
|
+
pageCount,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
import { colors, getEntryColor } from './colors.js';
|
|
4
|
+
import { pageHeading, printPaginatedScriptList } from './display.js';
|
|
5
|
+
import { BACK_KEY, buildVisibleOptions, NEXT_KEY, PAGE_SIZE, PREVIOUS_KEY, } from './pagination.js';
|
|
6
|
+
import { findScriptEntry } from './scripts.js';
|
|
7
|
+
async function promptWithReadline(entries, title) {
|
|
8
|
+
let page = 0;
|
|
9
|
+
const rl = readline.createInterface({ input, output });
|
|
10
|
+
try {
|
|
11
|
+
while (true) {
|
|
12
|
+
const state = buildVisibleOptions(entries, page);
|
|
13
|
+
page = state.page;
|
|
14
|
+
printPaginatedScriptList(state, title);
|
|
15
|
+
const answer = (await rl.question(colors.cyan('\nSelect an option by number or name: '))).trim();
|
|
16
|
+
if (!answer)
|
|
17
|
+
continue;
|
|
18
|
+
const numeric = Number.parseInt(answer, 10);
|
|
19
|
+
if (!Number.isNaN(numeric)) {
|
|
20
|
+
const option = state.options.find((opt) => opt.hotkey === numeric);
|
|
21
|
+
if (!option) {
|
|
22
|
+
console.log(colors.yellow(`Unknown option "${answer}". Try again.`));
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (option.type === 'entry') {
|
|
26
|
+
return option.entry;
|
|
27
|
+
}
|
|
28
|
+
if (option.action === 'back') {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (option.action === 'previous') {
|
|
32
|
+
if (option.enabled)
|
|
33
|
+
page = Math.max(0, state.page - 1);
|
|
34
|
+
else
|
|
35
|
+
console.log(colors.yellow('No previous page.'));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (option.action === 'next') {
|
|
39
|
+
if (option.enabled)
|
|
40
|
+
page = Math.min(state.pageCount - 1, state.page + 1);
|
|
41
|
+
else
|
|
42
|
+
console.log(colors.yellow('No next page.'));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const byName = findScriptEntry(entries, answer);
|
|
47
|
+
if (byName)
|
|
48
|
+
return byName;
|
|
49
|
+
console.log(colors.yellow(`Could not find "${answer}". Try again.`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
rl.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function formatInteractiveLines(state, selectedIndex, title) {
|
|
57
|
+
const heading = pageHeading(title, state.page, state.pageCount);
|
|
58
|
+
const lines = [heading];
|
|
59
|
+
state.options.forEach((option, index) => {
|
|
60
|
+
const isSelected = index === selectedIndex;
|
|
61
|
+
const pointer = isSelected ? `${colors.green('➤')} ` : '';
|
|
62
|
+
const colorizer = option.type === 'entry' && option.entry.color
|
|
63
|
+
? getEntryColor(option.entry)
|
|
64
|
+
: colors.cyan;
|
|
65
|
+
const numberLabel = colorizer(`${option.hotkey}`.padStart(2, ' '));
|
|
66
|
+
if (option.type === 'entry') {
|
|
67
|
+
const label = option.entry.color
|
|
68
|
+
? colorizer(option.entry.displayName)
|
|
69
|
+
: isSelected
|
|
70
|
+
? colors.green(option.entry.displayName)
|
|
71
|
+
: option.entry.displayName;
|
|
72
|
+
lines.push(`${pointer}${numberLabel}. ${option.entry.emoji} ${label} ${colors.dim(option.entry.metaLabel)}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const icon = option.action === 'back'
|
|
76
|
+
? '↩️'
|
|
77
|
+
: option.action === 'previous'
|
|
78
|
+
? '⬅️'
|
|
79
|
+
: '➡️';
|
|
80
|
+
const baseLabel = option.action === 'previous'
|
|
81
|
+
? 'Previous page'
|
|
82
|
+
: option.action === 'next'
|
|
83
|
+
? 'Next page'
|
|
84
|
+
: 'Back';
|
|
85
|
+
const navLabel = option.enabled ? baseLabel : colors.dim(baseLabel);
|
|
86
|
+
lines.push(`${pointer}${numberLabel}. ${icon} ${navLabel}`);
|
|
87
|
+
});
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push(colors.dim(`Use ↑/↓ (or j/k) to move, 1-${PAGE_SIZE} to run, ${PREVIOUS_KEY} prev page, ${NEXT_KEY} next page, ${BACK_KEY} back, Enter to confirm, Esc/Ctrl+C to exit.`));
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
function renderInteractiveList(lines, previousLineCount) {
|
|
93
|
+
if (previousLineCount > 0) {
|
|
94
|
+
process.stdout.write(`\x1b[${previousLineCount}A`);
|
|
95
|
+
process.stdout.write('\x1b[0J');
|
|
96
|
+
}
|
|
97
|
+
lines.forEach((line) => console.log(line));
|
|
98
|
+
return lines.length;
|
|
99
|
+
}
|
|
100
|
+
export async function promptForScript(entries, title) {
|
|
101
|
+
const supportsRawMode = typeof input.setRawMode === 'function' && input.isTTY;
|
|
102
|
+
if (!supportsRawMode) {
|
|
103
|
+
return promptWithReadline(entries, title);
|
|
104
|
+
}
|
|
105
|
+
const wasRaw = input.isRaw;
|
|
106
|
+
if (!wasRaw) {
|
|
107
|
+
input.setRawMode(true);
|
|
108
|
+
input.resume();
|
|
109
|
+
}
|
|
110
|
+
process.stdout.write('\x1b[?25l');
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
let selectedIndex = 0;
|
|
113
|
+
let renderedLines = 0;
|
|
114
|
+
let state = buildVisibleOptions(entries, 0);
|
|
115
|
+
const cleanup = () => {
|
|
116
|
+
if (renderedLines > 0) {
|
|
117
|
+
process.stdout.write(`\x1b[${renderedLines}A`);
|
|
118
|
+
process.stdout.write('\x1b[0J');
|
|
119
|
+
renderedLines = 0;
|
|
120
|
+
}
|
|
121
|
+
process.stdout.write('\x1b[?25h');
|
|
122
|
+
if (!wasRaw) {
|
|
123
|
+
input.setRawMode(false);
|
|
124
|
+
input.pause();
|
|
125
|
+
}
|
|
126
|
+
input.removeListener('data', onData);
|
|
127
|
+
};
|
|
128
|
+
const commitSelection = (entry) => {
|
|
129
|
+
cleanup();
|
|
130
|
+
console.log();
|
|
131
|
+
resolve(entry);
|
|
132
|
+
};
|
|
133
|
+
const commitBack = () => {
|
|
134
|
+
cleanup();
|
|
135
|
+
console.log();
|
|
136
|
+
resolve(undefined);
|
|
137
|
+
};
|
|
138
|
+
const setPage = (page) => {
|
|
139
|
+
state = buildVisibleOptions(entries, page);
|
|
140
|
+
selectedIndex = Math.min(selectedIndex, state.options.length - 1);
|
|
141
|
+
selectedIndex = Math.max(0, selectedIndex);
|
|
142
|
+
render();
|
|
143
|
+
};
|
|
144
|
+
const handleNav = (option) => {
|
|
145
|
+
if (option.action === 'back') {
|
|
146
|
+
commitBack();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (option.action === 'previous') {
|
|
150
|
+
if (option.enabled)
|
|
151
|
+
setPage(state.page - 1);
|
|
152
|
+
else
|
|
153
|
+
process.stdout.write('\x07');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (option.action === 'next') {
|
|
157
|
+
if (option.enabled)
|
|
158
|
+
setPage(state.page + 1);
|
|
159
|
+
else
|
|
160
|
+
process.stdout.write('\x07');
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const activateOption = (option) => {
|
|
164
|
+
if (option.type === 'entry') {
|
|
165
|
+
commitSelection(option.entry);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
handleNav(option);
|
|
169
|
+
};
|
|
170
|
+
const render = () => {
|
|
171
|
+
const lines = formatInteractiveLines(state, selectedIndex, title);
|
|
172
|
+
renderedLines = renderInteractiveList(lines, renderedLines);
|
|
173
|
+
};
|
|
174
|
+
const onData = (buffer) => {
|
|
175
|
+
const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
|
|
176
|
+
const isArrowDown = buffer.equals(Buffer.from([0x1b, 0x5b, 0x42]));
|
|
177
|
+
const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
|
|
178
|
+
const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
|
|
179
|
+
const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
|
|
180
|
+
if (isCtrlC || isEscape) {
|
|
181
|
+
cleanup();
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
if (isArrowUp ||
|
|
185
|
+
(buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
|
|
186
|
+
selectedIndex =
|
|
187
|
+
(selectedIndex - 1 + state.options.length) % state.options.length;
|
|
188
|
+
render();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (isArrowDown ||
|
|
192
|
+
(buffer.length === 1 && (buffer[0] === 0x6a || buffer[0] === 0x4a))) {
|
|
193
|
+
selectedIndex = (selectedIndex + 1) % state.options.length;
|
|
194
|
+
render();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (isEnter) {
|
|
198
|
+
activateOption(state.options[selectedIndex]);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (buffer.length === 1 && buffer[0] >= 0x30 && buffer[0] <= 0x39) {
|
|
202
|
+
const numericValue = buffer[0] - 0x30;
|
|
203
|
+
const option = state.options.find((opt) => opt.hotkey === numericValue);
|
|
204
|
+
if (option)
|
|
205
|
+
activateOption(option);
|
|
206
|
+
else
|
|
207
|
+
process.stdout.write('\x07');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (buffer.length === 1 &&
|
|
211
|
+
((buffer[0] >= 0x41 && buffer[0] <= 0x5a) ||
|
|
212
|
+
(buffer[0] >= 0x61 && buffer[0] <= 0x7a))) {
|
|
213
|
+
const char = String.fromCharCode(buffer[0]).toLowerCase();
|
|
214
|
+
const foundIndex = entries.findIndex((entry) => entry.displayName.toLowerCase().startsWith(char));
|
|
215
|
+
if (foundIndex !== -1) {
|
|
216
|
+
const page = Math.floor(foundIndex / PAGE_SIZE);
|
|
217
|
+
state = buildVisibleOptions(entries, page);
|
|
218
|
+
selectedIndex = foundIndex % PAGE_SIZE;
|
|
219
|
+
render();
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
process.stdout.write('\x07');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
input.on('data', onData);
|
|
227
|
+
render();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { colors } from './colors.js';
|
|
5
|
+
import { managerRoot, rootDir } from './env.js';
|
|
6
|
+
import { buildTsNodeRegisterImport } from './ts-node.js';
|
|
7
|
+
export function runEntry(entry, forwardedArgs) {
|
|
8
|
+
const detail = entry.script
|
|
9
|
+
? path.relative(rootDir, entry.absoluteScript ?? entry.script)
|
|
10
|
+
: (entry.metaLabel ?? '[callback]');
|
|
11
|
+
console.log(`${entry.emoji} ${colors.green(`Running "${entry.displayName}"`)} ${colors.dim(detail)}`);
|
|
12
|
+
if (entry.handler) {
|
|
13
|
+
return Promise.resolve(entry.handler({ args: forwardedArgs, entry, rootDir }));
|
|
14
|
+
}
|
|
15
|
+
if (!entry.absoluteScript) {
|
|
16
|
+
throw new Error(`Script "${entry.displayName}" is missing a resolved path.`);
|
|
17
|
+
}
|
|
18
|
+
const scriptPath = entry.absoluteScript;
|
|
19
|
+
const tsConfigPath = path.join(rootDir, 'tsconfig.base.json');
|
|
20
|
+
const tsConfigFallback = path.join(rootDir, 'tsconfig.json');
|
|
21
|
+
const bundledTsconfig = path.join(managerRoot, 'tsconfig.base.json');
|
|
22
|
+
const projectPath = existsSync(tsConfigPath)
|
|
23
|
+
? tsConfigPath
|
|
24
|
+
: existsSync(tsConfigFallback)
|
|
25
|
+
? tsConfigFallback
|
|
26
|
+
: existsSync(bundledTsconfig)
|
|
27
|
+
? bundledTsconfig
|
|
28
|
+
: undefined;
|
|
29
|
+
const extension = path.extname(scriptPath).toLowerCase();
|
|
30
|
+
const isTypeScript = extension === '.js' || extension === '.mts' || extension === '.cts';
|
|
31
|
+
const command = process.execPath;
|
|
32
|
+
const execArgs = isTypeScript
|
|
33
|
+
? [
|
|
34
|
+
'--import',
|
|
35
|
+
buildTsNodeRegisterImport(scriptPath),
|
|
36
|
+
scriptPath,
|
|
37
|
+
...forwardedArgs,
|
|
38
|
+
]
|
|
39
|
+
: [scriptPath, ...forwardedArgs];
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const child = spawn(command, execArgs, {
|
|
42
|
+
cwd: rootDir,
|
|
43
|
+
stdio: 'inherit',
|
|
44
|
+
env: isTypeScript
|
|
45
|
+
? {
|
|
46
|
+
...process.env,
|
|
47
|
+
...(projectPath ? { TS_NODE_PROJECT: projectPath } : {}),
|
|
48
|
+
}
|
|
49
|
+
: process.env,
|
|
50
|
+
shell: process.platform === 'win32' && isTypeScript,
|
|
51
|
+
});
|
|
52
|
+
child.on('close', (code) => {
|
|
53
|
+
if (code === 0)
|
|
54
|
+
resolve();
|
|
55
|
+
else
|
|
56
|
+
reject(new Error(`Script "${entry.displayName}" exited with code ${code}`));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { rootDir } from './env.js';
|
|
3
|
+
export function normalizeScripts(entries) {
|
|
4
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
5
|
+
throw new Error('runHelperCli requires at least one script definition.');
|
|
6
|
+
}
|
|
7
|
+
return entries.map((entry, index) => {
|
|
8
|
+
if (!entry || typeof entry !== 'object') {
|
|
9
|
+
throw new Error(`Script entry at index ${index} is not an object.`);
|
|
10
|
+
}
|
|
11
|
+
if (!entry.name || typeof entry.name !== 'string') {
|
|
12
|
+
throw new Error(`Script entry at index ${index} is missing a "name".`);
|
|
13
|
+
}
|
|
14
|
+
const hasHandler = typeof entry.handler === 'function';
|
|
15
|
+
const hasScript = typeof entry.script === 'string' && entry.script.length > 0;
|
|
16
|
+
if (!hasHandler && !hasScript) {
|
|
17
|
+
throw new Error(`Script "${entry.name}" requires either a "script" path or a "handler" function.`);
|
|
18
|
+
}
|
|
19
|
+
const absoluteScript = hasScript && path.isAbsolute(entry.script)
|
|
20
|
+
? entry.script
|
|
21
|
+
: hasScript
|
|
22
|
+
? path.join(rootDir, entry.script)
|
|
23
|
+
: undefined;
|
|
24
|
+
return {
|
|
25
|
+
...entry,
|
|
26
|
+
emoji: entry.emoji ?? '🔧',
|
|
27
|
+
displayName: entry.name.trim(),
|
|
28
|
+
absoluteScript,
|
|
29
|
+
script: hasScript ? entry.script : undefined,
|
|
30
|
+
handler: hasHandler ? entry.handler : undefined,
|
|
31
|
+
metaLabel: entry.description ?? (hasScript ? entry.script : '[callback]'),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function findScriptEntry(entries, key) {
|
|
36
|
+
if (!key)
|
|
37
|
+
return undefined;
|
|
38
|
+
const normalized = key.toLowerCase();
|
|
39
|
+
return entries.find((entry) => {
|
|
40
|
+
const nameMatch = entry.displayName.toLowerCase() === normalized;
|
|
41
|
+
const aliasMatch = entry.displayName.toLowerCase().includes(normalized);
|
|
42
|
+
const scriptMatch = entry.script
|
|
43
|
+
? entry.script.toLowerCase().includes(normalized)
|
|
44
|
+
: false;
|
|
45
|
+
return nameMatch || aliasMatch || scriptMatch;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getTsNodeLoaderPath } from './env.js';
|
|
2
|
+
export function buildTsNodeRegisterImport(scriptPath) {
|
|
3
|
+
const loader = getTsNodeLoaderPath();
|
|
4
|
+
const code = [
|
|
5
|
+
'import { register } from "node:module";',
|
|
6
|
+
'import { pathToFileURL } from "node:url";',
|
|
7
|
+
`register(${JSON.stringify(loader)}, pathToFileURL(${JSON.stringify(scriptPath)}));`,
|
|
8
|
+
].join(' ');
|
|
9
|
+
return `data:text/javascript,${encodeURIComponent(code)}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|