cli4ai 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +39 -0
  2. package/dist/bin.d.ts +6 -0
  3. package/dist/bin.js +105 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +335 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.js +464 -0
  8. package/dist/commands/browse.d.ts +4 -0
  9. package/dist/commands/browse.js +382 -0
  10. package/dist/commands/config.d.ts +10 -0
  11. package/dist/commands/config.js +121 -0
  12. package/dist/commands/info.d.ts +9 -0
  13. package/dist/commands/info.js +125 -0
  14. package/dist/commands/init.d.ts +10 -0
  15. package/dist/commands/init.js +458 -0
  16. package/dist/commands/list.d.ts +10 -0
  17. package/dist/commands/list.js +76 -0
  18. package/dist/commands/mcp-config.d.ts +10 -0
  19. package/dist/commands/mcp-config.js +49 -0
  20. package/dist/commands/remotes.d.ts +22 -0
  21. package/dist/commands/remotes.js +196 -0
  22. package/dist/commands/remove.d.ts +8 -0
  23. package/dist/commands/remove.js +61 -0
  24. package/dist/commands/routines.d.ts +29 -0
  25. package/dist/commands/routines.js +363 -0
  26. package/dist/commands/run.d.ts +12 -0
  27. package/dist/commands/run.js +104 -0
  28. package/dist/commands/scheduler.d.ts +27 -0
  29. package/dist/commands/scheduler.js +350 -0
  30. package/dist/commands/search.d.ts +9 -0
  31. package/dist/commands/search.js +162 -0
  32. package/dist/commands/secrets.d.ts +28 -0
  33. package/dist/commands/secrets.js +236 -0
  34. package/dist/commands/serve.d.ts +13 -0
  35. package/dist/commands/serve.js +49 -0
  36. package/dist/commands/start.d.ts +8 -0
  37. package/dist/commands/start.js +27 -0
  38. package/dist/commands/update.d.ts +17 -0
  39. package/dist/commands/update.js +210 -0
  40. package/dist/core/config.d.ts +91 -0
  41. package/dist/core/config.js +738 -0
  42. package/dist/core/execute.d.ts +51 -0
  43. package/dist/core/execute.js +475 -0
  44. package/dist/core/link.d.ts +39 -0
  45. package/dist/core/link.js +214 -0
  46. package/dist/core/lockfile.d.ts +63 -0
  47. package/dist/core/lockfile.js +140 -0
  48. package/dist/core/manifest.d.ts +96 -0
  49. package/dist/core/manifest.js +224 -0
  50. package/dist/core/registry.d.ts +74 -0
  51. package/dist/core/registry.js +116 -0
  52. package/dist/core/remote-client.d.ts +98 -0
  53. package/dist/core/remote-client.js +252 -0
  54. package/dist/core/remotes.d.ts +88 -0
  55. package/dist/core/remotes.js +206 -0
  56. package/dist/core/routine-engine.d.ts +124 -0
  57. package/dist/core/routine-engine.js +699 -0
  58. package/dist/core/routines.d.ts +36 -0
  59. package/dist/core/routines.js +132 -0
  60. package/dist/core/scheduler-daemon.d.ts +10 -0
  61. package/dist/core/scheduler-daemon.js +77 -0
  62. package/dist/core/scheduler.d.ts +131 -0
  63. package/dist/core/scheduler.js +492 -0
  64. package/dist/core/secrets.d.ts +48 -0
  65. package/dist/core/secrets.js +384 -0
  66. package/dist/lib/cli.d.ts +84 -0
  67. package/dist/lib/cli.js +216 -0
  68. package/dist/mcp/adapter.d.ts +35 -0
  69. package/dist/mcp/adapter.js +94 -0
  70. package/dist/mcp/config-gen.d.ts +31 -0
  71. package/dist/mcp/config-gen.js +75 -0
  72. package/dist/mcp/server.d.ts +41 -0
  73. package/dist/mcp/server.js +296 -0
  74. package/dist/server/service.d.ts +85 -0
  75. package/dist/server/service.js +304 -0
  76. package/package.json +6 -3
  77. package/src/bin.ts +0 -118
  78. package/src/cli.ts +0 -412
  79. package/src/commands/add.ts +0 -562
  80. package/src/commands/browse.ts +0 -449
  81. package/src/commands/config.ts +0 -154
  82. package/src/commands/info.ts +0 -133
  83. package/src/commands/init.ts +0 -514
  84. package/src/commands/list.ts +0 -95
  85. package/src/commands/mcp-config.ts +0 -69
  86. package/src/commands/remotes.ts +0 -253
  87. package/src/commands/remove.ts +0 -78
  88. package/src/commands/routines.ts +0 -427
  89. package/src/commands/run.ts +0 -127
  90. package/src/commands/scheduler.ts +0 -438
  91. package/src/commands/search.ts +0 -185
  92. package/src/commands/secrets.ts +0 -292
  93. package/src/commands/serve.ts +0 -66
  94. package/src/commands/start.ts +0 -40
  95. package/src/commands/update.ts +0 -252
  96. package/src/core/config.ts +0 -845
  97. package/src/core/execute.ts +0 -569
  98. package/src/core/link.ts +0 -246
  99. package/src/core/lockfile.ts +0 -187
  100. package/src/core/manifest.ts +0 -327
  101. package/src/core/registry.ts +0 -165
  102. package/src/core/remote-client.ts +0 -419
  103. package/src/core/remotes.ts +0 -268
  104. package/src/core/routine-engine.ts +0 -895
  105. package/src/core/routines.ts +0 -171
  106. package/src/core/scheduler-daemon.ts +0 -94
  107. package/src/core/scheduler.ts +0 -606
  108. package/src/core/secrets.ts +0 -430
  109. package/src/lib/cli.ts +0 -261
  110. package/src/mcp/adapter.ts +0 -131
  111. package/src/mcp/config-gen.ts +0 -106
  112. package/src/mcp/server.ts +0 -365
  113. package/src/server/service.ts +0 -434
