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