@emeryld/manager 1.4.15 → 1.5.1
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/README.md +11 -3
- package/dist/docs/index.js +239 -0
- package/dist/helper-cli/multi-select.js +403 -0
- package/dist/helper-cli/prompts.js +17 -1
- package/dist/helper-cli.js +33 -0
- package/dist/menu.js +0 -18
- package/dist/packages/manifest-utils.js +0 -5
- package/dist/publish.js +51 -7
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -11,16 +11,24 @@ Dev dependency built for Codex agents: scan a pnpm workspace, scaffold RRRoutes
|
|
|
11
11
|
- Headless publish: `pnpm manager-cli all --non-interactive --bump patch --tag next`
|
|
12
12
|
|
|
13
13
|
## Running `manager-cli` interactively
|
|
14
|
-
- **Package selection**: scans for every `package.json` (ignores node_modules/dist). Menu entries include each package (color-coded), "All packages", and
|
|
14
|
+
- **Package selection**: scans for every `package.json` (ignores node_modules/dist). Menu entries include each package (color-coded), "All packages", and a root-level `utils` submenu (`docs`, format checker, robot metadata, create package). Navigate with ↑/↓/j/k or digits to run instantly.
|
|
15
15
|
- **Action menu** for the selection:
|
|
16
16
|
- `update dependencies` → runs `pnpm -r update` (or filtered update), stages only dependency files, prompts for a commit message, commits, and pushes.
|
|
17
17
|
- `test` → `pnpm test` (filtered to the package when possible).
|
|
18
|
-
- `format checker` → prompts for limits, then scans each source file for long functions and class methods, deep indentation, too many components/functions, and repeated/similar snippets before reporting offenders.
|
|
19
18
|
- `build` → `pnpm build` (filtered when a single package is selected).
|
|
20
19
|
- `publish` → ensures the working tree is committed, checks registry auth, prompts a version strategy, commits the bump, tags, publishes with pnpm, and pushes tags.
|
|
21
20
|
- `full` → update → test → build → publish in order.
|
|
22
21
|
- `back` → return to package selection. When a single package is chosen, its `package.json` scripts also appear as runnable entries.
|
|
23
22
|
|
|
23
|
+
## Docs feature
|
|
24
|
+
|
|
25
|
+
From the root picker, open `utils` -> `docs`.
|
|
26
|
+
|
|
27
|
+
- The helper creates/uses `.manager-docs` in your workspace root.
|
|
28
|
+
- Drop files into that folder.
|
|
29
|
+
- Toggle files on/off in the interactive picker (checked rows are included).
|
|
30
|
+
- Press `Space` to bundle all checked files and copy the result to your clipboard.
|
|
31
|
+
|
|
24
32
|
## Format checker configuration
|
|
25
33
|
|
|
26
34
|
`manager-cli` ships with a format checker action that respects defaults in `.vscode/settings.json` under the `manager.formatChecker` key. Define:
|
|
@@ -107,7 +115,7 @@ Before the extraction runs you can also choose how to consume the results. The d
|
|
|
107
115
|
To keep the payload more token-friendly, the interactive settings screen now exposes `Condense output for compact JSON` and `Docstring length limit`. Enabling the condensed output replaces the verbose object format with a small `fields` + `rows` array layout, and the docstring limit trims comments to the first N characters (set the value to `0` for unlimited). Set your preferred defaults in `.vscode/settings.json` under `manager.robot.condenseOutput` and `manager.robot.maxDocStringLength`.
|
|
108
116
|
|
|
109
117
|
### Running with arguments
|
|
110
|
-
Run `pnpm manager-cli`,
|
|
118
|
+
Run `pnpm manager-cli`, then choose `utils` -> `robot metadata`. The helper walks you through:
|
|
111
119
|
|
|
112
120
|
1. Set the symbol kinds to capture (comma-separated list or `all`).
|
|
113
121
|
2. Toggle `Only exported symbols`, adjust `Maximum columns`, `Condense output for compact JSON`, and `Docstring length limit`.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir, readFile, readdir, stat, writeFile, } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import readline from 'node:readline/promises';
|
|
5
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
6
|
+
import { rootDir } from '../helper-cli/env.js';
|
|
7
|
+
import { runHelperCliMultiSelect } from '../helper-cli.js';
|
|
8
|
+
import { colors, logGlobal } from '../utils/log.js';
|
|
9
|
+
const DOCS_FOLDER_NAME = '.manager-docs';
|
|
10
|
+
const DOCS_FALLBACK_FILE = 'docs-feature-output.txt';
|
|
11
|
+
export function buildDocsClipboardText(files) {
|
|
12
|
+
const lines = [];
|
|
13
|
+
lines.push('# docs feature bundle');
|
|
14
|
+
lines.push(`files: ${files.length}`);
|
|
15
|
+
lines.push('');
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
lines.push(`<<<FILE:${file.path}>>>`);
|
|
18
|
+
lines.push(file.content.replace(/\r\n/g, '\n'));
|
|
19
|
+
lines.push('<<<END FILE>>>');
|
|
20
|
+
lines.push('');
|
|
21
|
+
}
|
|
22
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
23
|
+
}
|
|
24
|
+
export function isLikelyBinary(buffer) {
|
|
25
|
+
if (buffer.length === 0)
|
|
26
|
+
return false;
|
|
27
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, 8000));
|
|
28
|
+
for (const byte of sample) {
|
|
29
|
+
if (byte === 0x00)
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
function formatSize(size) {
|
|
35
|
+
if (size < 1024)
|
|
36
|
+
return `${size} B`;
|
|
37
|
+
if (size < 1024 * 1024)
|
|
38
|
+
return `${(size / 1024).toFixed(1)} KB`;
|
|
39
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
40
|
+
}
|
|
41
|
+
async function collectDocsFiles(directory, baseDirectory = directory) {
|
|
42
|
+
const dirents = await readdir(directory, { withFileTypes: true });
|
|
43
|
+
const sorted = [...dirents].sort((a, b) => a.name.localeCompare(b.name));
|
|
44
|
+
const output = [];
|
|
45
|
+
for (const entry of sorted) {
|
|
46
|
+
const absolutePath = path.join(directory, entry.name);
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
output.push(...(await collectDocsFiles(absolutePath, baseDirectory)));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!entry.isFile())
|
|
52
|
+
continue;
|
|
53
|
+
const stats = await stat(absolutePath);
|
|
54
|
+
output.push({
|
|
55
|
+
absolutePath,
|
|
56
|
+
relativePath: path
|
|
57
|
+
.relative(baseDirectory, absolutePath)
|
|
58
|
+
.replace(/\\/g, '/'),
|
|
59
|
+
size: stats.size,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return output;
|
|
63
|
+
}
|
|
64
|
+
async function promptRescan(relativeDirectory) {
|
|
65
|
+
const rl = readline.createInterface({ input, output });
|
|
66
|
+
try {
|
|
67
|
+
const answer = (await rl.question(colors.cyan(`Drop files into ${relativeDirectory}, then press Enter to rescan (or type "b" to go back): `)))
|
|
68
|
+
.trim()
|
|
69
|
+
.toLowerCase();
|
|
70
|
+
return !(answer === 'b' || answer === 'back' || answer === '0');
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
rl.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function openFolderCommands(absoluteDirectory) {
|
|
77
|
+
if (process.platform === 'darwin') {
|
|
78
|
+
return [{ command: 'open', args: [absoluteDirectory] }];
|
|
79
|
+
}
|
|
80
|
+
if (process.platform === 'win32') {
|
|
81
|
+
return [{ command: 'cmd', args: ['/c', 'start', '', absoluteDirectory] }];
|
|
82
|
+
}
|
|
83
|
+
return [
|
|
84
|
+
{ command: 'xdg-open', args: [absoluteDirectory] },
|
|
85
|
+
{ command: 'gio', args: ['open', absoluteDirectory] },
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
function clipboardCommands() {
|
|
89
|
+
if (process.platform === 'darwin') {
|
|
90
|
+
return [{ command: 'pbcopy', args: [] }];
|
|
91
|
+
}
|
|
92
|
+
if (process.platform === 'win32') {
|
|
93
|
+
return [{ command: 'clip', args: [] }];
|
|
94
|
+
}
|
|
95
|
+
return [
|
|
96
|
+
{ command: 'wl-copy', args: [] },
|
|
97
|
+
{ command: 'xclip', args: ['-selection', 'clipboard'] },
|
|
98
|
+
{ command: 'xsel', args: ['--clipboard', '--input'] },
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
function launchCommand(command, args) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const child = spawn(command, args, {
|
|
104
|
+
cwd: rootDir,
|
|
105
|
+
stdio: 'ignore',
|
|
106
|
+
detached: true,
|
|
107
|
+
});
|
|
108
|
+
let settled = false;
|
|
109
|
+
child.once('error', () => {
|
|
110
|
+
if (settled)
|
|
111
|
+
return;
|
|
112
|
+
settled = true;
|
|
113
|
+
resolve(false);
|
|
114
|
+
});
|
|
115
|
+
child.once('spawn', () => {
|
|
116
|
+
if (settled)
|
|
117
|
+
return;
|
|
118
|
+
settled = true;
|
|
119
|
+
child.unref();
|
|
120
|
+
resolve(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function openDocsFolder(absoluteDirectory) {
|
|
125
|
+
for (const candidate of openFolderCommands(absoluteDirectory)) {
|
|
126
|
+
const launched = await launchCommand(candidate.command, candidate.args);
|
|
127
|
+
if (launched)
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
function tryClipboardCommand(command, args, text) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
const child = spawn(command, args, {
|
|
135
|
+
cwd: rootDir,
|
|
136
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
137
|
+
});
|
|
138
|
+
let settled = false;
|
|
139
|
+
const done = (value) => {
|
|
140
|
+
if (settled)
|
|
141
|
+
return;
|
|
142
|
+
settled = true;
|
|
143
|
+
resolve(value);
|
|
144
|
+
};
|
|
145
|
+
child.once('error', () => done(false));
|
|
146
|
+
child.once('close', (code) => done(code === 0));
|
|
147
|
+
if (!child.stdin) {
|
|
148
|
+
done(false);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
child.stdin.on('error', () => done(false));
|
|
152
|
+
child.stdin.end(text);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async function copyToClipboard(text) {
|
|
156
|
+
for (const candidate of clipboardCommands()) {
|
|
157
|
+
const ok = await tryClipboardCommand(candidate.command, candidate.args, text);
|
|
158
|
+
if (ok)
|
|
159
|
+
return candidate;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
async function loadClipboardFiles(selected) {
|
|
164
|
+
const files = [];
|
|
165
|
+
const skipped = [];
|
|
166
|
+
for (const file of selected) {
|
|
167
|
+
const buffer = await readFile(file.absolutePath);
|
|
168
|
+
if (isLikelyBinary(buffer)) {
|
|
169
|
+
skipped.push(file.relativePath);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
files.push({
|
|
173
|
+
path: file.relativePath,
|
|
174
|
+
content: buffer.toString('utf8'),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return { files, skipped };
|
|
178
|
+
}
|
|
179
|
+
export async function runDocsFeature() {
|
|
180
|
+
const docsDirectory = path.join(rootDir, DOCS_FOLDER_NAME);
|
|
181
|
+
await mkdir(docsDirectory, { recursive: true });
|
|
182
|
+
const relativeDocsDirectory = path.relative(rootDir, docsDirectory).replace(/\\/g, '/') || '.';
|
|
183
|
+
logGlobal(`Docs feature folder: ${relativeDocsDirectory}`, colors.cyan);
|
|
184
|
+
const opened = await openDocsFolder(docsDirectory);
|
|
185
|
+
if (opened) {
|
|
186
|
+
logGlobal('Drop files into that folder, then return to this terminal.', colors.dim);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
logGlobal(`Could not auto-open ${relativeDocsDirectory}; add files manually and continue.`, colors.yellow);
|
|
190
|
+
}
|
|
191
|
+
// eslint-disable-next-line no-constant-condition
|
|
192
|
+
while (true) {
|
|
193
|
+
const files = await collectDocsFiles(docsDirectory);
|
|
194
|
+
if (files.length === 0) {
|
|
195
|
+
logGlobal(`No files found in ${relativeDocsDirectory}.`, colors.yellow);
|
|
196
|
+
const shouldRescan = await promptRescan(relativeDocsDirectory);
|
|
197
|
+
if (!shouldRescan)
|
|
198
|
+
return;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const selectedNames = await runHelperCliMultiSelect({
|
|
202
|
+
title: 'Docs feature (toggle files, press Space to copy)',
|
|
203
|
+
scripts: files.map((file) => ({
|
|
204
|
+
name: file.relativePath,
|
|
205
|
+
emoji: '📄',
|
|
206
|
+
description: formatSize(file.size),
|
|
207
|
+
handler: async () => { },
|
|
208
|
+
})),
|
|
209
|
+
argv: [],
|
|
210
|
+
});
|
|
211
|
+
if (!selectedNames)
|
|
212
|
+
return;
|
|
213
|
+
const selectedSet = new Set(selectedNames);
|
|
214
|
+
const selectedFiles = files.filter((file) => selectedSet.has(file.relativePath));
|
|
215
|
+
if (selectedFiles.length === 0) {
|
|
216
|
+
logGlobal('No files selected; nothing copied.', colors.yellow);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const { files: clipboardFiles, skipped } = await loadClipboardFiles(selectedFiles);
|
|
220
|
+
if (!clipboardFiles.length) {
|
|
221
|
+
logGlobal('All selected files looked binary. Select text files instead.', colors.yellow);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const bundle = buildDocsClipboardText(clipboardFiles);
|
|
225
|
+
const clipboardCommand = await copyToClipboard(bundle);
|
|
226
|
+
if (clipboardCommand) {
|
|
227
|
+
logGlobal(`Copied ${clipboardFiles.length} files to clipboard via ${clipboardCommand.command}.`, colors.green);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const fallbackPath = path.join(docsDirectory, DOCS_FALLBACK_FILE);
|
|
231
|
+
await writeFile(fallbackPath, bundle, 'utf8');
|
|
232
|
+
logGlobal(`Clipboard command not found. Wrote bundle to ${path.relative(rootDir, fallbackPath)}.`, colors.yellow);
|
|
233
|
+
}
|
|
234
|
+
if (skipped.length > 0) {
|
|
235
|
+
logGlobal(`Skipped ${skipped.length} binary files: ${skipped.join(', ')}`, colors.yellow);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
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 } from './display.js';
|
|
5
|
+
import { BACK_KEY, buildVisibleOptions, NEXT_KEY, PAGE_SIZE, PREVIOUS_KEY, } from './pagination.js';
|
|
6
|
+
import { findScriptEntry } from './scripts.js';
|
|
7
|
+
function formatInteractiveLines(state, selectedIndex, selectedEntries, title, searchState) {
|
|
8
|
+
const heading = pageHeading(title, state.page, state.pageCount);
|
|
9
|
+
const lines = [heading];
|
|
10
|
+
lines.push(colors.dim(`Selected: ${selectedEntries.size}`));
|
|
11
|
+
if (searchState?.active) {
|
|
12
|
+
const queryLabel = searchState.query || '(type to filter)';
|
|
13
|
+
lines.push(colors.dim(`Search: ${queryLabel}`));
|
|
14
|
+
}
|
|
15
|
+
state.options.forEach((option, index) => {
|
|
16
|
+
const isSelectedRow = index === selectedIndex;
|
|
17
|
+
const pointer = isSelectedRow ? `${colors.green('➤')} ` : '';
|
|
18
|
+
const entryColorizer = option.type === 'entry' && option.entry.color
|
|
19
|
+
? getEntryColor(option.entry)
|
|
20
|
+
: colors.cyan;
|
|
21
|
+
const numberLabelColorizer = isSelectedRow ? colors.green : entryColorizer;
|
|
22
|
+
const numberLabel = numberLabelColorizer(`${option.hotkey}`.padStart(2, ' '));
|
|
23
|
+
if (option.type === 'entry') {
|
|
24
|
+
const isChecked = selectedEntries.has(option.entry);
|
|
25
|
+
const checkbox = isChecked ? colors.green('[x]') : colors.dim('[ ]');
|
|
26
|
+
const labelColorizer = isSelectedRow ? colors.green : entryColorizer;
|
|
27
|
+
const label = labelColorizer(option.entry.displayName);
|
|
28
|
+
lines.push(`${pointer}${numberLabel}. ${checkbox} ${option.entry.emoji} ${label} ${colors.dim(option.entry.metaLabel)}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const icon = option.action === 'back'
|
|
32
|
+
? '↩️'
|
|
33
|
+
: option.action === 'previous'
|
|
34
|
+
? '⬅️'
|
|
35
|
+
: '➡️';
|
|
36
|
+
const baseLabel = option.action === 'previous'
|
|
37
|
+
? 'Previous page'
|
|
38
|
+
: option.action === 'next'
|
|
39
|
+
? 'Next page'
|
|
40
|
+
: 'Back';
|
|
41
|
+
const navLabel = isSelectedRow && option.enabled
|
|
42
|
+
? colors.green(baseLabel)
|
|
43
|
+
: option.enabled
|
|
44
|
+
? baseLabel
|
|
45
|
+
: colors.dim(baseLabel);
|
|
46
|
+
lines.push(`${pointer}${numberLabel}. ${icon} ${navLabel}`);
|
|
47
|
+
});
|
|
48
|
+
if (searchState?.active && !searchState.hasResults) {
|
|
49
|
+
lines.push(colors.yellow('No scripts match your search.'));
|
|
50
|
+
}
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push(colors.dim(`Use ↑/↓ (or j/k) to move, Enter or 1-${PAGE_SIZE} to toggle, Space to run selected, ${PREVIOUS_KEY} prev page, ${NEXT_KEY} next page, ${BACK_KEY} back, Type to filter, Esc/Ctrl+C to exit.`));
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
function renderInteractiveList(lines, previousLineCount) {
|
|
56
|
+
const columns = process.stdout.columns ?? 80;
|
|
57
|
+
const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
58
|
+
const visibleLength = (text) => {
|
|
59
|
+
const clean = text.replace(ansiRegex, '');
|
|
60
|
+
let length = 0;
|
|
61
|
+
for (const ch of clean) {
|
|
62
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
63
|
+
length += code > 0xffff ? 2 : 1;
|
|
64
|
+
}
|
|
65
|
+
return length;
|
|
66
|
+
};
|
|
67
|
+
const nextLineCount = lines.reduce((total, line) => {
|
|
68
|
+
const len = visibleLength(line);
|
|
69
|
+
const wrapped = Math.max(1, Math.ceil(len / columns));
|
|
70
|
+
return total + wrapped;
|
|
71
|
+
}, 0);
|
|
72
|
+
if (previousLineCount > 0) {
|
|
73
|
+
process.stdout.write(`\x1b[${previousLineCount}A`);
|
|
74
|
+
process.stdout.write('\x1b[0J');
|
|
75
|
+
}
|
|
76
|
+
lines.forEach((line) => console.log(line));
|
|
77
|
+
return nextLineCount;
|
|
78
|
+
}
|
|
79
|
+
function renderReadlinePage(state, selectedEntries, title) {
|
|
80
|
+
console.log(pageHeading(title, state.page, state.pageCount));
|
|
81
|
+
console.log(colors.dim(`Selected: ${selectedEntries.size}`));
|
|
82
|
+
state.options.forEach((option) => {
|
|
83
|
+
const colorizer = option.type === 'entry' && option.entry.color
|
|
84
|
+
? getEntryColor(option.entry)
|
|
85
|
+
: colors.cyan;
|
|
86
|
+
const numberLabel = colorizer(String(option.hotkey).padStart(2, ' '));
|
|
87
|
+
if (option.type === 'entry') {
|
|
88
|
+
const isChecked = selectedEntries.has(option.entry);
|
|
89
|
+
const checkbox = isChecked ? colors.green('[x]') : colors.dim('[ ]');
|
|
90
|
+
const label = option.entry.color
|
|
91
|
+
? colorizer(option.entry.displayName)
|
|
92
|
+
: option.entry.displayName;
|
|
93
|
+
console.log(` ${numberLabel} ${checkbox} ${option.entry.emoji} ${label} ${colors.dim(option.entry.metaLabel)}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const icon = option.action === 'back'
|
|
97
|
+
? '↩️'
|
|
98
|
+
: option.action === 'previous'
|
|
99
|
+
? '⬅️'
|
|
100
|
+
: '➡️';
|
|
101
|
+
const label = option.action === 'previous'
|
|
102
|
+
? 'Previous page'
|
|
103
|
+
: option.action === 'next'
|
|
104
|
+
? 'Next page'
|
|
105
|
+
: 'Back';
|
|
106
|
+
const display = option.enabled ? label : colors.dim(label);
|
|
107
|
+
console.log(` ${numberLabel} ${icon} ${display}`);
|
|
108
|
+
});
|
|
109
|
+
console.log(colors.dim(`Toggle by number/name. Type "run" (or "space") to continue, ${BACK_KEY} to go back.`));
|
|
110
|
+
}
|
|
111
|
+
async function promptWithReadline(entries, title) {
|
|
112
|
+
let page = 0;
|
|
113
|
+
const selectedEntries = new Set();
|
|
114
|
+
const rl = readline.createInterface({ input, output });
|
|
115
|
+
try {
|
|
116
|
+
while (true) {
|
|
117
|
+
const state = buildVisibleOptions(entries, page);
|
|
118
|
+
page = state.page;
|
|
119
|
+
renderReadlinePage(state, selectedEntries, title);
|
|
120
|
+
const answer = (await rl.question(colors.cyan('\nToggle option by number or name (run/space/0): '))).trim();
|
|
121
|
+
if (!answer)
|
|
122
|
+
continue;
|
|
123
|
+
const normalized = answer.toLowerCase();
|
|
124
|
+
if (normalized === 'run' || normalized === 'space') {
|
|
125
|
+
if (selectedEntries.size === 0) {
|
|
126
|
+
console.log(colors.yellow('No entries selected. Toggle at least one.'));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
return entries.filter((entry) => selectedEntries.has(entry));
|
|
130
|
+
}
|
|
131
|
+
const numeric = Number.parseInt(answer, 10);
|
|
132
|
+
if (!Number.isNaN(numeric)) {
|
|
133
|
+
const option = state.options.find((opt) => opt.hotkey === numeric);
|
|
134
|
+
if (!option) {
|
|
135
|
+
console.log(colors.yellow(`Unknown option "${answer}". Try again.`));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (option.type === 'entry') {
|
|
139
|
+
if (selectedEntries.has(option.entry))
|
|
140
|
+
selectedEntries.delete(option.entry);
|
|
141
|
+
else
|
|
142
|
+
selectedEntries.add(option.entry);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (option.action === 'back')
|
|
146
|
+
return undefined;
|
|
147
|
+
if (option.action === 'previous') {
|
|
148
|
+
if (option.enabled)
|
|
149
|
+
page = Math.max(0, state.page - 1);
|
|
150
|
+
else
|
|
151
|
+
console.log(colors.yellow('No previous page.'));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (option.action === 'next') {
|
|
155
|
+
if (option.enabled)
|
|
156
|
+
page = Math.min(state.pageCount - 1, state.page + 1);
|
|
157
|
+
else
|
|
158
|
+
console.log(colors.yellow('No next page.'));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const byName = findScriptEntry(entries, answer);
|
|
163
|
+
if (byName) {
|
|
164
|
+
if (selectedEntries.has(byName))
|
|
165
|
+
selectedEntries.delete(byName);
|
|
166
|
+
else
|
|
167
|
+
selectedEntries.add(byName);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
console.log(colors.yellow(`Could not find "${answer}". Try again.`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
rl.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export async function promptForScriptMultiSelect(entries, title) {
|
|
178
|
+
const supportsRawMode = typeof input.setRawMode === 'function' && input.isTTY;
|
|
179
|
+
if (!supportsRawMode) {
|
|
180
|
+
return promptWithReadline(entries, title);
|
|
181
|
+
}
|
|
182
|
+
const wasRaw = input.isRaw;
|
|
183
|
+
if (!wasRaw) {
|
|
184
|
+
input.setRawMode(true);
|
|
185
|
+
input.resume();
|
|
186
|
+
}
|
|
187
|
+
process.stdout.write('\x1b[?25l');
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
const baseEntries = entries;
|
|
190
|
+
let filteredEntries = baseEntries;
|
|
191
|
+
const selectedEntries = new Set();
|
|
192
|
+
let selectedIndex = 0;
|
|
193
|
+
let renderedLines = 0;
|
|
194
|
+
let state = buildVisibleOptions(filteredEntries, 0);
|
|
195
|
+
let searchActive = false;
|
|
196
|
+
let searchQuery = '';
|
|
197
|
+
let pageBeforeSearch = 0;
|
|
198
|
+
let selectionBeforeSearch = 0;
|
|
199
|
+
const cleanup = () => {
|
|
200
|
+
if (renderedLines > 0) {
|
|
201
|
+
process.stdout.write(`\x1b[${renderedLines}A`);
|
|
202
|
+
process.stdout.write('\x1b[0J');
|
|
203
|
+
renderedLines = 0;
|
|
204
|
+
}
|
|
205
|
+
process.stdout.write('\x1b[?25h');
|
|
206
|
+
if (!wasRaw) {
|
|
207
|
+
input.setRawMode(false);
|
|
208
|
+
input.pause();
|
|
209
|
+
}
|
|
210
|
+
input.removeListener('data', onData);
|
|
211
|
+
};
|
|
212
|
+
const commitSelection = () => {
|
|
213
|
+
cleanup();
|
|
214
|
+
console.log();
|
|
215
|
+
resolve(baseEntries.filter((entry) => selectedEntries.has(entry)));
|
|
216
|
+
};
|
|
217
|
+
const commitBack = () => {
|
|
218
|
+
cleanup();
|
|
219
|
+
console.log();
|
|
220
|
+
resolve(undefined);
|
|
221
|
+
};
|
|
222
|
+
const rebuildState = (page) => {
|
|
223
|
+
state = buildVisibleOptions(filteredEntries, page);
|
|
224
|
+
selectedIndex = Math.min(selectedIndex, state.options.length - 1);
|
|
225
|
+
selectedIndex = Math.max(0, selectedIndex);
|
|
226
|
+
};
|
|
227
|
+
const render = () => {
|
|
228
|
+
const searchState = searchActive
|
|
229
|
+
? {
|
|
230
|
+
active: true,
|
|
231
|
+
query: searchQuery.trim(),
|
|
232
|
+
hasResults: filteredEntries.length > 0,
|
|
233
|
+
}
|
|
234
|
+
: undefined;
|
|
235
|
+
const lines = formatInteractiveLines(state, selectedIndex, selectedEntries, title, searchState);
|
|
236
|
+
renderedLines = renderInteractiveList(lines, renderedLines);
|
|
237
|
+
};
|
|
238
|
+
const setPage = (page) => {
|
|
239
|
+
rebuildState(page);
|
|
240
|
+
render();
|
|
241
|
+
};
|
|
242
|
+
const handleNav = (option) => {
|
|
243
|
+
if (option.action === 'back') {
|
|
244
|
+
commitBack();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (option.action === 'previous') {
|
|
248
|
+
if (option.enabled)
|
|
249
|
+
setPage(state.page - 1);
|
|
250
|
+
else
|
|
251
|
+
process.stdout.write('\x07');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (option.action === 'next') {
|
|
255
|
+
if (option.enabled)
|
|
256
|
+
setPage(state.page + 1);
|
|
257
|
+
else
|
|
258
|
+
process.stdout.write('\x07');
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const toggleEntry = (entry) => {
|
|
262
|
+
if (selectedEntries.has(entry))
|
|
263
|
+
selectedEntries.delete(entry);
|
|
264
|
+
else
|
|
265
|
+
selectedEntries.add(entry);
|
|
266
|
+
render();
|
|
267
|
+
};
|
|
268
|
+
const activateOption = (option) => {
|
|
269
|
+
if (option.type === 'entry') {
|
|
270
|
+
toggleEntry(option.entry);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
handleNav(option);
|
|
274
|
+
};
|
|
275
|
+
const matchesSearchTerm = (entry, term) => {
|
|
276
|
+
const haystack = [
|
|
277
|
+
entry.displayName,
|
|
278
|
+
entry.metaLabel,
|
|
279
|
+
entry.script ?? '',
|
|
280
|
+
entry.description ?? '',
|
|
281
|
+
]
|
|
282
|
+
.join(' ')
|
|
283
|
+
.toLowerCase();
|
|
284
|
+
return haystack.includes(term);
|
|
285
|
+
};
|
|
286
|
+
const applySearch = () => {
|
|
287
|
+
const normalized = searchQuery.trim().toLowerCase();
|
|
288
|
+
if (normalized.length > 0) {
|
|
289
|
+
filteredEntries = baseEntries.filter((entry) => matchesSearchTerm(entry, normalized));
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
filteredEntries = baseEntries;
|
|
293
|
+
}
|
|
294
|
+
rebuildState(0);
|
|
295
|
+
selectedIndex = 0;
|
|
296
|
+
render();
|
|
297
|
+
};
|
|
298
|
+
const enterSearchMode = (initialQuery = '') => {
|
|
299
|
+
if (searchActive)
|
|
300
|
+
return;
|
|
301
|
+
pageBeforeSearch = state.page;
|
|
302
|
+
selectionBeforeSearch = selectedIndex;
|
|
303
|
+
searchActive = true;
|
|
304
|
+
searchQuery = initialQuery;
|
|
305
|
+
applySearch();
|
|
306
|
+
};
|
|
307
|
+
const exitSearchMode = () => {
|
|
308
|
+
if (!searchActive)
|
|
309
|
+
return;
|
|
310
|
+
searchActive = false;
|
|
311
|
+
searchQuery = '';
|
|
312
|
+
filteredEntries = baseEntries;
|
|
313
|
+
state = buildVisibleOptions(filteredEntries, pageBeforeSearch);
|
|
314
|
+
selectedIndex = Math.min(selectionBeforeSearch, state.options.length - 1);
|
|
315
|
+
render();
|
|
316
|
+
};
|
|
317
|
+
const onData = (buffer) => {
|
|
318
|
+
const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
|
|
319
|
+
const isArrowDown = buffer.equals(Buffer.from([0x1b, 0x5b, 0x42]));
|
|
320
|
+
const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
|
|
321
|
+
const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
|
|
322
|
+
const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
|
|
323
|
+
const isBackspace = buffer.length === 1 && (buffer[0] === 0x7f || buffer[0] === 0x08);
|
|
324
|
+
const isSpace = buffer.length === 1 && buffer[0] === 0x20;
|
|
325
|
+
const ascii = buffer.length === 1 ? buffer[0] : undefined;
|
|
326
|
+
const isPrintable = ascii !== undefined && ascii >= 0x20 && ascii <= 0x7e;
|
|
327
|
+
const isDigit = ascii !== undefined && ascii >= 0x30 && ascii <= 0x39;
|
|
328
|
+
const typedChar = isPrintable && ascii !== undefined ? String.fromCharCode(ascii) : '';
|
|
329
|
+
if (isCtrlC) {
|
|
330
|
+
cleanup();
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
if (isEscape) {
|
|
334
|
+
if (searchActive) {
|
|
335
|
+
exitSearchMode();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
cleanup();
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
if (isArrowUp ||
|
|
342
|
+
(buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
|
|
343
|
+
selectedIndex =
|
|
344
|
+
(selectedIndex - 1 + state.options.length) % state.options.length;
|
|
345
|
+
render();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (isArrowDown ||
|
|
349
|
+
(buffer.length === 1 && (buffer[0] === 0x6a || buffer[0] === 0x4a))) {
|
|
350
|
+
selectedIndex = (selectedIndex + 1) % state.options.length;
|
|
351
|
+
render();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (isSpace) {
|
|
355
|
+
if (selectedEntries.size === 0) {
|
|
356
|
+
process.stdout.write('\x07');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
commitSelection();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (isEnter) {
|
|
363
|
+
const option = state.options[selectedIndex];
|
|
364
|
+
if (option)
|
|
365
|
+
activateOption(option);
|
|
366
|
+
else
|
|
367
|
+
process.stdout.write('\x07');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (searchActive) {
|
|
371
|
+
if (isBackspace) {
|
|
372
|
+
if (searchQuery.length > 0) {
|
|
373
|
+
searchQuery = searchQuery.slice(0, -1);
|
|
374
|
+
applySearch();
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (isPrintable) {
|
|
379
|
+
searchQuery += typedChar;
|
|
380
|
+
applySearch();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (isDigit && ascii !== undefined) {
|
|
386
|
+
const numericValue = ascii - 0x30;
|
|
387
|
+
const option = state.options.find((opt) => opt.hotkey === numericValue);
|
|
388
|
+
if (option)
|
|
389
|
+
activateOption(option);
|
|
390
|
+
else
|
|
391
|
+
process.stdout.write('\x07');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (isPrintable) {
|
|
395
|
+
enterSearchMode(typedChar);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
process.stdout.write('\x07');
|
|
399
|
+
};
|
|
400
|
+
input.on('data', onData);
|
|
401
|
+
render();
|
|
402
|
+
});
|
|
403
|
+
}
|
|
@@ -110,12 +110,28 @@ function formatInteractiveLines(state, selectedIndex, title, searchState) {
|
|
|
110
110
|
return lines;
|
|
111
111
|
}
|
|
112
112
|
function renderInteractiveList(lines, previousLineCount) {
|
|
113
|
+
const columns = process.stdout.columns ?? 80;
|
|
114
|
+
const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
115
|
+
const visibleLength = (text) => {
|
|
116
|
+
const clean = text.replace(ansiRegex, '');
|
|
117
|
+
let length = 0;
|
|
118
|
+
for (const ch of clean) {
|
|
119
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
120
|
+
length += code > 0xffff ? 2 : 1;
|
|
121
|
+
}
|
|
122
|
+
return length;
|
|
123
|
+
};
|
|
124
|
+
const nextLineCount = lines.reduce((total, line) => {
|
|
125
|
+
const len = visibleLength(line);
|
|
126
|
+
const wrapped = Math.max(1, Math.ceil(len / columns));
|
|
127
|
+
return total + wrapped;
|
|
128
|
+
}, 0);
|
|
113
129
|
if (previousLineCount > 0) {
|
|
114
130
|
process.stdout.write(`\x1b[${previousLineCount}A`);
|
|
115
131
|
process.stdout.write('\x1b[0J');
|
|
116
132
|
}
|
|
117
133
|
lines.forEach((line) => console.log(line));
|
|
118
|
-
return
|
|
134
|
+
return nextLineCount;
|
|
119
135
|
}
|
|
120
136
|
export async function promptForScript(entries, title) {
|
|
121
137
|
const supportsRawMode = typeof input.setRawMode === 'function' && input.isTTY;
|
package/dist/helper-cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { colors } from './helper-cli/colors.js';
|
|
2
2
|
import { printScriptList } from './helper-cli/display.js';
|
|
3
|
+
import { promptForScriptMultiSelect } from './helper-cli/multi-select.js';
|
|
3
4
|
import { promptForScript } from './helper-cli/prompts.js';
|
|
4
5
|
import { findScriptEntry, normalizeScripts } from './helper-cli/scripts.js';
|
|
5
6
|
import { runEntry } from './helper-cli/runner.js';
|
|
@@ -40,3 +41,35 @@ export async function runHelperCli({ scripts, title = 'Helper CLI', argv = proce
|
|
|
40
41
|
}
|
|
41
42
|
return false;
|
|
42
43
|
}
|
|
44
|
+
export async function runHelperCliMultiSelect({ scripts, title = 'Helper CLI', argv = process.argv.slice(2), }) {
|
|
45
|
+
const normalized = normalizeScripts(scripts);
|
|
46
|
+
let args = Array.isArray(argv) ? [...argv] : [];
|
|
47
|
+
const separatorIndex = args.indexOf('--');
|
|
48
|
+
if (separatorIndex !== -1) {
|
|
49
|
+
args = [...args.slice(0, separatorIndex), ...args.slice(separatorIndex + 1)];
|
|
50
|
+
}
|
|
51
|
+
const [firstArg] = args;
|
|
52
|
+
if (firstArg === '--list' || firstArg === '-l') {
|
|
53
|
+
printScriptList(normalized, title);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (firstArg === '--help' || firstArg === '-h') {
|
|
57
|
+
console.log(colors.magenta('Usage:'));
|
|
58
|
+
console.log(' pnpm run <cli> [script]');
|
|
59
|
+
console.log('\nFlags:');
|
|
60
|
+
console.log(' --list Show available scripts');
|
|
61
|
+
console.log(' --help Show this information');
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const argLooksLikeScript = firstArg && !firstArg.startsWith('-');
|
|
65
|
+
if (argLooksLikeScript) {
|
|
66
|
+
const requested = findScriptEntry(normalized, firstArg);
|
|
67
|
+
if (requested)
|
|
68
|
+
return [requested.displayName];
|
|
69
|
+
console.log(colors.yellow(`Unknown script "${firstArg}". Falling back to interactive selection…`));
|
|
70
|
+
}
|
|
71
|
+
const selection = await promptForScriptMultiSelect(normalized, title);
|
|
72
|
+
if (!selection)
|
|
73
|
+
return undefined;
|
|
74
|
+
return selection.map((entry) => entry.displayName);
|
|
75
|
+
}
|
package/dist/menu.js
CHANGED
|
@@ -9,8 +9,6 @@ import { ensureWorkingTreeCommitted } from './preflight.js';
|
|
|
9
9
|
import { openDockerHelper } from './docker.js';
|
|
10
10
|
import { run } from './utils/run.js';
|
|
11
11
|
import { makeBaseScriptEntries, getPackageMarker, getPackageModule } from './menu/script-helpers.js';
|
|
12
|
-
import { runFormatChecker } from './format-checker/index.js';
|
|
13
|
-
import { runRobot } from './robot/index.js';
|
|
14
12
|
import { describeVersionControlScope, makeVersionControlEntries, } from './version-control.js';
|
|
15
13
|
function formatKindLabel(kind) {
|
|
16
14
|
if (kind === 'cli')
|
|
@@ -70,22 +68,6 @@ function makeManagerStepEntries(targets, packages, state) {
|
|
|
70
68
|
state.lastStep = 'test';
|
|
71
69
|
},
|
|
72
70
|
},
|
|
73
|
-
{
|
|
74
|
-
name: 'format checker',
|
|
75
|
-
emoji: '🧮',
|
|
76
|
-
description: 'Scan for long functions, deep indentation, too many files/components, and repeated snippets',
|
|
77
|
-
handler: async () => {
|
|
78
|
-
await runFormatChecker();
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
name: 'robot metadata',
|
|
83
|
-
emoji: '🤖',
|
|
84
|
-
description: 'Collect functions, components, types, consts, and classes across the workspace',
|
|
85
|
-
handler: async () => {
|
|
86
|
-
await runRobot();
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
71
|
{
|
|
90
72
|
name: 'build',
|
|
91
73
|
emoji: '🏗️',
|
|
@@ -122,11 +122,6 @@ export function mergeManifestEntries(rootDir, inferred, overrides) {
|
|
|
122
122
|
merged.push({ ...baseEntry, path: normalized });
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
normalizedOverrides.forEach((entry) => {
|
|
126
|
-
const name = entry.name || path.basename(entry.path) || 'package';
|
|
127
|
-
const color = entry.color ?? colorFromSeed(name);
|
|
128
|
-
merged.push({ name, path: entry.path, color });
|
|
129
|
-
});
|
|
130
125
|
return merged;
|
|
131
126
|
}
|
|
132
127
|
export function colorFromSeed(seed) {
|
package/dist/publish.js
CHANGED
|
@@ -7,10 +7,12 @@ import { runHelperCli } from './helper-cli.js';
|
|
|
7
7
|
import { buildPackageSelectionMenu, runStepLoop } from './menu.js';
|
|
8
8
|
import { getOrderedPackages, loadPackages, resolvePackage } from './packages.js';
|
|
9
9
|
import { releaseMultiple, releaseSingle, } from './release.js';
|
|
10
|
-
import { runFormatCheckerScanCli } from './format-checker/index.js';
|
|
10
|
+
import { runFormatChecker, runFormatCheckerScanCli } from './format-checker/index.js';
|
|
11
11
|
import { ensureWorkingTreeCommitted } from './preflight.js';
|
|
12
12
|
import { publishCliState } from './prompts.js';
|
|
13
13
|
import { createRrrPackage, runCreatePackageCli, } from './create-package/index.js';
|
|
14
|
+
import { runRobot } from './robot/index.js';
|
|
15
|
+
import { runDocsFeature } from './docs/index.js';
|
|
14
16
|
import { colors, logGlobal } from './utils/log.js';
|
|
15
17
|
import { run } from './utils/run.js';
|
|
16
18
|
const MANAGER_PACKAGE = '@emeryld/manager';
|
|
@@ -183,12 +185,54 @@ async function runPackageSelectionLoop(packages, helperArgs) {
|
|
|
183
185
|
lastStep = step;
|
|
184
186
|
}),
|
|
185
187
|
{
|
|
186
|
-
name: '
|
|
187
|
-
emoji: '
|
|
188
|
-
description: '
|
|
188
|
+
name: 'utils',
|
|
189
|
+
emoji: '🛠️',
|
|
190
|
+
description: 'Docs, format checker, robot metadata, and package scaffolding',
|
|
189
191
|
handler: async () => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
// eslint-disable-next-line no-constant-condition
|
|
193
|
+
while (true) {
|
|
194
|
+
const ranUtility = await runHelperCli({
|
|
195
|
+
title: 'Utilities',
|
|
196
|
+
scripts: [
|
|
197
|
+
{
|
|
198
|
+
name: 'docs',
|
|
199
|
+
emoji: '📚',
|
|
200
|
+
description: 'Pick files from .manager-docs and copy a bundled payload to clipboard',
|
|
201
|
+
handler: async () => {
|
|
202
|
+
await runDocsFeature();
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'format checker',
|
|
207
|
+
emoji: '🧮',
|
|
208
|
+
description: 'Scan for long functions, deep indentation, and repeated snippets',
|
|
209
|
+
handler: async () => {
|
|
210
|
+
await runFormatChecker();
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'robot metadata',
|
|
215
|
+
emoji: '🤖',
|
|
216
|
+
description: 'Collect functions, components, types, consts, and classes',
|
|
217
|
+
handler: async () => {
|
|
218
|
+
await runRobot();
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'Create package',
|
|
223
|
+
emoji: '✨',
|
|
224
|
+
description: 'Scaffold a new rrr package (contract/server/client)',
|
|
225
|
+
handler: async () => {
|
|
226
|
+
await createRrrPackage();
|
|
227
|
+
currentPackages = await loadPackages();
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
argv: [],
|
|
232
|
+
});
|
|
233
|
+
if (!ranUtility)
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
192
236
|
lastStep = 'back';
|
|
193
237
|
},
|
|
194
238
|
},
|
|
@@ -238,7 +282,7 @@ async function main() {
|
|
|
238
282
|
return;
|
|
239
283
|
}
|
|
240
284
|
if (packages.length === 0) {
|
|
241
|
-
logGlobal('No packages found (no package.json discovered). Use "Create package" to scaffold one.', colors.yellow);
|
|
285
|
+
logGlobal('No packages found (no package.json discovered). Use "utils" -> "Create package" to scaffold one.', colors.yellow);
|
|
242
286
|
}
|
|
243
287
|
// Interactive flow (unchanged): selection menu then step menu
|
|
244
288
|
if (parsed.selectionArg) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emeryld/manager",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"node": ">=18"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"semver": "^7.7.
|
|
26
|
+
"semver": "^7.7.4",
|
|
27
27
|
"ts-morph": "^27.0.2",
|
|
28
|
-
"ts-node": "^10.9.
|
|
29
|
-
"typescript": "^5.3
|
|
28
|
+
"ts-node": "^10.9.2",
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@types/node": "^20.
|
|
32
|
+
"@types/node": "^20.19.33",
|
|
33
33
|
"@types/semver": "^7.7.1",
|
|
34
34
|
"cross-env": "^7.0.3"
|
|
35
35
|
},
|