@@ -1,449 +0,0 @@
1
- /**
2
- * cli4ai browse - Interactive package browser
3
- */
4
-
5
- import { spawnSync } from 'child_process';
6
- import { log, outputError } from '../lib/cli.js';
7
- import { getNpmGlobalPackages, getGlobalPackages, getLocalPackages } from '../core/config.js';
8
-
9
- interface NpmPackage {
10
- name: string;
11
- version: string;
12
- description: string;
13
- keywords: string[];
14
- }
15
-
16
- // ANSI codes
17
- const RESET = '\x1B[0m';
18
- const BOLD = '\x1B[1m';
19
- const DIM = '\x1B[2m';
20
- const ITALIC = '\x1B[3m';
21
- const CYAN = '\x1B[36m';
22
- const GREEN = '\x1B[32m';
23
- const YELLOW = '\x1B[33m';
24
- const WHITE = '\x1B[37m';
25
- const MAGENTA = '\x1B[35m';
26
- const BLUE = '\x1B[34m';
27
- const BG_CYAN = '\x1B[46m';
28
- const BG_BLUE = '\x1B[44m';
29
-
30
- // Box drawing
31
- const BOX = {
32
- TL: '╭', TR: '╮', BL: '╰', BR: '╯',
33
- H: '─', V: '│',
34
- LT: '├', RT: '┤', TT: '┬', BT: '┴', X: '┼'
35
- };
36
-
37
- const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
38
-
39
- /**
40
- * Fetch @cli4ai packages from npm registry API (fast!)
41
- */
42
- async function fetchPackages(): Promise<NpmPackage[]> {
43
- try {
44
- const response = await fetch('https://registry.npmjs.org/-/v1/search?text=@cli4ai&size=100', {
45
- headers: { 'Accept': 'application/json' }
46
- });
47
-
48
- if (!response.ok) {
49
- throw new Error(`HTTP ${response.status}`);
50
- }
51
-
52
- const data = await response.json() as {
53
- objects: Array<{
54
- package: {
55
- name: string;
56
- version: string;
57
- description: string;
58
- keywords: string[];
59
- };
60
- }>;
61
- };
62
-
63
- // Filter to only @cli4ai packages (exclude @cli4ai/lib)
64
- return data.objects
65
- .map(o => o.package)
66
- .filter(p => p.name.startsWith('@cli4ai/') && p.name !== '@cli4ai/lib')
67
- .map(p => ({
68
- name: p.name,
69
- version: p.version,
70
- description: p.description || '',
71
- keywords: p.keywords || []
72
- }));
73
- } catch (err) {
74
- outputError('NETWORK_ERROR', 'Failed to fetch packages from npm', {
75
- hint: 'Check your internet connection'
76
- });
77
- return [];
78
- }
79
- }
80
-
81
- /**
82
- * Get set of all installed package names (from all sources)
83
- */
84
- function getInstalledPackageNames(): Set<string> {
85
- const installed = new Set<string>();
86
-
87
- // Check npm global @cli4ai packages
88
- for (const pkg of getNpmGlobalPackages()) {
89
- installed.add(pkg.name);
90
- }
91
-
92
- // Check cli4ai global packages
93
- for (const pkg of getGlobalPackages()) {
94
- installed.add(pkg.name);
95
- }
96
-
97
- // Check local packages
98
- for (const pkg of getLocalPackages(process.cwd())) {
99
- installed.add(pkg.name);
100
- }
101
-
102
- return installed;
103
- }
104
-
105
- /**
106
- * Pad string to width
107
- */
108
- function pad(str: string, width: number): string {
109
- if (str.length >= width) return str.slice(0, width);
110
- return str + ' '.repeat(width - str.length);
111
- }
112
-
113
- /**
114
- * Center string in width
115
- */
116
- function center(str: string, width: number): string {
117
- const stripped = str.replace(/\x1B\[[0-9;]*m/g, '');
118
- const padding = Math.max(0, width - stripped.length);
119
- const left = Math.floor(padding / 2);
120
- const right = padding - left;
121
- return ' '.repeat(left) + str + ' '.repeat(right);
122
- }
123
-
124
- interface BrowseItem {
125
- name: string;
126
- version: string;
127
- description: string;
128
- keywords: string[];
129
- installed: boolean;
130
- }
131
-
132
- /**
133
- * Loading animation
134
- */
135
- async function showLoadingAnimation(): Promise<void> {
136
- const frames = ['[•_•]', '[•_•]', '[°_°]', '[•_•]'];
137
- const text = 'Fetching packages';
138
-
139
- for (let i = 0; i < 8; i++) {
140
- const dots = '.'.repeat((i % 3) + 1).padEnd(3);
141
- process.stderr.write(`\r ${CYAN}${frames[i % frames.length]}${RESET} ${DIM}${text}${dots}${RESET}`);
142
- await sleep(150);
143
- }
144
- process.stderr.write('\r\x1B[K');
145
- }
146
-
147
- /**
148
- * Interactive multi-select UI with table layout
149
- */
150
- async function multiSelect(items: BrowseItem[]): Promise<string[]> {
151
- if (!process.stdin.isTTY) {
152
- outputError('INVALID_INPUT', 'Interactive mode requires a TTY', {
153
- hint: 'Use "cli4ai add <package>" for non-interactive installation'
154
- });
155
- }
156
-
157
- const selected = new Set<number>();
158
- let cursor = 0;
159
-
160
- const WIDTH = 72;
161
-
162
- const render = () => {
163
- // Clear screen and move to top
164
- process.stderr.write('\x1B[2J\x1B[H');
165
-
166
- // Robot header
167
- log('');
168
- log(` ${CYAN}${BOLD}[•_•]${RESET} ${BOLD}cli4ai${RESET} ${DIM}─${RESET} ${WHITE}Package Browser${RESET}`);
169
- log(` ${DIM}cli4ai.com${RESET}`);
170
- log('');
171
-
172
- // Box top
173
- log(` ${CYAN}${BOX.TL}${BOX.H.repeat(WIDTH)}${BOX.TR}${RESET}`);
174
-
175
- // Controls bar
176
- const controls = `${DIM}↑↓${RESET} move ${DIM}space${RESET} select ${DIM}a${RESET} all ${DIM}enter${RESET} install ${DIM}q${RESET} quit`;
177
- log(` ${CYAN}${BOX.V}${RESET} ${controls}${' '.repeat(WIDTH - 55)}${CYAN}${BOX.V}${RESET}`);
178
-
179
- // Separator
180
- log(` ${CYAN}${BOX.LT}${BOX.H.repeat(WIDTH)}${BOX.RT}${RESET}`);
181
-
182
- // Package list
183
- items.forEach((item, i) => {
184
- const isCursor = i === cursor;
185
- const isSelected = selected.has(i);
186
- const name = item.name.replace('@cli4ai/', '');
187
-
188
- // Checkbox
189
- let checkbox: string;
190
- if (isSelected) {
191
- checkbox = `${GREEN}◉${RESET}`;
192
- } else {
193
- checkbox = `${DIM}○${RESET}`;
194
- }
195
-
196
- // Status badge
197
- let badge = '';
198
- if (item.installed) {
199
- badge = ` ${GREEN}●${RESET}`;
200
- }
201
-
202
- // Name with cursor indicator
203
- let line: string;
204
- if (isCursor) {
205
- line = ` ${CYAN}▸${RESET} ${checkbox} ${BOLD}${CYAN}${pad(name, 14)}${RESET} ${DIM}v${item.version}${RESET}${badge}`;
206
- } else {
207
- line = ` ${checkbox} ${WHITE}${pad(name, 14)}${RESET} ${DIM}v${item.version}${RESET}${badge}`;
208
- }
209
-
210
- // Pad to fit box
211
- const stripped = line.replace(/\x1B\[[0-9;]*m/g, '');
212
- const padding = WIDTH - stripped.length + 2;
213
- log(` ${CYAN}${BOX.V}${RESET}${line}${' '.repeat(Math.max(0, padding))}${CYAN}${BOX.V}${RESET}`);
214
-
215
- // Description for selected item
216
- if (isCursor && item.description) {
217
- const desc = item.description.slice(0, WIDTH - 8);
218
- log(` ${CYAN}${BOX.V}${RESET} ${DIM}${desc}${RESET}${' '.repeat(Math.max(0, WIDTH - desc.length - 6))}${CYAN}${BOX.V}${RESET}`);
219
-
220
- // Keywords
221
- if (item.keywords.length > 0) {
222
- const tags = item.keywords.slice(0, 4).map(k => `${MAGENTA}#${k}${RESET}`).join(' ');
223
- const tagsStripped = tags.replace(/\x1B\[[0-9;]*m/g, '');
224
- log(` ${CYAN}${BOX.V}${RESET} ${tags}${' '.repeat(Math.max(0, WIDTH - tagsStripped.length - 6))}${CYAN}${BOX.V}${RESET}`);
225
- }
226
-
227
- log(` ${CYAN}${BOX.V}${RESET}${' '.repeat(WIDTH)}${CYAN}${BOX.V}${RESET}`);
228
- }
229
- });
230
-
231
- // Box bottom
232
- log(` ${CYAN}${BOX.BL}${BOX.H.repeat(WIDTH)}${BOX.BR}${RESET}`);
233
-
234
- // Footer
235
- log('');
236
- const selectedCount = selected.size;
237
- const installedCount = items.filter(i => i.installed).length;
238
-
239
- if (selectedCount > 0) {
240
- log(` ${GREEN}◉${RESET} ${BOLD}${selectedCount}${RESET} selected for installation`);
241
- }
242
-
243
- log(` ${GREEN}●${RESET} ${DIM}${installedCount} already installed${RESET}`);
244
- log('');
245
- };
246
-
247
- return new Promise((resolve) => {
248
- process.stdin.setRawMode(true);
249
- process.stdin.resume();
250
- process.stdin.setEncoding('utf8');
251
-
252
- render();
253
-
254
- const onKeypress = (key: string) => {
255
- // Handle arrow keys (escape sequences)
256
- if (key === '\x1B[A' || key === 'k') {
257
- // Up arrow or k
258
- cursor = Math.max(0, cursor - 1);
259
- render();
260
- } else if (key === '\x1B[B' || key === 'j') {
261
- // Down arrow or j
262
- cursor = Math.min(items.length - 1, cursor + 1);
263
- render();
264
- } else if (key === ' ') {
265
- // Space - toggle selection
266
- if (selected.has(cursor)) {
267
- selected.delete(cursor);
268
- } else {
269
- selected.add(cursor);
270
- }
271
- render();
272
- } else if (key === '\r' || key === '\n') {
273
- // Enter - confirm
274
- process.stdin.setRawMode(false);
275
- process.stdin.removeListener('data', onKeypress);
276
- process.stdin.pause();
277
- process.stderr.write('\x1B[2J\x1B[H'); // Clear screen
278
-
279
- const selectedPackages = Array.from(selected).map(i => items[i].name);
280
- resolve(selectedPackages);
281
- } else if (key === 'q' || key === '\x1B' || key === '\x03') {
282
- // q, Escape, or Ctrl+C - quit
283
- process.stdin.setRawMode(false);
284
- process.stdin.removeListener('data', onKeypress);
285
- process.stdin.pause();
286
- process.stderr.write('\x1B[2J\x1B[H'); // Clear screen
287
- resolve([]);
288
- } else if (key === 'a') {
289
- // Select all
290
- items.forEach((_, i) => selected.add(i));
291
- render();
292
- } else if (key === 'n') {
293
- // Select none
294
- selected.clear();
295
- render();
296
- }
297
- };
298
-
299
- process.stdin.on('data', onKeypress);
300
- });
301
- }
302
-
303
- /**
304
- * Ask user for install scope (global or local)
305
- */
306
- async function askInstallScope(): Promise<'global' | 'local' | null> {
307
- return new Promise((resolve) => {
308
- process.stdin.setRawMode(true);
309
- process.stdin.resume();
310
- process.stdin.setEncoding('utf8');
311
-
312
- let cursor = 0; // 0 = global, 1 = local
313
- const options = [
314
- { key: 'global', label: 'Global', desc: 'Available everywhere (~/.cli4ai/)' },
315
- { key: 'local', label: 'Project', desc: 'Only in this directory (./.cli4ai/)' }
316
- ];
317
-
318
- const render = () => {
319
- process.stderr.write('\x1B[2J\x1B[H'); // Clear screen
320
-
321
- log('');
322
- log(` ${CYAN}${BOLD}[•_•]${RESET} ${BOLD}Where to install?${RESET}`);
323
- log('');
324
-
325
- options.forEach((opt, i) => {
326
- const isSelected = i === cursor;
327
- const bullet = isSelected ? `${CYAN}▸${RESET}` : ' ';
328
- const label = isSelected ? `${BOLD}${WHITE}${opt.label}${RESET}` : `${DIM}${opt.label}${RESET}`;
329
- const desc = isSelected ? `${opt.desc}` : `${DIM}${opt.desc}${RESET}`;
330
- log(` ${bullet} ${label} ${DIM}─${RESET} ${desc}`);
331
- });
332
-
333
- log('');
334
- log(` ${DIM}↑↓ select enter confirm q cancel${RESET}`);
335
- log('');
336
- };
337
-
338
- render();
339
-
340
- const onKeypress = (key: string) => {
341
- if (key === '\x1B[A' || key === 'k') {
342
- cursor = Math.max(0, cursor - 1);
343
- render();
344
- } else if (key === '\x1B[B' || key === 'j') {
345
- cursor = Math.min(options.length - 1, cursor + 1);
346
- render();
347
- } else if (key === '\r' || key === '\n') {
348
- process.stdin.setRawMode(false);
349
- process.stdin.removeListener('data', onKeypress);
350
- process.stdin.pause();
351
- resolve(cursor === 0 ? 'global' : 'local');
352
- } else if (key === 'q' || key === '\x1B' || key === '\x03') {
353
- process.stdin.setRawMode(false);
354
- process.stdin.removeListener('data', onKeypress);
355
- process.stdin.pause();
356
- resolve(null);
357
- }
358
- };
359
-
360
- process.stdin.on('data', onKeypress);
361
- });
362
- }
363
-
364
- /**
365
- * Install packages with animation
366
- */
367
- async function installPackages(packages: string[], scope: 'global' | 'local'): Promise<void> {
368
- if (packages.length === 0) {
369
- log(` ${DIM}[•_•] No packages selected. Bye!${RESET}\n`);
370
- return;
371
- }
372
-
373
- const scopeFlag = scope === 'global' ? '-g' : '';
374
- const scopeLabel = scope === 'global' ? 'globally' : 'locally';
375
-
376
- log('');
377
- log(` ${CYAN}[•_•]${RESET} ${BOLD}Installing ${packages.length} package${packages.length !== 1 ? 's' : ''} ${scopeLabel}...${RESET}`);
378
- log('');
379
-
380
- for (const pkg of packages) {
381
- const shortName = pkg.replace('@cli4ai/', '');
382
-
383
- // Animated install
384
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
385
- let frameIndex = 0;
386
- const spinner = setInterval(() => {
387
- process.stderr.write(`\r ${CYAN}${frames[frameIndex]}${RESET} Installing ${BOLD}${shortName}${RESET}...`);
388
- frameIndex = (frameIndex + 1) % frames.length;
389
- }, 80);
390
-
391
- try {
392
- // Use cli4ai add command for proper integration
393
- // Use spawnSync with argument array to prevent command injection
394
- const addArgs = ['add', pkg];
395
- if (scopeFlag) addArgs.push(scopeFlag);
396
- addArgs.push('-y');
397
- const result = spawnSync('cli4ai', addArgs, { stdio: 'pipe' });
398
- clearInterval(spinner);
399
- if (result.status === 0) {
400
- process.stderr.write(`\r ${GREEN}✓${RESET} ${BOLD}${shortName}${RESET} installed \n`);
401
- } else {
402
- process.stderr.write(`\r ${YELLOW}✗${RESET} ${shortName} failed \n`);
403
- }
404
- } catch (err) {
405
- clearInterval(spinner);
406
- process.stderr.write(`\r ${YELLOW}✗${RESET} ${shortName} failed \n`);
407
- }
408
- }
409
-
410
- log('');
411
- log(` ${GREEN}[°_°]${RESET} ${BOLD}Done!${RESET}`);
412
- log(` ${DIM}Run with:${RESET} cli4ai run ${CYAN}<package>${RESET} ${CYAN}<command>${RESET}`);
413
- log('');
414
- }
415
-
416
- export async function browseCommand(): Promise<void> {
417
- log('');
418
- await showLoadingAnimation();
419
-
420
- const packages = await fetchPackages();
421
-
422
- if (packages.length === 0) {
423
- log(` ${YELLOW}[•_•]${RESET} ${DIM}No packages found on npm.${RESET}\n`);
424
- return;
425
- }
426
-
427
- // Check installation status
428
- const installedNames = getInstalledPackageNames();
429
- const items: BrowseItem[] = packages.map(pkg => ({
430
- name: pkg.name,
431
- version: pkg.version,
432
- description: pkg.description,
433
- keywords: pkg.keywords,
434
- installed: installedNames.has(pkg.name.replace('@cli4ai/', ''))
435
- }));
436
-
437
- // Show interactive selector
438
- const selected = await multiSelect(items);
439
-
440
- // Ask for scope and install selected packages
441
- if (selected.length > 0) {
442
- const scope = await askInstallScope();
443
- if (scope) {
444
- await installPackages(selected, scope);
445
- } else {
446
- log(` ${DIM}[•_•] Installation cancelled. Bye!${RESET}\n`);
447
- }
448
- }
449
- }
@@ -1,154 +0,0 @@
1
- /**
2
- * cli4ai config - Configuration management
3
- */
4
-
5
- import { output, outputError, log } from '../lib/cli.js';
6
- import {
7
- loadConfig,
8
- updateConfig,
9
- addLocalRegistry,
10
- removeLocalRegistry,
11
- CLI4AI_HOME,
12
- CONFIG_FILE,
13
- type Config
14
- } from '../core/config.js';
15
-
16
- interface ConfigOptions {
17
- list?: boolean;
18
- addRegistry?: string;
19
- removeRegistry?: string;
20
- }
21
-
22
- export async function configCommand(
23
- key: string | undefined,
24
- value: string | undefined,
25
- options: ConfigOptions
26
- ): Promise<void> {
27
- const config = loadConfig();
28
-
29
- // Handle --add-registry
30
- if (options.addRegistry) {
31
- addLocalRegistry(options.addRegistry);
32
- output({
33
- action: 'add-registry',
34
- path: options.addRegistry,
35
- localRegistries: loadConfig().localRegistries
36
- });
37
- return;
38
- }
39
-
40
- // Handle --remove-registry
41
- if (options.removeRegistry) {
42
- removeLocalRegistry(options.removeRegistry);
43
- output({
44
- action: 'remove-registry',
45
- path: options.removeRegistry,
46
- localRegistries: loadConfig().localRegistries
47
- });
48
- return;
49
- }
50
-
51
- // Handle --list or no args
52
- if (options.list || (!key && !value)) {
53
- output({
54
- configFile: CONFIG_FILE,
55
- cli4aiHome: CLI4AI_HOME,
56
- config
57
- });
58
- return;
59
- }
60
-
61
- // Get specific key
62
- if (key && !value) {
63
- const val = getNestedValue(config as unknown as Record<string, unknown>, key);
64
- if (val === undefined) {
65
- outputError('NOT_FOUND', `Config key not found: ${key}`, {
66
- availableKeys: Object.keys(config)
67
- });
68
- }
69
- output({ [key]: val });
70
- return;
71
- }
72
-
73
- // Set key=value
74
- if (key && value) {
75
- const parsed = parseValue(value);
76
-
77
- if (key === 'registry') {
78
- validateRegistryUrl(parsed);
79
- }
80
-
81
- const updated = updateConfig((current) => setNestedValue(current, key, parsed));
82
- log(`Set ${key} = ${value}`);
83
- output({
84
- action: 'set',
85
- key,
86
- value: getNestedValue(updated as unknown as Record<string, unknown>, key)
87
- });
88
- return;
89
- }
90
- }
91
-
92
- function validateRegistryUrl(value: unknown): void {
93
- if (typeof value !== 'string' || value.trim().length === 0) {
94
- outputError('INVALID_INPUT', 'Registry must be a non-empty URL string', {
95
- got: value
96
- });
97
- }
98
-
99
- let url: URL;
100
- try {
101
- url = new URL(value);
102
- } catch {
103
- outputError('INVALID_INPUT', `Invalid registry URL: ${value}`, {
104
- hint: 'Use a valid http(s) URL, e.g. https://registry.cli4ai.com'
105
- });
106
- }
107
-
108
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
109
- outputError('INVALID_INPUT', `Invalid registry URL protocol: ${url.protocol}`, {
110
- hint: 'Registry URL must start with http:// or https://'
111
- });
112
- }
113
- }
114
-
115
- function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
116
- const parts = path.split('.');
117
- let current: unknown = obj;
118
-
119
- for (const part of parts) {
120
- if (current === null || current === undefined) return undefined;
121
- if (typeof current !== 'object') return undefined;
122
- current = (current as Record<string, unknown>)[part];
123
- }
124
-
125
- return current;
126
- }
127
-
128
- function setNestedValue(obj: Config, path: string, value: unknown): Config {
129
- const parts = path.split('.');
130
- // Create a mutable copy that we can safely modify
131
- const result: Record<string, unknown> = JSON.parse(JSON.stringify(obj));
132
-
133
- let current = result;
134
- for (let i = 0; i < parts.length - 1; i++) {
135
- const part = parts[i];
136
- if (typeof current[part] !== 'object' || current[part] === null) {
137
- current[part] = {};
138
- }
139
- current = current[part] as Record<string, unknown>;
140
- }
141
-
142
- current[parts[parts.length - 1]] = value;
143
- return result as unknown as Config;
144
- }
145
-
146
- function parseValue(value: string): unknown {
147
- // Try to parse as JSON
148
- try {
149
- return JSON.parse(value);
150
- } catch {
151
- // Return as string
152
- return value;
153
- }
154
- }