@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.
Files changed (54) hide show
  1. package/README.md +96 -0
  2. package/dist/create-package/cli-args.js +78 -0
  3. package/dist/create-package/prompts.js +138 -0
  4. package/dist/create-package/shared/configs.js +309 -0
  5. package/dist/create-package/shared/constants.js +5 -0
  6. package/dist/create-package/shared/fs-utils.js +69 -0
  7. package/dist/create-package/tasks.js +89 -0
  8. package/dist/create-package/types.js +1 -0
  9. package/dist/create-package/variant-info.js +67 -0
  10. package/dist/create-package/variants/client/expo-react-native/lib-files.js +168 -0
  11. package/dist/create-package/variants/client/expo-react-native/package-files.js +94 -0
  12. package/dist/create-package/variants/client/expo-react-native/scaffold.js +59 -0
  13. package/dist/create-package/variants/client/expo-react-native/ui-files.js +215 -0
  14. package/dist/create-package/variants/client/vite-react/health-page.js +251 -0
  15. package/dist/create-package/variants/client/vite-react/lib-files.js +176 -0
  16. package/dist/create-package/variants/client/vite-react/package-files.js +79 -0
  17. package/dist/create-package/variants/client/vite-react/scaffold.js +68 -0
  18. package/dist/create-package/variants/client/vite-react/ui-files.js +154 -0
  19. package/dist/create-package/variants/fullstack/files.js +129 -0
  20. package/dist/create-package/variants/fullstack/index.js +86 -0
  21. package/dist/create-package/variants/fullstack/utils.js +241 -0
  22. package/dist/llm-pack.js +2 -0
  23. package/dist/robot/cli/prompts.js +84 -27
  24. package/dist/robot/cli/settings.js +131 -56
  25. package/dist/robot/config.js +123 -50
  26. package/dist/robot/coordinator.js +10 -105
  27. package/dist/robot/extractors/classes.js +14 -13
  28. package/dist/robot/extractors/components.js +17 -10
  29. package/dist/robot/extractors/constants.js +9 -6
  30. package/dist/robot/extractors/functions.js +11 -8
  31. package/dist/robot/extractors/shared.js +6 -1
  32. package/dist/robot/extractors/types.js +5 -8
  33. package/dist/robot/llm-pack.js +1226 -0
  34. package/dist/robot/pack/builder.js +374 -0
  35. package/dist/robot/pack/cli.js +65 -0
  36. package/dist/robot/pack/exemplars.js +573 -0
  37. package/dist/robot/pack/globs.js +119 -0
  38. package/dist/robot/pack/selection.js +44 -0
  39. package/dist/robot/pack/symbols.js +309 -0
  40. package/dist/robot/pack/type-registry.js +285 -0
  41. package/dist/robot/pack/types.js +48 -0
  42. package/dist/robot/pack/utils.js +36 -0
  43. package/dist/robot/serializer.js +97 -0
  44. package/dist/robot/v2/cli.js +86 -0
  45. package/dist/robot/v2/globs.js +103 -0
  46. package/dist/robot/v2/parser/bundles.js +55 -0
  47. package/dist/robot/v2/parser/candidates.js +63 -0
  48. package/dist/robot/v2/parser/exemplars.js +114 -0
  49. package/dist/robot/v2/parser/exports.js +57 -0
  50. package/dist/robot/v2/parser/symbols.js +179 -0
  51. package/dist/robot/v2/parser.js +114 -0
  52. package/dist/robot/v2/types.js +42 -0
  53. package/dist/utils/export.js +39 -18
  54. 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
+ }
@@ -0,0 +1,2 @@
1
+ EXPORTS: TYPES: SIGNATURES: EXEMPLARS: ;
2
+ export {};
@@ -1,11 +1,10 @@
1
1
  import { colors } from '../../utils/log.js';
2
2
  import { askLine } from '../../prompts.js';
3
- import { formatValue, parseInteractiveValue, parseKindsInput, SETTING_DESCRIPTORS, validateRobotSettings, } from './settings.js';
4
- import { promptInteractiveSettings, } from '../../cli/interactive-settings.js';
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 input.setRawMode === 'function' && input.isTTY;
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 extractor with these settings? (Y/n): ');
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
- includeKinds: await promptKinds(defaults.includeKinds),
36
- exportedOnly: await promptBoolean('Only consider exported symbols', defaults.exportedOnly),
37
- maxColumns: await promptNumber('Maximum columns', defaults.maxColumns),
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 descriptor = SETTING_DESCRIPTORS.find((entry) => entry.key === 'includeKinds');
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 promptBoolean(label, fallback) {
59
- const question = colors.cyan(`${label} [default ${fallback ? 'yes' : 'no'}]: `);
100
+ async function promptPositiveNumber(label, fallback) {
101
+ const question = colors.cyan(`${label} [default ${fallback}]: `);
60
102
  while (true) {
61
- const answer = (await askLine(question)).trim().toLowerCase();
103
+ const answer = await askLine(question);
62
104
  if (!answer)
63
105
  return fallback;
64
- if (['yes', 'y', 'true', '1'].includes(answer))
65
- return true;
66
- if (['no', 'n', 'false', '0'].includes(answer))
67
- return false;
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 promptNumber(label, fallback) {
72
- const question = colors.cyan(`${label} [default ${fallback}]: `);
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 (!Number.isNaN(parsed) && parsed > 0) {
123
+ if (Number.isFinite(parsed) && parsed > 0)
79
124
  return Math.floor(parsed);
80
- }
81
- console.log(colors.yellow('Provide a positive integer or leave blank to keep the default.'));
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 comma separated values for kinds, Backspace to clear characters, and Enter to validate and confirm the selection.',
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 { ROBOT_KINDS } from '../types.js';
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: 'exportedOnly',
12
- label: 'Only exported symbols',
13
- type: 'boolean',
46
+ key: 'maxExemplars',
47
+ label: 'Maximum exemplars',
48
+ type: 'number',
14
49
  },
15
50
  {
16
- key: 'maxColumns',
17
- label: 'Maximum columns',
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 kinds = value;
28
- if (kinds.length === ROBOT_KINDS.length)
29
- return 'all';
30
- return kinds.join(', ');
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
- if (descriptor.type === 'number') {
40
- const parsed = parsePositiveInteger(trimmed, { allowZero: descriptor.allowZero });
41
- if (parsed === undefined) {
42
- return {
43
- error: `${descriptor.label} requires a ${descriptor.allowZero ? 'non-negative' : 'positive'} integer.`,
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
- return { value: parsed };
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.includeKinds.length) {
61
- return 'Select at least one kind to include.';
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 (!Number.isFinite(settings.maxColumns) || settings.maxColumns <= 0) {
64
- return 'Maximum columns must be a positive number.';
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 parsePositiveInteger(inputValue, options) {
69
- const num = Number(inputValue);
70
- if (!Number.isFinite(num))
71
- return undefined;
72
- const floored = Math.floor(num);
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 === 0) {
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: [...ROBOT_KINDS] };
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 (!KNOWN_KINDS.has(entry)) {
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 ${ROBOT_KINDS.join(', ')}.`,
171
+ error: `Unknown kinds: ${invalid.join(', ')}. Valid kinds are ${ROBOT_PACK_SYMBOL_KINDS.join(', ')}.`,
114
172
  };
115
173
  }
116
- if (unique.length === 0) {
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
+ }