@emeryld/manager 0.6.3 → 0.6.4

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.
@@ -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 { stdin as input } from 'node:process';
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
- const supportsRawMode = typeof input.setRawMode === 'function' && input.isTTY;
301
- if (!supportsRawMode) {
302
- const fallbackMessage = [
303
- 'Pick a package template:',
304
- VARIANTS.map((opt, idx) => ` [${idx + 1}] ${opt.label}`).join('\n'),
305
- `Enter 1-${VARIANTS.length}: `,
306
- ].join('\n');
307
- return promptSingleKey(fallbackMessage, (key) => {
308
- const idx = Number.parseInt(key, 10);
309
- if (Number.isInteger(idx) && idx >= 1 && idx <= VARIANTS.length) {
310
- return VARIANTS[idx - 1];
311
- }
312
- return undefined;
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
- const wasRaw = input.isRaw;
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 runCommand('pnpm', ['install'], workspaceRoot);
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 runCommand('pnpm', ['-r', '--filter', `${options.pkgName}...`, 'build'], workspaceRoot);
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 runCommand('pnpm', ['-r', 'build'], workspaceRoot);
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 runCommand('pnpm', ['-C', targetDir, 'run', 'build'], workspaceRoot);
335
+ await run('pnpm', ['-C', targetDir, 'run', 'build'], {
336
+ cwd: workspaceRoot,
337
+ });
442
338
  }
443
339
  }
444
340
  catch (error) {
@@ -0,0 +1,7 @@
1
+ import { colors as sharedColors } from '../colors-shared.js';
2
+ export const colors = sharedColors;
3
+ export function getEntryColor(entry) {
4
+ if (entry.color && colors[entry.color])
5
+ return colors[entry.color];
6
+ return colors.cyan;
7
+ }
@@ -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 {};