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