cli4ai 1.2.0 → 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 -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,438 +0,0 @@
1
- /**
2
- * Scheduler CLI commands.
3
- *
4
- * cli4ai scheduler start [--foreground] # Start daemon
5
- * cli4ai scheduler stop # Stop daemon
6
- * cli4ai scheduler status # Show status + upcoming runs
7
- * cli4ai scheduler logs [routine] # View logs
8
- * cli4ai scheduler history [routine] # View execution history
9
- * cli4ai scheduler run <routine> # Manual trigger
10
- */
11
-
12
- import { spawn } from 'child_process';
13
- import { readFileSync, existsSync, createReadStream } from 'fs';
14
- import { resolve, dirname } from 'path';
15
- import { fileURLToPath } from 'url';
16
- import { output, outputError, log } from '../lib/cli.js';
17
- import {
18
- isDaemonRunning,
19
- getDaemonPid,
20
- removeDaemonPid,
21
- loadSchedulerState,
22
- getRunHistory,
23
- getNextRunTime,
24
- Scheduler,
25
- SCHEDULER_LOG_FILE,
26
- SCHEDULER_PID_FILE
27
- } from '../core/scheduler.js';
28
- import { getScheduledRoutines } from '../core/routines.js';
29
-
30
- // ═══════════════════════════════════════════════════════════════════════════
31
- // SCHEDULER START
32
- // ═══════════════════════════════════════════════════════════════════════════
33
-
34
- interface SchedulerStartOptions {
35
- foreground?: boolean;
36
- }
37
-
38
- export async function schedulerStartCommand(options: SchedulerStartOptions): Promise<void> {
39
- // Check if already running
40
- if (isDaemonRunning()) {
41
- const pid = getDaemonPid();
42
- outputError('INVALID_INPUT', `Scheduler is already running (PID ${pid})`, {
43
- hint: 'Use "cli4ai scheduler stop" to stop it first'
44
- });
45
- return;
46
- }
47
-
48
- // Clean up stale PID file
49
- if (existsSync(SCHEDULER_PID_FILE)) {
50
- removeDaemonPid();
51
- }
52
-
53
- if (options.foreground) {
54
- // Run in foreground (blocking)
55
- log('Starting scheduler in foreground mode...');
56
- log('Press Ctrl+C to stop');
57
-
58
- const scheduler = new Scheduler({ projectDir: process.cwd() });
59
- await scheduler.start();
60
-
61
- // Keep running until signal
62
- await new Promise<void>((resolve) => {
63
- process.on('SIGINT', async () => {
64
- log('\nStopping scheduler...');
65
- await scheduler.stop();
66
- resolve();
67
- });
68
- process.on('SIGTERM', async () => {
69
- await scheduler.stop();
70
- resolve();
71
- });
72
- });
73
-
74
- output({ status: 'stopped' });
75
- return;
76
- }
77
-
78
- // Run as daemon (background)
79
- const daemonScript = resolve(dirname(fileURLToPath(import.meta.url)), '../core/scheduler-daemon.ts');
80
-
81
- // Spawn with detached mode
82
- const child = spawn('npx', ['tsx', daemonScript, '--project-dir', process.cwd()], {
83
- detached: true,
84
- stdio: 'ignore',
85
- env: {
86
- ...process.env,
87
- CLI4AI_DAEMON: 'true'
88
- }
89
- });
90
-
91
- // Unref to allow parent to exit
92
- child.unref();
93
-
94
- // Wait a moment for the daemon to write its PID
95
- await new Promise(r => setTimeout(r, 500));
96
-
97
- const pid = getDaemonPid();
98
- if (pid && isDaemonRunning()) {
99
- log(`Scheduler daemon started (PID ${pid})`);
100
- output({ status: 'started', pid });
101
- } else {
102
- outputError('API_ERROR', 'Failed to start scheduler daemon', {
103
- hint: 'Check logs with "cli4ai scheduler logs"'
104
- });
105
- }
106
- }
107
-
108
- // ═══════════════════════════════════════════════════════════════════════════
109
- // SCHEDULER STOP
110
- // ═══════════════════════════════════════════════════════════════════════════
111
-
112
- export async function schedulerStopCommand(): Promise<void> {
113
- const pid = getDaemonPid();
114
-
115
- if (!pid) {
116
- outputError('NOT_FOUND', 'Scheduler daemon is not running');
117
- return;
118
- }
119
-
120
- if (!isDaemonRunning()) {
121
- // Clean up stale PID file
122
- removeDaemonPid();
123
- log('Scheduler was not running (cleaned up stale PID file)');
124
- output({ status: 'stopped', pid: null });
125
- return;
126
- }
127
-
128
- log(`Stopping scheduler daemon (PID ${pid})...`);
129
-
130
- try {
131
- // Send SIGTERM for graceful shutdown
132
- process.kill(pid, 'SIGTERM');
133
-
134
- // Wait for process to exit (up to 10 seconds)
135
- const maxWait = 10000;
136
- const checkInterval = 100;
137
- let waited = 0;
138
-
139
- while (waited < maxWait) {
140
- await new Promise(r => setTimeout(r, checkInterval));
141
- waited += checkInterval;
142
-
143
- if (!isDaemonRunning()) {
144
- break;
145
- }
146
- }
147
-
148
- if (isDaemonRunning()) {
149
- // Force kill if still running
150
- log('Scheduler did not stop gracefully, sending SIGKILL...');
151
- process.kill(pid, 'SIGKILL');
152
- await new Promise(r => setTimeout(r, 500));
153
- }
154
-
155
- removeDaemonPid();
156
- log('Scheduler daemon stopped');
157
- output({ status: 'stopped', pid });
158
- } catch (err) {
159
- if ((err as NodeJS.ErrnoException).code === 'ESRCH') {
160
- // Process doesn't exist
161
- removeDaemonPid();
162
- log('Scheduler was not running (cleaned up stale PID file)');
163
- output({ status: 'stopped', pid: null });
164
- } else {
165
- outputError('API_ERROR', `Failed to stop scheduler: ${err instanceof Error ? err.message : String(err)}`);
166
- }
167
- }
168
- }
169
-
170
- // ═══════════════════════════════════════════════════════════════════════════
171
- // SCHEDULER STATUS
172
- // ═══════════════════════════════════════════════════════════════════════════
173
-
174
- export async function schedulerStatusCommand(): Promise<void> {
175
- const pid = getDaemonPid();
176
- const running = isDaemonRunning();
177
-
178
- // Get scheduled routines
179
- const scheduledRoutines = getScheduledRoutines(process.cwd());
180
- const state = loadSchedulerState();
181
-
182
- if (!running) {
183
- log('Scheduler: not running');
184
-
185
- if (scheduledRoutines.length === 0) {
186
- log('\nNo scheduled routines found.');
187
- log('Add a "schedule" field to your .routine.json files to enable scheduling.');
188
- } else {
189
- log(`\nFound ${scheduledRoutines.length} scheduled routine(s):`);
190
- for (const routine of scheduledRoutines) {
191
- const scheduleStr = routine.schedule.cron
192
- ? `cron: ${routine.schedule.cron}`
193
- : `interval: ${routine.schedule.interval}`;
194
- log(` - ${routine.name} (${scheduleStr})`);
195
- }
196
- log('\nStart the scheduler with: cli4ai scheduler start');
197
- }
198
-
199
- output({
200
- running: false,
201
- pid: null,
202
- routines: scheduledRoutines.map(r => ({
203
- name: r.name,
204
- schedule: r.schedule,
205
- path: r.path
206
- }))
207
- });
208
- return;
209
- }
210
-
211
- log(`Scheduler: running (PID ${pid})`);
212
-
213
- if (state) {
214
- log(`Started: ${state.startedAt}`);
215
-
216
- const routineStates = Object.values(state.routines);
217
- if (routineStates.length === 0) {
218
- log('\nNo scheduled routines.');
219
- } else {
220
- log(`\nScheduled routines (${routineStates.length}):`);
221
- log('');
222
-
223
- for (const routine of routineStates) {
224
- const scheduleStr = routine.schedule.cron
225
- ? `cron: ${routine.schedule.cron}`
226
- : `interval: ${routine.schedule.interval}`;
227
-
228
- const statusIcon = routine.running ? '⏳' :
229
- routine.lastStatus === 'success' ? '✓' :
230
- routine.lastStatus === 'failed' ? '✗' : '○';
231
-
232
- log(` ${statusIcon} ${routine.name}`);
233
- log(` Schedule: ${scheduleStr}`);
234
-
235
- if (routine.nextRunAt) {
236
- const nextRun = new Date(routine.nextRunAt);
237
- const now = new Date();
238
- const diffMs = nextRun.getTime() - now.getTime();
239
- const diffMins = Math.round(diffMs / 60000);
240
-
241
- if (diffMins < 1) {
242
- log(` Next run: in < 1 minute`);
243
- } else if (diffMins < 60) {
244
- log(` Next run: in ${diffMins} minute${diffMins === 1 ? '' : 's'}`);
245
- } else {
246
- const diffHours = Math.round(diffMins / 60);
247
- log(` Next run: in ${diffHours} hour${diffHours === 1 ? '' : 's'}`);
248
- }
249
- }
250
-
251
- if (routine.lastRunAt) {
252
- log(` Last run: ${routine.lastRunAt} (${routine.lastStatus})`);
253
- }
254
-
255
- log('');
256
- }
257
- }
258
- }
259
-
260
- output({
261
- running: true,
262
- pid,
263
- startedAt: state?.startedAt,
264
- routines: state ? Object.values(state.routines) : []
265
- });
266
- }
267
-
268
- // ═══════════════════════════════════════════════════════════════════════════
269
- // SCHEDULER LOGS
270
- // ═══════════════════════════════════════════════════════════════════════════
271
-
272
- interface SchedulerLogsOptions {
273
- follow?: boolean;
274
- lines?: number;
275
- }
276
-
277
- export async function schedulerLogsCommand(options: SchedulerLogsOptions): Promise<void> {
278
- if (!existsSync(SCHEDULER_LOG_FILE)) {
279
- log('No scheduler logs found.');
280
- output({ logs: [] });
281
- return;
282
- }
283
-
284
- const lines = options.lines ?? 50;
285
-
286
- if (options.follow) {
287
- // Tail -f mode
288
- log(`Tailing scheduler logs (${SCHEDULER_LOG_FILE})...`);
289
- log('Press Ctrl+C to stop\n');
290
-
291
- // Read initial content
292
- const content = readFileSync(SCHEDULER_LOG_FILE, 'utf-8');
293
- const allLines = content.split('\n');
294
- const lastLines = allLines.slice(-lines);
295
- process.stdout.write(lastLines.join('\n') + '\n');
296
-
297
- // Watch for new content
298
- const { watch } = await import('fs');
299
- let lastSize = content.length;
300
-
301
- const watcher = watch(SCHEDULER_LOG_FILE, (eventType) => {
302
- if (eventType === 'change') {
303
- const newContent = readFileSync(SCHEDULER_LOG_FILE, 'utf-8');
304
- if (newContent.length > lastSize) {
305
- process.stdout.write(newContent.slice(lastSize));
306
- lastSize = newContent.length;
307
- }
308
- }
309
- });
310
-
311
- // Keep running until interrupted
312
- await new Promise<void>((resolve) => {
313
- let closed = false;
314
- const closeWatcher = () => {
315
- if (closed) return;
316
- closed = true;
317
- try {
318
- watcher.close();
319
- } catch {
320
- // ignore
321
- }
322
- };
323
-
324
- const stop = () => {
325
- closeWatcher();
326
- resolve();
327
- };
328
-
329
- process.once('exit', closeWatcher);
330
- process.once('SIGINT', stop);
331
- process.once('SIGTERM', stop);
332
- });
333
- } else {
334
- // Read last N lines
335
- const content = readFileSync(SCHEDULER_LOG_FILE, 'utf-8');
336
- const allLines = content.split('\n').filter(l => l.trim());
337
- const lastLines = allLines.slice(-lines);
338
-
339
- for (const line of lastLines) {
340
- log(line);
341
- }
342
-
343
- output({ logs: lastLines });
344
- }
345
- }
346
-
347
- // ═══════════════════════════════════════════════════════════════════════════
348
- // SCHEDULER HISTORY
349
- // ═══════════════════════════════════════════════════════════════════════════
350
-
351
- interface SchedulerHistoryOptions {
352
- limit?: number;
353
- }
354
-
355
- export async function schedulerHistoryCommand(routineName?: string, options: SchedulerHistoryOptions = {}): Promise<void> {
356
- const limit = options.limit ?? 20;
357
- const history = getRunHistory(routineName, limit);
358
-
359
- if (history.length === 0) {
360
- log(routineName
361
- ? `No execution history found for routine: ${routineName}`
362
- : 'No execution history found.');
363
- output({ history: [] });
364
- return;
365
- }
366
-
367
- log(routineName
368
- ? `Execution history for ${routineName} (last ${history.length}):`
369
- : `Execution history (last ${history.length}):`);
370
- log('');
371
-
372
- for (const record of history) {
373
- const statusIcon = record.status === 'success' ? '✓' : '✗';
374
- const duration = record.durationMs < 1000
375
- ? `${record.durationMs}ms`
376
- : `${(record.durationMs / 1000).toFixed(1)}s`;
377
-
378
- log(` ${statusIcon} ${record.routine}`);
379
- log(` Started: ${record.startedAt}`);
380
- log(` Duration: ${duration}`);
381
- if (record.retryAttempt > 0) {
382
- log(` Retry: #${record.retryAttempt}`);
383
- }
384
- if (record.error) {
385
- log(` Error: ${record.error}`);
386
- }
387
- log('');
388
- }
389
-
390
- output({ history });
391
- }
392
-
393
- // ═══════════════════════════════════════════════════════════════════════════
394
- // SCHEDULER RUN
395
- // ═══════════════════════════════════════════════════════════════════════════
396
-
397
- export async function schedulerRunCommand(routineName: string): Promise<void> {
398
- // Check if routine exists and is scheduled
399
- const scheduledRoutines = getScheduledRoutines(process.cwd());
400
- const routine = scheduledRoutines.find(r => r.name === routineName);
401
-
402
- if (!routine) {
403
- // Check if it exists but isn't scheduled
404
- const { resolveRoutine } = await import('../core/routines.js');
405
- const exists = resolveRoutine(routineName, process.cwd());
406
-
407
- if (exists) {
408
- outputError('NOT_FOUND', `Routine "${routineName}" exists but has no schedule configured`, {
409
- hint: 'Add a "schedule" field to enable scheduling, or use "cli4ai routines run" for one-time execution'
410
- });
411
- } else {
412
- outputError('NOT_FOUND', `Routine not found: ${routineName}`);
413
- }
414
- return;
415
- }
416
-
417
- log(`Running routine: ${routineName}`);
418
-
419
- const scheduler = new Scheduler({ projectDir: process.cwd() });
420
- scheduler.refreshRoutines();
421
-
422
- try {
423
- const record = await scheduler.runNow(routineName);
424
-
425
- if (record.status === 'success') {
426
- log(`Routine completed successfully in ${record.durationMs}ms`);
427
- } else {
428
- log(`Routine failed (exit code ${record.exitCode})`);
429
- if (record.error) {
430
- log(`Error: ${record.error}`);
431
- }
432
- }
433
-
434
- output(record);
435
- } catch (err) {
436
- outputError('API_ERROR', `Failed to run routine: ${err instanceof Error ? err.message : String(err)}`);
437
- }
438
- }
@@ -1,185 +0,0 @@
1
- /**
2
- * cli4ai search - Search for packages
3
- */
4
-
5
- import { readdirSync, existsSync } from 'fs';
6
- import { resolve } from 'path';
7
- import { spawnSync } from 'child_process';
8
- import { output, outputError, log } from '../lib/cli.js';
9
- import { loadConfig, getGlobalPackages, getLocalPackages } from '../core/config.js';
10
- import { tryLoadManifest, type Manifest } from '../core/manifest.js';
11
- import { remoteListPackages, RemoteConnectionError, RemoteApiError } from '../core/remote-client.js';
12
-
13
- interface SearchOptions {
14
- limit?: string;
15
- remote?: string;
16
- }
17
-
18
- interface SearchResult {
19
- name: string;
20
- version: string;
21
- description?: string;
22
- path?: string;
23
- source: 'local-registry' | 'installed' | 'npm';
24
- installed: boolean;
25
- }
26
-
27
- export async function searchCommand(query: string, options: SearchOptions): Promise<void> {
28
- const limit = parseInt(options.limit || '20', 10);
29
- const queryLower = query.toLowerCase();
30
-
31
- // Handle remote search
32
- if (options.remote) {
33
- try {
34
- const packageList = await remoteListPackages(options.remote);
35
- const results = packageList.packages
36
- .filter(pkg =>
37
- pkg.name.toLowerCase().includes(queryLower) ||
38
- pkg.path.toLowerCase().includes(queryLower)
39
- )
40
- .slice(0, limit)
41
- .map(pkg => ({
42
- name: pkg.name,
43
- version: pkg.version,
44
- source: 'remote' as const,
45
- installed: true
46
- }));
47
-
48
- output({
49
- remote: options.remote,
50
- query,
51
- results,
52
- count: results.length
53
- });
54
- } catch (err) {
55
- if (err instanceof RemoteConnectionError) {
56
- outputError('NETWORK_ERROR', err.message, { remote: err.remoteName, url: err.url });
57
- } else if (err instanceof RemoteApiError) {
58
- outputError(err.code, err.message, { remote: err.remoteName, details: err.details });
59
- } else {
60
- throw err;
61
- }
62
- }
63
- return;
64
- }
65
-
66
- const results: SearchResult[] = [];
67
- const seen = new Set<string>();
68
-
69
- // Get installed packages
70
- const installedNames = new Set([
71
- ...getLocalPackages(process.cwd()).map(p => p.name),
72
- ...getGlobalPackages().map(p => p.name)
73
- ]);
74
-
75
- // Search local registries
76
- const config = loadConfig();
77
- for (const registryPath of config.localRegistries) {
78
- if (!existsSync(registryPath)) continue;
79
-
80
- try {
81
- for (const entry of readdirSync(registryPath, { withFileTypes: true })) {
82
- if (!entry.isDirectory()) continue;
83
- if (seen.has(entry.name)) continue;
84
-
85
- const pkgPath = resolve(registryPath, entry.name);
86
- const manifest = tryLoadManifest(pkgPath);
87
-
88
- if (!manifest) continue;
89
-
90
- // Check if matches query
91
- if (matches(manifest, queryLower)) {
92
- seen.add(manifest.name);
93
- results.push({
94
- name: manifest.name,
95
- version: manifest.version,
96
- description: manifest.description,
97
- path: pkgPath,
98
- source: 'local-registry',
99
- installed: installedNames.has(manifest.name)
100
- });
101
- }
102
-
103
- if (results.length >= limit) break;
104
- }
105
- } catch {
106
- // Skip inaccessible registries
107
- }
108
-
109
- if (results.length >= limit) break;
110
- }
111
-
112
- // Search npm for @cli4ai packages
113
- if (results.length < limit) {
114
- try {
115
- log(`Searching npm for @cli4ai packages...`);
116
- // Use spawnSync with argument array to prevent command injection
117
- const searchResult = spawnSync('npm', ['search', `@cli4ai/${query}`, '--json'], {
118
- encoding: 'utf-8',
119
- timeout: 10000,
120
- stdio: ['pipe', 'pipe', 'pipe']
121
- });
122
-
123
- let npmResults = searchResult.stdout || '';
124
-
125
- // Fallback to searching @cli4ai if specific query fails
126
- if (!npmResults || npmResults === '[]') {
127
- const fallbackResult = spawnSync('npm', ['search', '@cli4ai', '--json'], {
128
- encoding: 'utf-8',
129
- timeout: 10000,
130
- stdio: ['pipe', 'pipe', 'pipe']
131
- });
132
- npmResults = fallbackResult.stdout || '[]';
133
- }
134
-
135
- const packages = JSON.parse(npmResults || '[]');
136
- for (const pkg of packages) {
137
- if (seen.has(pkg.name)) continue;
138
-
139
- // Filter to only @cli4ai scoped packages
140
- if (!pkg.name.startsWith('@cli4ai/')) continue;
141
-
142
- // Check if matches query
143
- const shortName = pkg.name.replace('@cli4ai/', '');
144
- if (
145
- shortName.toLowerCase().includes(queryLower) ||
146
- pkg.description?.toLowerCase().includes(queryLower) ||
147
- pkg.keywords?.some((k: string) => k.toLowerCase().includes(queryLower))
148
- ) {
149
- seen.add(pkg.name);
150
- results.push({
151
- name: shortName,
152
- version: pkg.version,
153
- description: pkg.description,
154
- source: 'npm',
155
- installed: installedNames.has(shortName) || installedNames.has(pkg.name)
156
- });
157
- }
158
-
159
- if (results.length >= limit) break;
160
- }
161
- } catch {
162
- // npm search failed, continue with local results
163
- }
164
- }
165
-
166
- output({
167
- query,
168
- results: results.slice(0, limit),
169
- count: results.length,
170
- registries: config.localRegistries
171
- });
172
- }
173
-
174
- function matches(manifest: Manifest, query: string): boolean {
175
- // Match against name
176
- if (manifest.name.toLowerCase().includes(query)) return true;
177
-
178
- // Match against description
179
- if (manifest.description?.toLowerCase().includes(query)) return true;
180
-
181
- // Match against keywords
182
- if (manifest.keywords?.some(k => k.toLowerCase().includes(query))) return true;
183
-
184
- return false;
185
- }