@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.
@@ -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
- `register(${JSON.stringify(
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
+ }