@emeryld/manager 1.3.0 → 1.4.0
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 +96 -0
- package/dist/create-package/cli-args.js +78 -0
- package/dist/create-package/prompts.js +138 -0
- package/dist/create-package/shared/configs.js +309 -0
- package/dist/create-package/shared/constants.js +5 -0
- package/dist/create-package/shared/fs-utils.js +69 -0
- package/dist/create-package/tasks.js +89 -0
- package/dist/create-package/types.js +1 -0
- package/dist/create-package/variant-info.js +67 -0
- package/dist/create-package/variants/client/expo-react-native/lib-files.js +168 -0
- package/dist/create-package/variants/client/expo-react-native/package-files.js +94 -0
- package/dist/create-package/variants/client/expo-react-native/scaffold.js +59 -0
- package/dist/create-package/variants/client/expo-react-native/ui-files.js +215 -0
- package/dist/create-package/variants/client/vite-react/health-page.js +251 -0
- package/dist/create-package/variants/client/vite-react/lib-files.js +176 -0
- package/dist/create-package/variants/client/vite-react/package-files.js +79 -0
- package/dist/create-package/variants/client/vite-react/scaffold.js +68 -0
- package/dist/create-package/variants/client/vite-react/ui-files.js +154 -0
- package/dist/create-package/variants/fullstack/files.js +129 -0
- package/dist/create-package/variants/fullstack/index.js +86 -0
- package/dist/create-package/variants/fullstack/utils.js +241 -0
- package/dist/llm-pack.js +2 -0
- package/dist/robot/cli/prompts.js +84 -27
- package/dist/robot/cli/settings.js +131 -56
- package/dist/robot/config.js +123 -50
- package/dist/robot/coordinator.js +10 -105
- package/dist/robot/extractors/classes.js +14 -13
- package/dist/robot/extractors/components.js +17 -10
- package/dist/robot/extractors/constants.js +9 -6
- package/dist/robot/extractors/functions.js +11 -8
- package/dist/robot/extractors/shared.js +6 -1
- package/dist/robot/extractors/types.js +5 -8
- package/dist/robot/llm-pack.js +1226 -0
- package/dist/robot/pack/builder.js +374 -0
- package/dist/robot/pack/cli.js +65 -0
- package/dist/robot/pack/exemplars.js +573 -0
- package/dist/robot/pack/globs.js +119 -0
- package/dist/robot/pack/selection.js +44 -0
- package/dist/robot/pack/symbols.js +309 -0
- package/dist/robot/pack/type-registry.js +285 -0
- package/dist/robot/pack/types.js +48 -0
- package/dist/robot/pack/utils.js +36 -0
- package/dist/robot/serializer.js +97 -0
- package/dist/robot/v2/cli.js +86 -0
- package/dist/robot/v2/globs.js +103 -0
- package/dist/robot/v2/parser/bundles.js +55 -0
- package/dist/robot/v2/parser/candidates.js +63 -0
- package/dist/robot/v2/parser/exemplars.js +114 -0
- package/dist/robot/v2/parser/exports.js +57 -0
- package/dist/robot/v2/parser/symbols.js +179 -0
- package/dist/robot/v2/parser.js +114 -0
- package/dist/robot/v2/types.js +42 -0
- package/dist/utils/export.js +39 -18
- package/package.json +2 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function deriveNames(baseName) {
|
|
4
|
+
const normalized = baseName.trim();
|
|
5
|
+
return {
|
|
6
|
+
contract: `@${normalized}/contract`,
|
|
7
|
+
server: `@${normalized}/server`,
|
|
8
|
+
client: `@${normalized}/client`,
|
|
9
|
+
docker: `${normalized}-docker`,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function deriveDirs(rootDir, baseName) {
|
|
13
|
+
const packagesRoot = path.join(rootDir, 'packages');
|
|
14
|
+
return {
|
|
15
|
+
root: rootDir,
|
|
16
|
+
packagesRoot,
|
|
17
|
+
contract: path.join(packagesRoot, `${baseName}-contract`),
|
|
18
|
+
server: path.join(packagesRoot, `${baseName}-server`),
|
|
19
|
+
client: path.join(packagesRoot, `${baseName}-client`),
|
|
20
|
+
docker: path.join(packagesRoot, `${baseName}-docker`),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function toPosixPath(value) {
|
|
24
|
+
return value.split(path.sep).join('/');
|
|
25
|
+
}
|
|
26
|
+
export async function pathExists(target) {
|
|
27
|
+
try {
|
|
28
|
+
await access(target);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function normalizeWorkspaceGlob(value) {
|
|
36
|
+
const unquoted = value.replace(/^['"]|['"]$/g, '').trim();
|
|
37
|
+
const normalized = toPosixPath(unquoted || '');
|
|
38
|
+
return normalized.replace(/^\.\//, '');
|
|
39
|
+
}
|
|
40
|
+
function globCoversCandidate(pattern, candidate) {
|
|
41
|
+
const normalizedPattern = normalizeWorkspaceGlob(pattern);
|
|
42
|
+
const normalizedCandidate = normalizeWorkspaceGlob(candidate);
|
|
43
|
+
if (!normalizedPattern || !normalizedCandidate)
|
|
44
|
+
return false;
|
|
45
|
+
if (normalizedPattern === normalizedCandidate)
|
|
46
|
+
return true;
|
|
47
|
+
if (normalizedPattern === '*' || normalizedPattern === '**')
|
|
48
|
+
return true;
|
|
49
|
+
if (normalizedPattern.endsWith('/**')) {
|
|
50
|
+
return normalizedCandidate.startsWith(normalizedPattern.slice(0, -3));
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
function patternsContain(patterns, candidate) {
|
|
55
|
+
return patterns.some((pattern) => globCoversCandidate(pattern, candidate));
|
|
56
|
+
}
|
|
57
|
+
function parsePnpmWorkspacePackages(raw) {
|
|
58
|
+
const packages = [];
|
|
59
|
+
const lines = raw.split(/\r?\n/);
|
|
60
|
+
const packagesIndex = lines.findIndex((line) => line.trim().startsWith('packages:'));
|
|
61
|
+
if (packagesIndex === -1)
|
|
62
|
+
return packages;
|
|
63
|
+
const baseIndent = lines[packagesIndex]?.match(/^(\s*)/)?.[1] ?? '';
|
|
64
|
+
for (let i = packagesIndex + 1; i < lines.length; i++) {
|
|
65
|
+
const line = lines[i];
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
continue;
|
|
69
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
70
|
+
if (indent.length <= baseIndent.length && !trimmed.startsWith('-'))
|
|
71
|
+
break;
|
|
72
|
+
if (!trimmed.startsWith('-')) {
|
|
73
|
+
if (indent.length <= baseIndent.length)
|
|
74
|
+
break;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const value = trimmed.slice(1).trim().replace(/^['"]|['"]$/g, '');
|
|
78
|
+
if (value)
|
|
79
|
+
packages.push(value);
|
|
80
|
+
}
|
|
81
|
+
return packages;
|
|
82
|
+
}
|
|
83
|
+
function insertIntoPnpmWorkspace(raw, pattern) {
|
|
84
|
+
const normalizedPattern = normalizeWorkspaceGlob(pattern);
|
|
85
|
+
if (!normalizedPattern)
|
|
86
|
+
return { added: false, content: raw };
|
|
87
|
+
const existing = parsePnpmWorkspacePackages(raw);
|
|
88
|
+
if (patternsContain(existing, normalizedPattern)) {
|
|
89
|
+
return { added: false, content: raw };
|
|
90
|
+
}
|
|
91
|
+
const lines = raw.split(/\r?\n/);
|
|
92
|
+
const packagesIndex = lines.findIndex((line) => line.trim().startsWith('packages:'));
|
|
93
|
+
const entryLine = packagesIndex === -1
|
|
94
|
+
? ` - '${normalizedPattern}'`
|
|
95
|
+
: `${lines[packagesIndex]?.match(/^(\s*)/)?.[1] ?? ''} - '${normalizedPattern}'`;
|
|
96
|
+
if (packagesIndex === -1) {
|
|
97
|
+
const prefix = raw.trimEnd() ? `${raw.trimEnd()}\n` : '';
|
|
98
|
+
const content = `${prefix}packages:\n${entryLine}\n`;
|
|
99
|
+
return { added: true, content };
|
|
100
|
+
}
|
|
101
|
+
let insertAt = packagesIndex + 1;
|
|
102
|
+
const baseIndent = lines[packagesIndex]?.match(/^(\s*)/)?.[1] ?? '';
|
|
103
|
+
for (let i = packagesIndex + 1; i < lines.length; i++) {
|
|
104
|
+
const line = lines[i];
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
107
|
+
if (!trimmed) {
|
|
108
|
+
insertAt = i + 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (indent.length <= baseIndent.length && !trimmed.startsWith('-')) {
|
|
112
|
+
insertAt = i;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (trimmed.startsWith('-')) {
|
|
116
|
+
insertAt = i + 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (indent.length <= baseIndent.length) {
|
|
120
|
+
insertAt = i;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
lines.splice(insertAt, 0, entryLine);
|
|
125
|
+
const content = lines.join('\n').replace(/\n+$/, '\n');
|
|
126
|
+
return { added: true, content };
|
|
127
|
+
}
|
|
128
|
+
async function readPackageJsonWorkspaces(configPath) {
|
|
129
|
+
const raw = await readFile(configPath, 'utf8');
|
|
130
|
+
const pkg = JSON.parse(raw);
|
|
131
|
+
const workspaces = pkg.workspaces;
|
|
132
|
+
if (typeof workspaces === 'string')
|
|
133
|
+
return [workspaces];
|
|
134
|
+
if (Array.isArray(workspaces))
|
|
135
|
+
return workspaces.map(normalizeWorkspaceGlob).filter(Boolean);
|
|
136
|
+
if (workspaces && typeof workspaces === 'object' && 'packages' in workspaces) {
|
|
137
|
+
const packages = workspaces.packages;
|
|
138
|
+
if (typeof packages === 'string')
|
|
139
|
+
return [packages];
|
|
140
|
+
if (Array.isArray(packages))
|
|
141
|
+
return packages.map(normalizeWorkspaceGlob).filter(Boolean);
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
async function readPnpmWorkspace(configPath) {
|
|
146
|
+
const raw = await readFile(configPath, 'utf8');
|
|
147
|
+
return parsePnpmWorkspacePackages(raw).map(normalizeWorkspaceGlob).filter(Boolean);
|
|
148
|
+
}
|
|
149
|
+
export async function findWorkspaceRoot(startDir) {
|
|
150
|
+
let current = path.resolve(startDir);
|
|
151
|
+
// eslint-disable-next-line no-constant-condition
|
|
152
|
+
while (true) {
|
|
153
|
+
const pnpmPath = path.join(current, 'pnpm-workspace.yaml');
|
|
154
|
+
if (await pathExists(pnpmPath)) {
|
|
155
|
+
const patterns = await readPnpmWorkspace(pnpmPath);
|
|
156
|
+
return { rootDir: current, configPath: pnpmPath, type: 'pnpm-workspace', patterns };
|
|
157
|
+
}
|
|
158
|
+
const pkgPath = path.join(current, 'package.json');
|
|
159
|
+
if (await pathExists(pkgPath)) {
|
|
160
|
+
const patterns = await readPackageJsonWorkspaces(pkgPath);
|
|
161
|
+
if (patterns) {
|
|
162
|
+
return { rootDir: current, configPath: pkgPath, type: 'package-json', patterns };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const parent = path.dirname(current);
|
|
166
|
+
if (parent === current)
|
|
167
|
+
break;
|
|
168
|
+
current = parent;
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
async function updatePnpmWorkspaceFile(configPath, pattern) {
|
|
173
|
+
const raw = await readFile(configPath, 'utf8');
|
|
174
|
+
const { added, content } = insertIntoPnpmWorkspace(raw, pattern);
|
|
175
|
+
if (!added)
|
|
176
|
+
return false;
|
|
177
|
+
await writeFile(configPath, content, 'utf8');
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
async function updatePackageJsonWorkspaces(configPath, pattern) {
|
|
181
|
+
const raw = await readFile(configPath, 'utf8');
|
|
182
|
+
const pkg = JSON.parse(raw);
|
|
183
|
+
const normalizedPattern = normalizeWorkspaceGlob(pattern);
|
|
184
|
+
if (!normalizedPattern)
|
|
185
|
+
return false;
|
|
186
|
+
if (typeof pkg.workspaces === 'string') {
|
|
187
|
+
if (globCoversCandidate(pkg.workspaces, normalizedPattern))
|
|
188
|
+
return false;
|
|
189
|
+
pkg.workspaces = [pkg.workspaces, normalizedPattern];
|
|
190
|
+
}
|
|
191
|
+
else if (Array.isArray(pkg.workspaces)) {
|
|
192
|
+
if (patternsContain(pkg.workspaces, normalizedPattern))
|
|
193
|
+
return false;
|
|
194
|
+
pkg.workspaces.push(normalizedPattern);
|
|
195
|
+
}
|
|
196
|
+
else if (pkg.workspaces && typeof pkg.workspaces === 'object') {
|
|
197
|
+
const packages = pkg.workspaces.packages;
|
|
198
|
+
if (Array.isArray(packages)) {
|
|
199
|
+
if (patternsContain(packages, normalizedPattern))
|
|
200
|
+
return false;
|
|
201
|
+
packages.push(normalizedPattern);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
pkg.workspaces.packages = [normalizedPattern];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
pkg.workspaces = [normalizedPattern];
|
|
209
|
+
}
|
|
210
|
+
const next = `${JSON.stringify(pkg, null, 2)}\n`;
|
|
211
|
+
await writeFile(configPath, next, 'utf8');
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
export async function ensureWorkspacePattern(info, pattern) {
|
|
215
|
+
const normalizedPattern = normalizeWorkspaceGlob(pattern);
|
|
216
|
+
if (!normalizedPattern)
|
|
217
|
+
return false;
|
|
218
|
+
if (patternsContain(info.patterns, normalizedPattern))
|
|
219
|
+
return false;
|
|
220
|
+
const updated = info.type === 'pnpm-workspace'
|
|
221
|
+
? await updatePnpmWorkspaceFile(info.configPath, normalizedPattern)
|
|
222
|
+
: await updatePackageJsonWorkspaces(info.configPath, normalizedPattern);
|
|
223
|
+
if (updated) {
|
|
224
|
+
info.patterns.push(normalizedPattern);
|
|
225
|
+
const rel = path.relative(info.rootDir, info.configPath) || path.basename(info.configPath);
|
|
226
|
+
console.log(` updated ${rel} (added workspace path ${normalizedPattern})`);
|
|
227
|
+
}
|
|
228
|
+
return updated;
|
|
229
|
+
}
|
|
230
|
+
export function workspacePatternForPackagesRoot(workspaceRootDir, packagesRoot) {
|
|
231
|
+
const relative = toPosixPath(path.relative(workspaceRootDir, path.join(packagesRoot, '*')));
|
|
232
|
+
const normalized = relative || './packages/*';
|
|
233
|
+
return normalized.startsWith('./') ? normalized.slice(2) : normalized;
|
|
234
|
+
}
|
|
235
|
+
export function workspacePatternForStackRoot(workspaceRootDir, stackRoot) {
|
|
236
|
+
const relative = toPosixPath(path.relative(workspaceRootDir, stackRoot));
|
|
237
|
+
const normalized = relative || '';
|
|
238
|
+
if (!normalized || normalized === '.')
|
|
239
|
+
return undefined;
|
|
240
|
+
return normalized.startsWith('./') ? normalized.slice(2) : normalized;
|
|
241
|
+
}
|
package/dist/llm-pack.js
ADDED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { colors } from '../../utils/log.js';
|
|
2
2
|
import { askLine } from '../../prompts.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { stdin as input } from 'node:process';
|
|
3
|
+
import { promptInteractiveSettings } from '../../cli/interactive-settings.js';
|
|
4
|
+
import { SETTING_DESCRIPTORS, formatValue, parseInteractiveValue, validateRobotSettings, parseKindsInput, } from './settings.js';
|
|
6
5
|
const READY_PROMPT = colors.dim('Settings retained. Use the keys above to adjust and confirm again.');
|
|
7
6
|
export async function promptRobotSettings(defaults) {
|
|
8
|
-
const supportsInteractive = typeof
|
|
7
|
+
const supportsInteractive = typeof process.stdin.setRawMode === 'function' && process.stdin.isTTY;
|
|
9
8
|
let currentSettings = defaults;
|
|
10
9
|
while (true) {
|
|
11
10
|
const chosen = supportsInteractive
|
|
@@ -19,7 +18,7 @@ export async function promptRobotSettings(defaults) {
|
|
|
19
18
|
}
|
|
20
19
|
}
|
|
21
20
|
async function confirmExecution() {
|
|
22
|
-
const question = colors.cyan('Run the robot
|
|
21
|
+
const question = colors.cyan('Run the robot with these settings? (Y/n): ');
|
|
23
22
|
while (true) {
|
|
24
23
|
const answer = (await askLine(question)).trim().toLowerCase();
|
|
25
24
|
if (!answer || answer === 'y' || answer === 'yes')
|
|
@@ -31,17 +30,60 @@ async function confirmExecution() {
|
|
|
31
30
|
}
|
|
32
31
|
async function promptRobotSettingsSequential(defaults) {
|
|
33
32
|
console.log(colors.dim('Enter values to override defaults or press Enter to keep the current setting.'));
|
|
33
|
+
const includeGlobs = await promptList('Include globs', defaults.includeGlobs);
|
|
34
|
+
const excludeGlobs = await promptList('Exclude globs', defaults.excludeGlobs);
|
|
35
|
+
const entrypoints = await promptList('Entrypoints', defaults.entrypoints);
|
|
36
|
+
const exportMode = await promptChoice('Export mode', defaults.exportMode, ['entrypoints', 'all-files']);
|
|
37
|
+
const visibility = await promptChoice('Visibility', defaults.visibility, ['exported-only', 'exported+reexported', 'all']);
|
|
38
|
+
const closure = await promptChoice('Type closure', defaults.closure, ['surface-only', 'surface+deps']);
|
|
39
|
+
const includeKinds = await promptKinds(defaults.includeKinds);
|
|
40
|
+
const maxExemplars = await promptPositiveNumber('Maximum exemplars', defaults.maxExemplars);
|
|
41
|
+
const tokenBudget = await promptTokenBudget('Token budget (blank = unlimited)', defaults.tokenBudget);
|
|
42
|
+
const preferTypeSurface = await promptBoolean('Prefer type surface in dependencies', defaults.preferTypeSurface);
|
|
34
43
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
...defaults,
|
|
45
|
+
includeGlobs,
|
|
46
|
+
excludeGlobs,
|
|
47
|
+
entrypoints,
|
|
48
|
+
exportMode,
|
|
49
|
+
visibility,
|
|
50
|
+
closure,
|
|
51
|
+
includeKinds,
|
|
52
|
+
maxExemplars,
|
|
53
|
+
tokenBudget,
|
|
54
|
+
preferTypeSurface,
|
|
38
55
|
};
|
|
39
56
|
}
|
|
57
|
+
async function promptList(label, fallback) {
|
|
58
|
+
const question = colors.cyan(`${label} [default ${fallback.join(', ')}]: `);
|
|
59
|
+
while (true) {
|
|
60
|
+
const answer = await askLine(question);
|
|
61
|
+
if (!answer)
|
|
62
|
+
return fallback;
|
|
63
|
+
const parsed = answer
|
|
64
|
+
.split(',')
|
|
65
|
+
.map((chunk) => chunk.trim())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
if (parsed.length)
|
|
68
|
+
return parsed;
|
|
69
|
+
console.log(colors.yellow('Provide at least one value or leave blank to keep the default.'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function promptChoice(label, fallback, options) {
|
|
73
|
+
const question = colors.cyan(`${label} [default ${fallback}]: `);
|
|
74
|
+
const lookup = new Set(options);
|
|
75
|
+
while (true) {
|
|
76
|
+
const answer = await askLine(question);
|
|
77
|
+
if (!answer)
|
|
78
|
+
return fallback;
|
|
79
|
+
const normalized = answer.trim();
|
|
80
|
+
if (lookup.has(normalized))
|
|
81
|
+
return normalized;
|
|
82
|
+
console.log(colors.yellow(`Choose one of: ${options.join(', ')}.`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
40
85
|
async function promptKinds(fallback) {
|
|
41
|
-
const
|
|
42
|
-
if (!descriptor)
|
|
43
|
-
return fallback;
|
|
44
|
-
const question = colors.cyan(`Kinds to include [default ${formatValue(fallback, descriptor)}]: `);
|
|
86
|
+
const question = colors.cyan(`Kinds to include [default ${fallback.join(', ')}]: `);
|
|
45
87
|
while (true) {
|
|
46
88
|
const answer = await askLine(question);
|
|
47
89
|
if (!answer)
|
|
@@ -55,30 +97,45 @@ async function promptKinds(fallback) {
|
|
|
55
97
|
return parsed.value;
|
|
56
98
|
}
|
|
57
99
|
}
|
|
58
|
-
async function
|
|
59
|
-
const question = colors.cyan(`${label} [default ${fallback
|
|
100
|
+
async function promptPositiveNumber(label, fallback) {
|
|
101
|
+
const question = colors.cyan(`${label} [default ${fallback}]: `);
|
|
60
102
|
while (true) {
|
|
61
|
-
const answer =
|
|
103
|
+
const answer = await askLine(question);
|
|
62
104
|
if (!answer)
|
|
63
105
|
return fallback;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
console.log(colors.yellow('Please answer "yes" or "no", or leave blank to keep the default.'));
|
|
106
|
+
const parsed = Number(answer);
|
|
107
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
108
|
+
return Math.floor(parsed);
|
|
109
|
+
console.log(colors.yellow('Provide a positive integer or leave blank to keep the default.'));
|
|
69
110
|
}
|
|
70
111
|
}
|
|
71
|
-
async function
|
|
72
|
-
const
|
|
112
|
+
async function promptTokenBudget(label, fallback) {
|
|
113
|
+
const text = fallback ? `${fallback}` : 'unlimited';
|
|
114
|
+
const question = colors.cyan(`${label} [default ${text}]: `);
|
|
73
115
|
while (true) {
|
|
74
116
|
const answer = await askLine(question);
|
|
75
117
|
if (!answer)
|
|
76
118
|
return fallback;
|
|
119
|
+
if (answer.trim() === '' || Number(answer) === 0) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
77
122
|
const parsed = Number(answer);
|
|
78
|
-
if (
|
|
123
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
79
124
|
return Math.floor(parsed);
|
|
80
|
-
|
|
81
|
-
|
|
125
|
+
console.log(colors.yellow('Provide a positive integer, 0 for unlimited, or leave blank for the default.'));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function promptBoolean(label, fallback) {
|
|
129
|
+
const question = colors.cyan(`${label} [default ${fallback ? 'yes' : 'no'}]: `);
|
|
130
|
+
while (true) {
|
|
131
|
+
const answer = (await askLine(question)).trim().toLowerCase();
|
|
132
|
+
if (!answer)
|
|
133
|
+
return fallback;
|
|
134
|
+
if (['yes', 'y', 'true', '1'].includes(answer))
|
|
135
|
+
return true;
|
|
136
|
+
if (['no', 'n', 'false', '0'].includes(answer))
|
|
137
|
+
return false;
|
|
138
|
+
console.log(colors.yellow('Please answer "yes" or "no", or leave blank to keep the default.'));
|
|
82
139
|
}
|
|
83
140
|
}
|
|
84
141
|
async function promptRobotSettingsInteractive(defaults) {
|
|
@@ -90,11 +147,11 @@ async function promptRobotSettingsInteractive(defaults) {
|
|
|
90
147
|
parse: (buffer) => parseInteractiveValue(descriptor, buffer),
|
|
91
148
|
}));
|
|
92
149
|
return promptInteractiveSettings({
|
|
93
|
-
title: 'Robot settings (type to edit values)',
|
|
150
|
+
title: 'Robot pack settings (type to edit values)',
|
|
94
151
|
descriptors,
|
|
95
152
|
initial: defaults,
|
|
96
153
|
instructions: [
|
|
97
|
-
'Use ↑/↓ to change rows, type
|
|
154
|
+
'Use ↑/↓ to change rows, type to replace the highlighted value, Backspace to clear characters, and Enter to validate and confirm the selection.',
|
|
98
155
|
'Press Esc/Ctrl+C to abort, or hit Enter again after reviewing the summary to continue.',
|
|
99
156
|
],
|
|
100
157
|
validate: validateRobotSettings,
|
|
@@ -1,22 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const KNOWN_KINDS = new Set(ROBOT_KINDS);
|
|
1
|
+
import { ROBOT_PACK_SYMBOL_KINDS, } from '../pack/types.js';
|
|
3
2
|
export const SETTING_DESCRIPTORS = [
|
|
3
|
+
{
|
|
4
|
+
key: 'includeGlobs',
|
|
5
|
+
label: 'Include globs',
|
|
6
|
+
type: 'list',
|
|
7
|
+
unit: 'comma-separated patterns',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
key: 'excludeGlobs',
|
|
11
|
+
label: 'Exclude globs',
|
|
12
|
+
type: 'list',
|
|
13
|
+
unit: 'comma-separated patterns',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: 'entrypoints',
|
|
17
|
+
label: 'Entrypoints',
|
|
18
|
+
type: 'list',
|
|
19
|
+
unit: 'comma-separated patterns',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: 'exportMode',
|
|
23
|
+
label: 'Export mode',
|
|
24
|
+
type: 'choice',
|
|
25
|
+
options: ['entrypoints', 'all-files'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: 'visibility',
|
|
29
|
+
label: 'Visibility',
|
|
30
|
+
type: 'choice',
|
|
31
|
+
options: ['exported-only', 'exported+reexported', 'all'],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: 'closure',
|
|
35
|
+
label: 'Type closure',
|
|
36
|
+
type: 'choice',
|
|
37
|
+
options: ['surface-only', 'surface+deps'],
|
|
38
|
+
},
|
|
4
39
|
{
|
|
5
40
|
key: 'includeKinds',
|
|
6
41
|
label: 'Kinds to include',
|
|
7
|
-
unit: 'comma-separated (function, component, type, const, class)',
|
|
8
42
|
type: 'list',
|
|
43
|
+
unit: 'comma-separated',
|
|
9
44
|
},
|
|
10
45
|
{
|
|
11
|
-
key: '
|
|
12
|
-
label: '
|
|
13
|
-
type: '
|
|
46
|
+
key: 'maxExemplars',
|
|
47
|
+
label: 'Maximum exemplars',
|
|
48
|
+
type: 'number',
|
|
14
49
|
},
|
|
15
50
|
{
|
|
16
|
-
key: '
|
|
17
|
-
label: '
|
|
18
|
-
unit: 'columns',
|
|
51
|
+
key: 'tokenBudget',
|
|
52
|
+
label: 'Token budget (blank = unlimited)',
|
|
19
53
|
type: 'number',
|
|
54
|
+
allowEmpty: true,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: 'preferTypeSurface',
|
|
58
|
+
label: 'Prefer type surface in dependencies',
|
|
59
|
+
type: 'boolean',
|
|
20
60
|
},
|
|
21
61
|
];
|
|
22
62
|
export function formatValue(value, descriptor) {
|
|
@@ -24,81 +64,99 @@ export function formatValue(value, descriptor) {
|
|
|
24
64
|
return value ? 'true' : 'false';
|
|
25
65
|
}
|
|
26
66
|
if (descriptor.type === 'list') {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
return '
|
|
30
|
-
return
|
|
67
|
+
const items = value;
|
|
68
|
+
if (!items.length)
|
|
69
|
+
return 'none';
|
|
70
|
+
return items.join(', ');
|
|
71
|
+
}
|
|
72
|
+
if (descriptor.type === 'choice') {
|
|
73
|
+
return `${value}`;
|
|
74
|
+
}
|
|
75
|
+
if (descriptor.type === 'number') {
|
|
76
|
+
if (descriptor.key === 'tokenBudget' && value === undefined) {
|
|
77
|
+
return 'unlimited';
|
|
78
|
+
}
|
|
79
|
+
return `${value}`;
|
|
31
80
|
}
|
|
32
81
|
return `${value}`;
|
|
33
82
|
}
|
|
34
83
|
export function parseInteractiveValue(descriptor, buffer) {
|
|
35
84
|
const trimmed = buffer.trim();
|
|
36
85
|
if (!trimmed) {
|
|
86
|
+
if (descriptor.allowEmpty) {
|
|
87
|
+
return { value: undefined };
|
|
88
|
+
}
|
|
37
89
|
return { value: undefined };
|
|
38
90
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
};
|
|
91
|
+
switch (descriptor.type) {
|
|
92
|
+
case 'list':
|
|
93
|
+
if (descriptor.key === 'includeKinds') {
|
|
94
|
+
return parseKindsInput(trimmed);
|
|
95
|
+
}
|
|
96
|
+
return { value: parseStringList(trimmed) };
|
|
97
|
+
case 'choice':
|
|
98
|
+
if (!descriptor.options)
|
|
99
|
+
return { value: undefined };
|
|
100
|
+
if (!descriptor.options.includes(trimmed)) {
|
|
101
|
+
return { error: `Choose one of: ${descriptor.options.join(', ')}.` };
|
|
102
|
+
}
|
|
103
|
+
return { value: trimmed };
|
|
104
|
+
case 'number':
|
|
105
|
+
if (descriptor.allowEmpty && trimmed === '') {
|
|
106
|
+
return { value: undefined };
|
|
107
|
+
}
|
|
108
|
+
const parsed = parsePositiveInteger(trimmed);
|
|
109
|
+
if (parsed === undefined) {
|
|
110
|
+
return { error: `${descriptor.label} requires a positive integer.` };
|
|
111
|
+
}
|
|
112
|
+
return { value: parsed };
|
|
113
|
+
case 'boolean': {
|
|
114
|
+
const parsedBool = parseBooleanInput(trimmed);
|
|
115
|
+
if (parsedBool.error)
|
|
116
|
+
return { error: parsedBool.error };
|
|
117
|
+
return { value: parsedBool.value };
|
|
45
118
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (descriptor.type === 'boolean') {
|
|
49
|
-
const parsed = parseBooleanInput(trimmed);
|
|
50
|
-
if (parsed.error)
|
|
51
|
-
return { error: parsed.error };
|
|
52
|
-
return { value: parsed.value };
|
|
119
|
+
default:
|
|
120
|
+
return { value: undefined };
|
|
53
121
|
}
|
|
54
|
-
if (descriptor.type === 'list') {
|
|
55
|
-
return parseKindsInput(trimmed);
|
|
56
|
-
}
|
|
57
|
-
return { value: undefined };
|
|
58
122
|
}
|
|
59
123
|
export function validateRobotSettings(settings) {
|
|
60
|
-
if (!settings.
|
|
61
|
-
return '
|
|
124
|
+
if (!settings.includeGlobs.length)
|
|
125
|
+
return 'Specify at least one include glob.';
|
|
126
|
+
if (!settings.entrypoints.length)
|
|
127
|
+
return 'Specify at least one entrypoint pattern.';
|
|
128
|
+
if (!settings.includeKinds.length)
|
|
129
|
+
return 'Choose at least one kind to include.';
|
|
130
|
+
if (!Number.isFinite(settings.maxExemplars) || settings.maxExemplars <= 0) {
|
|
131
|
+
return 'Maximum exemplars must be a positive number.';
|
|
62
132
|
}
|
|
63
|
-
if (
|
|
64
|
-
return '
|
|
133
|
+
if (settings.tokenBudget !== undefined && settings.tokenBudget <= 0) {
|
|
134
|
+
return 'Token budget must be greater than zero or blank for unlimited.';
|
|
65
135
|
}
|
|
66
136
|
return undefined;
|
|
67
137
|
}
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const min = options?.allowZero ? 0 : 1;
|
|
74
|
-
if (floored < min)
|
|
75
|
-
return undefined;
|
|
76
|
-
return floored;
|
|
77
|
-
}
|
|
78
|
-
function parseBooleanInput(raw) {
|
|
79
|
-
const normalized = raw.trim().toLowerCase();
|
|
80
|
-
if (['yes', 'y', 'true', '1'].includes(normalized))
|
|
81
|
-
return { value: true };
|
|
82
|
-
if (['no', 'n', 'false', '0'].includes(normalized))
|
|
83
|
-
return { value: false };
|
|
84
|
-
return { error: 'Provide "true" or "false".' };
|
|
138
|
+
function parseStringList(input) {
|
|
139
|
+
return input
|
|
140
|
+
.split(',')
|
|
141
|
+
.map((chunk) => chunk.trim())
|
|
142
|
+
.filter(Boolean);
|
|
85
143
|
}
|
|
86
144
|
export function parseKindsInput(raw) {
|
|
87
145
|
const normalized = raw
|
|
88
146
|
.split(',')
|
|
89
147
|
.map((chunk) => chunk.trim().toLowerCase())
|
|
90
148
|
.filter(Boolean);
|
|
91
|
-
if (normalized.length
|
|
149
|
+
if (!normalized.length) {
|
|
92
150
|
return { error: 'Enter at least one kind or "all".' };
|
|
93
151
|
}
|
|
94
152
|
if (normalized.length === 1 && normalized[0] === 'all') {
|
|
95
|
-
return { value: [...
|
|
153
|
+
return { value: [...ROBOT_PACK_SYMBOL_KINDS] };
|
|
96
154
|
}
|
|
97
155
|
const invalid = [];
|
|
98
156
|
const unique = [];
|
|
99
157
|
const seen = new Set();
|
|
100
158
|
for (const entry of normalized) {
|
|
101
|
-
if (!
|
|
159
|
+
if (!ROBOT_PACK_SYMBOL_KINDS.includes(entry)) {
|
|
102
160
|
invalid.push(entry);
|
|
103
161
|
continue;
|
|
104
162
|
}
|
|
@@ -110,11 +168,28 @@ export function parseKindsInput(raw) {
|
|
|
110
168
|
}
|
|
111
169
|
if (invalid.length > 0) {
|
|
112
170
|
return {
|
|
113
|
-
error: `Unknown kinds: ${invalid.join(', ')}. Valid kinds are ${
|
|
171
|
+
error: `Unknown kinds: ${invalid.join(', ')}. Valid kinds are ${ROBOT_PACK_SYMBOL_KINDS.join(', ')}.`,
|
|
114
172
|
};
|
|
115
173
|
}
|
|
116
|
-
if (unique.length
|
|
174
|
+
if (!unique.length) {
|
|
117
175
|
return { error: 'Provide at least one valid kind.' };
|
|
118
176
|
}
|
|
119
177
|
return { value: unique };
|
|
120
178
|
}
|
|
179
|
+
function parsePositiveInteger(input) {
|
|
180
|
+
const value = Number(input);
|
|
181
|
+
if (!Number.isFinite(value))
|
|
182
|
+
return undefined;
|
|
183
|
+
const floored = Math.floor(value);
|
|
184
|
+
if (floored <= 0)
|
|
185
|
+
return undefined;
|
|
186
|
+
return floored;
|
|
187
|
+
}
|
|
188
|
+
function parseBooleanInput(raw) {
|
|
189
|
+
const normalized = raw.trim().toLowerCase();
|
|
190
|
+
if (['yes', 'y', 'true', '1'].includes(normalized))
|
|
191
|
+
return { value: true };
|
|
192
|
+
if (['no', 'n', 'false', '0'].includes(normalized))
|
|
193
|
+
return { value: false };
|
|
194
|
+
return { error: 'Provide "true" or "false".' };
|
|
195
|
+
}
|