@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
package/dist/helper-cli.js
CHANGED
|
@@ -1,285 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const managerRoot = path.resolve(path.dirname(__filename), '..');
|
|
10
|
-
// Workspace root is wherever the CLI is executed.
|
|
11
|
-
const rootDir = process.cwd();
|
|
12
|
-
const managerRequire = createRequire(import.meta.url);
|
|
13
|
-
let tsNodeLoaderPath;
|
|
14
|
-
function getTsNodeLoaderPath() {
|
|
15
|
-
if (!tsNodeLoaderPath) {
|
|
16
|
-
tsNodeLoaderPath = managerRequire.resolve('ts-node/esm.mjs');
|
|
17
|
-
}
|
|
18
|
-
return tsNodeLoaderPath;
|
|
19
|
-
}
|
|
20
|
-
export function buildTsNodeRegisterImport(scriptPath) {
|
|
21
|
-
const loader = getTsNodeLoaderPath();
|
|
22
|
-
const code = [
|
|
23
|
-
'import { register } from "node:module";',
|
|
24
|
-
'import { pathToFileURL } from "node:url";',
|
|
25
|
-
`register(${JSON.stringify(loader)}, pathToFileURL(${JSON.stringify(scriptPath)}));`,
|
|
26
|
-
].join(' ');
|
|
27
|
-
return `data:text/javascript,${encodeURIComponent(code)}`;
|
|
28
|
-
}
|
|
29
|
-
const ansi = (code) => (text) => `\x1b[${code}m${text}\x1b[0m`;
|
|
30
|
-
const colors = {
|
|
31
|
-
cyan: ansi(36),
|
|
32
|
-
green: ansi(32),
|
|
33
|
-
yellow: ansi(33),
|
|
34
|
-
magenta: ansi(35),
|
|
35
|
-
dim: ansi(2),
|
|
36
|
-
};
|
|
37
|
-
function normalizeScripts(entries) {
|
|
38
|
-
if (!Array.isArray(entries) || entries.length === 0) {
|
|
39
|
-
throw new Error('runHelperCli requires at least one script definition.');
|
|
40
|
-
}
|
|
41
|
-
return entries.map((entry, index) => {
|
|
42
|
-
if (!entry || typeof entry !== 'object') {
|
|
43
|
-
throw new Error(`Script entry at index ${index} is not an object.`);
|
|
44
|
-
}
|
|
45
|
-
if (!entry.name || typeof entry.name !== 'string') {
|
|
46
|
-
throw new Error(`Script entry at index ${index} is missing a "name".`);
|
|
47
|
-
}
|
|
48
|
-
const hasHandler = typeof entry.handler === 'function';
|
|
49
|
-
const hasScript = typeof entry.script === 'string' && entry.script.length > 0;
|
|
50
|
-
if (!hasHandler && !hasScript) {
|
|
51
|
-
throw new Error(`Script "${entry.name}" requires either a "script" path or a "handler" function.`);
|
|
52
|
-
}
|
|
53
|
-
const absoluteScript = hasScript && path.isAbsolute(entry.script)
|
|
54
|
-
? entry.script
|
|
55
|
-
: hasScript
|
|
56
|
-
? path.join(rootDir, entry.script)
|
|
57
|
-
: undefined;
|
|
58
|
-
return {
|
|
59
|
-
...entry,
|
|
60
|
-
emoji: entry.emoji ?? 'đ§',
|
|
61
|
-
displayName: entry.name.trim(),
|
|
62
|
-
absoluteScript,
|
|
63
|
-
script: hasScript ? entry.script : undefined,
|
|
64
|
-
handler: hasHandler ? entry.handler : undefined,
|
|
65
|
-
metaLabel: entry.description ?? (hasScript ? entry.script : '[callback]'),
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
function findScriptEntry(entries, key) {
|
|
70
|
-
if (!key)
|
|
71
|
-
return undefined;
|
|
72
|
-
const normalized = key.toLowerCase();
|
|
73
|
-
return entries.find((entry) => {
|
|
74
|
-
const nameMatch = entry.displayName.toLowerCase() === normalized;
|
|
75
|
-
const aliasMatch = entry.displayName.toLowerCase().includes(normalized);
|
|
76
|
-
const scriptMatch = entry.script
|
|
77
|
-
? entry.script.toLowerCase().includes(normalized)
|
|
78
|
-
: false;
|
|
79
|
-
return nameMatch || aliasMatch || scriptMatch;
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
function printScriptList(entries, title) {
|
|
83
|
-
const heading = title
|
|
84
|
-
? colors.magenta(title)
|
|
85
|
-
: colors.magenta('Available scripts');
|
|
86
|
-
console.log(heading);
|
|
87
|
-
entries.forEach((entry, index) => {
|
|
88
|
-
console.log(` ${colors.cyan(String(index + 1).padStart(2, ' '))} ${entry.emoji} ${entry.displayName} ${colors.dim(entry.metaLabel)}`);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
async function promptWithReadline(entries, title) {
|
|
92
|
-
printScriptList(entries, title);
|
|
93
|
-
const rl = readline.createInterface({ input, output });
|
|
94
|
-
try {
|
|
95
|
-
while (true) {
|
|
96
|
-
const answer = (await rl.question(colors.cyan('\nSelect a script by number or name: '))).trim();
|
|
97
|
-
if (!answer)
|
|
98
|
-
continue;
|
|
99
|
-
const numeric = Number.parseInt(answer, 10);
|
|
100
|
-
if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= entries.length) {
|
|
101
|
-
return entries[numeric - 1];
|
|
102
|
-
}
|
|
103
|
-
const byName = findScriptEntry(entries, answer);
|
|
104
|
-
if (byName)
|
|
105
|
-
return byName;
|
|
106
|
-
console.log(colors.yellow(`Could not find "${answer}". Try again.`));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
rl.close();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
function formatInteractiveLines(entries, selectedIndex, title) {
|
|
114
|
-
const heading = title
|
|
115
|
-
? colors.magenta(title)
|
|
116
|
-
: colors.magenta('Available scripts');
|
|
117
|
-
const lines = [heading];
|
|
118
|
-
entries.forEach((entry, index) => {
|
|
119
|
-
const isSelected = index === selectedIndex;
|
|
120
|
-
const pointer = isSelected ? `${colors.green('â¤')} ` : '';
|
|
121
|
-
const numberLabel = colors.cyan(`${index + 1}`.padStart(2, ' '));
|
|
122
|
-
const label = isSelected
|
|
123
|
-
? colors.green(entry.displayName)
|
|
124
|
-
: entry.displayName;
|
|
125
|
-
lines.push(`${pointer}${numberLabel}. ${entry.emoji} ${label} ${colors.dim(entry.metaLabel)}`);
|
|
126
|
-
});
|
|
127
|
-
lines.push('');
|
|
128
|
-
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.'));
|
|
129
|
-
return lines;
|
|
130
|
-
}
|
|
131
|
-
function renderInteractiveList(lines, previousLineCount) {
|
|
132
|
-
if (previousLineCount > 0) {
|
|
133
|
-
process.stdout.write(`\x1b[${previousLineCount}A`);
|
|
134
|
-
process.stdout.write('\x1b[0J');
|
|
135
|
-
}
|
|
136
|
-
lines.forEach((line) => console.log(line));
|
|
137
|
-
return lines.length;
|
|
138
|
-
}
|
|
139
|
-
async function promptForScript(entries, title) {
|
|
140
|
-
const supportsRawMode = typeof input.setRawMode === 'function' && input.isTTY;
|
|
141
|
-
if (!supportsRawMode) {
|
|
142
|
-
return promptWithReadline(entries, title);
|
|
143
|
-
}
|
|
144
|
-
const wasRaw = input.isRaw;
|
|
145
|
-
if (!wasRaw) {
|
|
146
|
-
input.setRawMode(true);
|
|
147
|
-
input.resume();
|
|
148
|
-
}
|
|
149
|
-
process.stdout.write('\x1b[?25l');
|
|
150
|
-
return new Promise((resolve) => {
|
|
151
|
-
let selectedIndex = 0;
|
|
152
|
-
let renderedLines = 0;
|
|
153
|
-
const cleanup = () => {
|
|
154
|
-
if (renderedLines > 0) {
|
|
155
|
-
process.stdout.write(`\x1b[${renderedLines}A`);
|
|
156
|
-
process.stdout.write('\x1b[0J');
|
|
157
|
-
renderedLines = 0;
|
|
158
|
-
}
|
|
159
|
-
process.stdout.write('\x1b[?25h');
|
|
160
|
-
if (!wasRaw) {
|
|
161
|
-
input.setRawMode(false);
|
|
162
|
-
input.pause();
|
|
163
|
-
}
|
|
164
|
-
input.removeListener('data', onData);
|
|
165
|
-
};
|
|
166
|
-
const commitSelection = (entry) => {
|
|
167
|
-
cleanup();
|
|
168
|
-
console.log();
|
|
169
|
-
resolve(entry);
|
|
170
|
-
};
|
|
171
|
-
const render = () => {
|
|
172
|
-
const lines = formatInteractiveLines(entries, selectedIndex, title);
|
|
173
|
-
renderedLines = renderInteractiveList(lines, renderedLines);
|
|
174
|
-
};
|
|
175
|
-
const onData = (buffer) => {
|
|
176
|
-
const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
|
|
177
|
-
const isArrowDown = buffer.equals(Buffer.from([0x1b, 0x5b, 0x42]));
|
|
178
|
-
const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
|
|
179
|
-
const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
|
|
180
|
-
const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
|
|
181
|
-
if (isCtrlC || isEscape) {
|
|
182
|
-
cleanup();
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
if (isArrowUp ||
|
|
186
|
-
(buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
|
|
187
|
-
selectedIndex = (selectedIndex - 1 + entries.length) % entries.length;
|
|
188
|
-
render();
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (isArrowDown ||
|
|
192
|
-
(buffer.length === 1 && (buffer[0] === 0x6a || buffer[0] === 0x4a))) {
|
|
193
|
-
selectedIndex = (selectedIndex + 1) % entries.length;
|
|
194
|
-
render();
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (isEnter) {
|
|
198
|
-
commitSelection(entries[selectedIndex]);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (buffer.length === 1 && buffer[0] >= 0x30 && buffer[0] <= 0x39) {
|
|
202
|
-
const numericValue = buffer[0] === 0x30 ? 10 : buffer[0] - 0x30;
|
|
203
|
-
const selected = numericValue - 1;
|
|
204
|
-
if (selected >= 0 && selected < entries.length) {
|
|
205
|
-
commitSelection(entries[selected]);
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
process.stdout.write('\x07');
|
|
209
|
-
}
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (buffer.length === 1 &&
|
|
213
|
-
((buffer[0] >= 0x41 && buffer[0] <= 0x5a) ||
|
|
214
|
-
(buffer[0] >= 0x61 && buffer[0] <= 0x7a))) {
|
|
215
|
-
const char = String.fromCharCode(buffer[0]).toLowerCase();
|
|
216
|
-
const foundIndex = entries.findIndex((entry) => entry.displayName.toLowerCase().startsWith(char));
|
|
217
|
-
if (foundIndex !== -1) {
|
|
218
|
-
selectedIndex = foundIndex;
|
|
219
|
-
render();
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
process.stdout.write('\x07');
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
input.on('data', onData);
|
|
227
|
-
render();
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
function runEntry(entry, forwardedArgs) {
|
|
231
|
-
const detail = entry.script
|
|
232
|
-
? path.relative(rootDir, entry.absoluteScript ?? entry.script)
|
|
233
|
-
: (entry.metaLabel ?? '[callback]');
|
|
234
|
-
console.log(`${entry.emoji} ${colors.green(`Running "${entry.displayName}"`)} ${colors.dim(detail)}`);
|
|
235
|
-
if (entry.handler) {
|
|
236
|
-
return Promise.resolve(entry.handler({ args: forwardedArgs, entry, rootDir }));
|
|
237
|
-
}
|
|
238
|
-
if (!entry.absoluteScript) {
|
|
239
|
-
throw new Error(`Script "${entry.displayName}" is missing a resolved path.`);
|
|
240
|
-
}
|
|
241
|
-
const scriptPath = entry.absoluteScript;
|
|
242
|
-
const tsConfigPath = path.join(rootDir, 'tsconfig.base.json');
|
|
243
|
-
const tsConfigFallback = path.join(rootDir, 'tsconfig.json');
|
|
244
|
-
const bundledTsconfig = path.join(managerRoot, 'tsconfig.base.json');
|
|
245
|
-
const projectPath = existsSync(tsConfigPath)
|
|
246
|
-
? tsConfigPath
|
|
247
|
-
: existsSync(tsConfigFallback)
|
|
248
|
-
? tsConfigFallback
|
|
249
|
-
: existsSync(bundledTsconfig)
|
|
250
|
-
? bundledTsconfig
|
|
251
|
-
: undefined;
|
|
252
|
-
const extension = path.extname(scriptPath).toLowerCase();
|
|
253
|
-
const isTypeScript = extension === '.js' || extension === '.mts' || extension === '.cts';
|
|
254
|
-
const command = process.execPath;
|
|
255
|
-
const execArgs = isTypeScript
|
|
256
|
-
? [
|
|
257
|
-
'--import',
|
|
258
|
-
buildTsNodeRegisterImport(scriptPath),
|
|
259
|
-
scriptPath,
|
|
260
|
-
...forwardedArgs,
|
|
261
|
-
]
|
|
262
|
-
: [scriptPath, ...forwardedArgs];
|
|
263
|
-
return new Promise((resolve, reject) => {
|
|
264
|
-
const child = spawn(command, execArgs, {
|
|
265
|
-
cwd: rootDir,
|
|
266
|
-
stdio: 'inherit',
|
|
267
|
-
env: isTypeScript
|
|
268
|
-
? {
|
|
269
|
-
...process.env,
|
|
270
|
-
...(projectPath ? { TS_NODE_PROJECT: projectPath } : {}),
|
|
271
|
-
}
|
|
272
|
-
: process.env,
|
|
273
|
-
shell: process.platform === 'win32' && isTypeScript,
|
|
274
|
-
});
|
|
275
|
-
child.on('close', (code) => {
|
|
276
|
-
if (code === 0)
|
|
277
|
-
resolve();
|
|
278
|
-
else
|
|
279
|
-
reject(new Error(`Script "${entry.displayName}" exited with code ${code}`));
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
}
|
|
1
|
+
import { colors } from './helper-cli/colors.js';
|
|
2
|
+
import { printScriptList } from './helper-cli/display.js';
|
|
3
|
+
import { promptForScript } from './helper-cli/prompts.js';
|
|
4
|
+
import { findScriptEntry, normalizeScripts } from './helper-cli/scripts.js';
|
|
5
|
+
import { runEntry } from './helper-cli/runner.js';
|
|
6
|
+
export { buildTsNodeRegisterImport } from './helper-cli/ts-node.js';
|
|
283
7
|
export async function runHelperCli({ scripts, title = 'Helper CLI', argv = process.argv.slice(2), }) {
|
|
284
8
|
const normalized = normalizeScripts(scripts);
|
|
285
9
|
let args = Array.isArray(argv) ? [...argv] : [];
|
|
@@ -310,5 +34,7 @@ export async function runHelperCli({ scripts, title = 'Helper CLI', argv = proce
|
|
|
310
34
|
console.log(colors.yellow(`Unknown script "${firstArg}". Falling back to interactive selectionâĻ`));
|
|
311
35
|
}
|
|
312
36
|
const selection = await promptForScript(normalized, title);
|
|
313
|
-
|
|
37
|
+
if (selection) {
|
|
38
|
+
await runEntry(selection, []);
|
|
39
|
+
}
|
|
314
40
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { colors } from "../utils/log.js";
|
|
2
|
+
import { run } from "../utils/run.js";
|
|
3
|
+
export function collectDependencies(pkg) {
|
|
4
|
+
return new Set([
|
|
5
|
+
...Object.keys(pkg.json?.dependencies ?? {}),
|
|
6
|
+
...Object.keys(pkg.json?.devDependencies ?? {}),
|
|
7
|
+
]);
|
|
8
|
+
}
|
|
9
|
+
const BASE_SCRIPT_KEYS = ['format', 'typecheck', 'dev', 'lint'];
|
|
10
|
+
function commandDescription(args) {
|
|
11
|
+
return `pnpm ${args.join(' ')}`;
|
|
12
|
+
}
|
|
13
|
+
function resolveRunScript(scripts, candidates) {
|
|
14
|
+
for (const name of candidates) {
|
|
15
|
+
if (!scripts[name])
|
|
16
|
+
continue;
|
|
17
|
+
const cmd = commandDescription(['run', name]);
|
|
18
|
+
const scriptDetail = scripts[name]?.trim();
|
|
19
|
+
const description = scriptDetail ? `${cmd} (${scriptDetail})` : cmd;
|
|
20
|
+
return {
|
|
21
|
+
label: name,
|
|
22
|
+
description,
|
|
23
|
+
args: ['run', name],
|
|
24
|
+
available: true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
function resolveFormatScript(pkg, scripts, dependencies) {
|
|
30
|
+
const direct = resolveRunScript(scripts, ['format', 'fmt']);
|
|
31
|
+
if (direct)
|
|
32
|
+
return direct;
|
|
33
|
+
if (dependencies.has('prettier')) {
|
|
34
|
+
const args = ['exec', 'prettier', '--check', '.'];
|
|
35
|
+
return {
|
|
36
|
+
label: 'prettier --check',
|
|
37
|
+
description: commandDescription(args),
|
|
38
|
+
args,
|
|
39
|
+
available: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
label: 'format',
|
|
44
|
+
description: `No format/fmt script found for ${pkg.name}`,
|
|
45
|
+
available: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function resolveTypecheckScript(pkg, scripts, dependencies) {
|
|
49
|
+
const direct = resolveRunScript(scripts, ['typecheck', 'check']);
|
|
50
|
+
if (direct)
|
|
51
|
+
return direct;
|
|
52
|
+
if (dependencies.has('typescript')) {
|
|
53
|
+
const args = ['exec', 'tsc', '--noEmit', '--pretty', 'false'];
|
|
54
|
+
return {
|
|
55
|
+
label: 'tsc --noEmit',
|
|
56
|
+
description: commandDescription(args),
|
|
57
|
+
args,
|
|
58
|
+
available: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
label: 'typecheck',
|
|
63
|
+
description: `No typecheck/check script found for ${pkg.name}`,
|
|
64
|
+
available: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function resolveLintScript(pkg, scripts, dependencies) {
|
|
68
|
+
const direct = resolveRunScript(scripts, ['lint']);
|
|
69
|
+
if (direct)
|
|
70
|
+
return direct;
|
|
71
|
+
if (dependencies.has('eslint')) {
|
|
72
|
+
const args = ['exec', 'eslint', '.', '--ext', '.ts,.tsx,.js,.jsx'];
|
|
73
|
+
return {
|
|
74
|
+
label: 'eslint .',
|
|
75
|
+
description: commandDescription(args),
|
|
76
|
+
args,
|
|
77
|
+
available: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
label: 'lint',
|
|
82
|
+
description: `No lint script found for ${pkg.name}`,
|
|
83
|
+
available: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function resolveDevScript(pkg, scripts, dependencies, kind) {
|
|
87
|
+
const devCandidates = kind === 'frontend'
|
|
88
|
+
? ['dev', 'start', 'serve']
|
|
89
|
+
: kind === 'backend'
|
|
90
|
+
? ['dev', 'start', 'watch', 'serve', 'start:dev']
|
|
91
|
+
: ['dev', 'start'];
|
|
92
|
+
const direct = resolveRunScript(scripts, devCandidates);
|
|
93
|
+
if (direct)
|
|
94
|
+
return direct;
|
|
95
|
+
if (dependencies.has('next')) {
|
|
96
|
+
const args = ['exec', 'next', 'dev'];
|
|
97
|
+
return {
|
|
98
|
+
label: 'next dev',
|
|
99
|
+
description: commandDescription(args),
|
|
100
|
+
args,
|
|
101
|
+
available: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (dependencies.has('vite')) {
|
|
105
|
+
const args = ['exec', 'vite', 'dev'];
|
|
106
|
+
return {
|
|
107
|
+
label: 'vite dev',
|
|
108
|
+
description: commandDescription(args),
|
|
109
|
+
args,
|
|
110
|
+
available: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (dependencies.has('expo')) {
|
|
114
|
+
const args = ['exec', 'expo', 'start'];
|
|
115
|
+
return {
|
|
116
|
+
label: 'expo start',
|
|
117
|
+
description: commandDescription(args),
|
|
118
|
+
args,
|
|
119
|
+
available: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
label: 'dev',
|
|
124
|
+
description: `No dev/start script found for ${pkg.name}`,
|
|
125
|
+
available: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function resolveBaseScript(pkg, key, scripts, dependencies, kind) {
|
|
129
|
+
switch (key) {
|
|
130
|
+
case 'format':
|
|
131
|
+
return resolveFormatScript(pkg, scripts, dependencies);
|
|
132
|
+
case 'typecheck':
|
|
133
|
+
return resolveTypecheckScript(pkg, scripts, dependencies);
|
|
134
|
+
case 'lint':
|
|
135
|
+
return resolveLintScript(pkg, scripts, dependencies);
|
|
136
|
+
case 'dev':
|
|
137
|
+
default:
|
|
138
|
+
return resolveDevScript(pkg, scripts, dependencies, kind);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function makeBaseScriptEntries(pkg) {
|
|
142
|
+
const scripts = pkg.json?.scripts ?? {};
|
|
143
|
+
const dependencies = collectDependencies(pkg);
|
|
144
|
+
const marker = getPackageMarker(pkg, dependencies);
|
|
145
|
+
return BASE_SCRIPT_KEYS.map((key) => {
|
|
146
|
+
const resolution = resolveBaseScript(pkg, key, scripts, dependencies, marker.kind);
|
|
147
|
+
const description = resolution.available
|
|
148
|
+
? resolution.description
|
|
149
|
+
: `No ${key} command detected`;
|
|
150
|
+
return {
|
|
151
|
+
name: key,
|
|
152
|
+
emoji: 'âĄī¸',
|
|
153
|
+
description,
|
|
154
|
+
handler: async () => {
|
|
155
|
+
if (!resolution.available || !resolution.args) {
|
|
156
|
+
console.log(colors.yellow(`No ${key} command found for ${pkg.name}.`));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await run('pnpm', resolution.args, { cwd: pkg.path });
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export function getPackageMarker(pkg, dependencies = collectDependencies(pkg)) {
|
|
165
|
+
const hasFrontend = ['react', 'next', 'expo', 'react-native', 'vite'].some((dep) => dependencies.has(dep));
|
|
166
|
+
const hasBackend = ['express', 'fastify', 'hono', 'koa', '@nestjs/core'].some((dep) => dependencies.has(dep));
|
|
167
|
+
const pkgType = pkg.json?.type?.toLowerCase();
|
|
168
|
+
if (hasFrontend) {
|
|
169
|
+
return { label: 'frontend', colorize: colors.magenta, kind: 'frontend' };
|
|
170
|
+
}
|
|
171
|
+
if (hasBackend) {
|
|
172
|
+
return { label: 'backend', colorize: colors.green, kind: 'backend' };
|
|
173
|
+
}
|
|
174
|
+
if (pkgType === 'module') {
|
|
175
|
+
return { label: 'esm', colorize: colors.yellow, kind: 'library' };
|
|
176
|
+
}
|
|
177
|
+
return { label: 'node', colorize: colors.cyan, kind: 'library' };
|
|
178
|
+
}
|
package/dist/menu.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// src/menu.js
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { colors, globalEmoji } from './utils/log.js';
|
|
4
|
-
import { updateDependencies, testAll, testSingle, buildAll, buildSingle, } from './workspace.js';
|
|
4
|
+
import { updateDependencies, testAll, testSingle, buildAll, buildSingle, cleanPackages, rebuildPackages, } from './workspace.js';
|
|
5
5
|
import { releaseMultiple, releaseSingle } from './release.js';
|
|
6
6
|
import { getOrderedPackages } from './packages.js';
|
|
7
7
|
import { runHelperCli } from './helper-cli.js';
|
|
8
8
|
import { ensureWorkingTreeCommitted } from './preflight.js';
|
|
9
9
|
import { openDockerHelper } from './docker.js';
|
|
10
10
|
import { run } from './utils/run.js';
|
|
11
|
+
import { makeBaseScriptEntries, getPackageMarker } from './menu/script-helpers.js';
|
|
11
12
|
function makeManagerStepEntries(targets, packages, state, options) {
|
|
12
13
|
const includeBack = options?.includeBack ?? true;
|
|
13
14
|
return [
|
|
@@ -20,6 +21,24 @@ function makeManagerStepEntries(targets, packages, state, options) {
|
|
|
20
21
|
state.lastStep = 'update';
|
|
21
22
|
},
|
|
22
23
|
},
|
|
24
|
+
{
|
|
25
|
+
name: 'clean',
|
|
26
|
+
emoji: 'đ§š',
|
|
27
|
+
description: 'Remove node_modules, dist, and caches',
|
|
28
|
+
handler: async () => {
|
|
29
|
+
await cleanPackages(targets);
|
|
30
|
+
state.lastStep = 'clean';
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'rebuild',
|
|
35
|
+
emoji: 'đ',
|
|
36
|
+
description: 'Clean then build',
|
|
37
|
+
handler: async () => {
|
|
38
|
+
await rebuildPackages(targets);
|
|
39
|
+
state.lastStep = 'rebuild';
|
|
40
|
+
},
|
|
41
|
+
},
|
|
23
42
|
{
|
|
24
43
|
name: 'test',
|
|
25
44
|
emoji: 'đ§Ē',
|
|
@@ -93,25 +112,32 @@ function makeManagerStepEntries(targets, packages, state, options) {
|
|
|
93
112
|
: []),
|
|
94
113
|
];
|
|
95
114
|
}
|
|
96
|
-
function
|
|
97
|
-
const scripts = pkg.json?.scripts ?? {};
|
|
98
|
-
const entries = [];
|
|
115
|
+
function makeDockerEntry(pkg) {
|
|
99
116
|
if (pkg.dockerfilePath) {
|
|
100
117
|
const dockerLabel = path
|
|
101
118
|
.relative(process.cwd(), pkg.dockerfilePath)
|
|
102
119
|
.replace(/\\/g, '/');
|
|
103
|
-
|
|
120
|
+
return {
|
|
104
121
|
name: `Dockerfile (${dockerLabel})`,
|
|
105
122
|
emoji: 'đŗ',
|
|
106
123
|
description: 'manager -> opens the docker cli',
|
|
107
124
|
handler: async () => {
|
|
108
125
|
await openDockerHelper(pkg);
|
|
109
126
|
},
|
|
110
|
-
}
|
|
127
|
+
};
|
|
111
128
|
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
function makePackageScriptEntries(pkg) {
|
|
132
|
+
const scripts = pkg.json?.scripts ?? {};
|
|
133
|
+
const baseEntries = makeBaseScriptEntries(pkg);
|
|
134
|
+
const baseNames = new Set(baseEntries.map((entry) => entry.name));
|
|
135
|
+
const entries = [];
|
|
112
136
|
Object.entries(scripts)
|
|
113
137
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
114
138
|
.forEach(([name, command]) => {
|
|
139
|
+
if (baseNames.has(name))
|
|
140
|
+
return;
|
|
115
141
|
entries.push({
|
|
116
142
|
name,
|
|
117
143
|
emoji: 'âļī¸',
|
|
@@ -121,23 +147,31 @@ function makePackageScriptEntries(pkg) {
|
|
|
121
147
|
},
|
|
122
148
|
});
|
|
123
149
|
});
|
|
124
|
-
return entries;
|
|
150
|
+
return [...baseEntries, ...entries];
|
|
125
151
|
}
|
|
126
152
|
export function buildPackageSelectionMenu(packages, onStepComplete) {
|
|
127
153
|
const ordered = getOrderedPackages(packages);
|
|
128
|
-
const entries = ordered.map((pkg) =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
const entries = ordered.map((pkg) => {
|
|
155
|
+
const marker = getPackageMarker(pkg);
|
|
156
|
+
const pkgColor = pkg.color ?? 'cyan';
|
|
157
|
+
const descriptionMeta = pkg.relativeDir ?? pkg.dirName;
|
|
158
|
+
const markerHint = marker.label ? ` ¡ ${marker.label}` : '';
|
|
159
|
+
return {
|
|
160
|
+
name: pkg.name ?? pkg.substitute ?? pkg.dirName,
|
|
161
|
+
emoji: marker.colorize('â'),
|
|
162
|
+
description: `${descriptionMeta}${colors.dim(markerHint)}`,
|
|
163
|
+
color: pkgColor,
|
|
164
|
+
handler: async () => {
|
|
165
|
+
const step = await runStepLoop([pkg], packages);
|
|
166
|
+
onStepComplete?.(step);
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
});
|
|
137
170
|
if (ordered.length === 0)
|
|
138
171
|
return entries;
|
|
139
172
|
entries.push({
|
|
140
173
|
name: 'All packages',
|
|
174
|
+
color: 'gray',
|
|
141
175
|
emoji: globalEmoji,
|
|
142
176
|
description: 'Select all packages',
|
|
143
177
|
handler: async () => {
|
|
@@ -154,10 +188,12 @@ export async function runStepLoop(targets, packages) {
|
|
|
154
188
|
// Single package: show combined menu (manager actions + package.json scripts)
|
|
155
189
|
if (targets.length === 1) {
|
|
156
190
|
const pkg = targets[0];
|
|
191
|
+
const dockerEntry = makeDockerEntry(pkg);
|
|
157
192
|
// eslint-disable-next-line no-constant-condition
|
|
158
193
|
while (true) {
|
|
159
194
|
const scriptEntries = makePackageScriptEntries(pkg);
|
|
160
195
|
const entries = [
|
|
196
|
+
...(dockerEntry ? [dockerEntry] : []),
|
|
161
197
|
{
|
|
162
198
|
name: 'manager actions',
|
|
163
199
|
emoji: globalEmoji,
|
|
@@ -170,9 +206,8 @@ export async function runStepLoop(targets, packages) {
|
|
|
170
206
|
{
|
|
171
207
|
name: 'back',
|
|
172
208
|
emoji: 'âŠī¸',
|
|
173
|
-
description: 'Return to package
|
|
209
|
+
description: 'Return to package menu',
|
|
174
210
|
handler: () => {
|
|
175
|
-
// no state change; just exit to the package scripts menu
|
|
176
211
|
state.lastStep = undefined;
|
|
177
212
|
},
|
|
178
213
|
},
|
|
@@ -184,7 +219,31 @@ export async function runStepLoop(targets, packages) {
|
|
|
184
219
|
});
|
|
185
220
|
},
|
|
186
221
|
},
|
|
187
|
-
|
|
222
|
+
{
|
|
223
|
+
name: 'package scripts',
|
|
224
|
+
emoji: 'đ',
|
|
225
|
+
description: 'Run package.json scripts',
|
|
226
|
+
handler: async () => {
|
|
227
|
+
if (scriptEntries.length === 0) {
|
|
228
|
+
console.log(colors.yellow(`No package.json scripts found for ${pkg.name}.`));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const scriptsMenu = [
|
|
232
|
+
...scriptEntries,
|
|
233
|
+
{
|
|
234
|
+
name: 'back',
|
|
235
|
+
emoji: 'âŠī¸',
|
|
236
|
+
description: 'Return to package menu',
|
|
237
|
+
handler: () => { },
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
await runHelperCli({
|
|
241
|
+
title: `${pkg.name} scripts`,
|
|
242
|
+
scripts: scriptsMenu,
|
|
243
|
+
argv: [],
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
},
|
|
188
247
|
{
|
|
189
248
|
name: 'back',
|
|
190
249
|
emoji: 'âŠī¸',
|