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.
- package/README.md +39 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.js +105 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +335 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.js +459 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.js +379 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +121 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.js +122 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +458 -0
- package/dist/commands/list.d.ts +10 -0
- package/dist/commands/list.js +76 -0
- package/dist/commands/mcp-config.d.ts +10 -0
- package/dist/commands/mcp-config.js +49 -0
- package/dist/commands/remotes.d.ts +22 -0
- package/dist/commands/remotes.js +196 -0
- package/dist/commands/remove.d.ts +8 -0
- package/dist/commands/remove.js +61 -0
- package/dist/commands/routines.d.ts +29 -0
- package/dist/commands/routines.js +363 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +104 -0
- package/dist/commands/scheduler.d.ts +27 -0
- package/dist/commands/scheduler.js +350 -0
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.js +159 -0
- package/dist/commands/secrets.d.ts +28 -0
- package/dist/commands/secrets.js +236 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +27 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.js +210 -0
- package/dist/core/config.d.ts +91 -0
- package/dist/core/config.js +738 -0
- package/dist/core/execute.d.ts +51 -0
- package/dist/core/execute.js +475 -0
- package/dist/core/link.d.ts +39 -0
- package/dist/core/link.js +214 -0
- package/dist/core/lockfile.d.ts +63 -0
- package/dist/core/lockfile.js +140 -0
- package/dist/core/manifest.d.ts +96 -0
- package/dist/core/manifest.js +224 -0
- package/dist/core/registry.d.ts +74 -0
- package/dist/core/registry.js +116 -0
- package/dist/core/remote-client.d.ts +98 -0
- package/dist/core/remote-client.js +252 -0
- package/dist/core/remotes.d.ts +88 -0
- package/dist/core/remotes.js +206 -0
- package/dist/core/routine-engine.d.ts +124 -0
- package/dist/core/routine-engine.js +699 -0
- package/dist/core/routines.d.ts +36 -0
- package/dist/core/routines.js +132 -0
- package/dist/core/scheduler-daemon.d.ts +10 -0
- package/dist/core/scheduler-daemon.js +77 -0
- package/dist/core/scheduler.d.ts +131 -0
- package/dist/core/scheduler.js +492 -0
- package/dist/core/secrets.d.ts +48 -0
- package/dist/core/secrets.js +384 -0
- package/dist/lib/cli.d.ts +84 -0
- package/dist/lib/cli.js +216 -0
- package/dist/mcp/adapter.d.ts +35 -0
- package/dist/mcp/adapter.js +94 -0
- package/dist/mcp/config-gen.d.ts +31 -0
- package/dist/mcp/config-gen.js +75 -0
- package/dist/mcp/server.d.ts +41 -0
- package/dist/mcp/server.js +296 -0
- package/dist/server/service.d.ts +85 -0
- package/dist/server/service.js +304 -0
- package/package.json +6 -3
- package/src/bin.ts +0 -118
- package/src/cli.ts +0 -412
- package/src/commands/add.ts +0 -562
- package/src/commands/browse.ts +0 -449
- package/src/commands/config.ts +0 -154
- package/src/commands/info.ts +0 -133
- package/src/commands/init.ts +0 -514
- package/src/commands/list.ts +0 -95
- package/src/commands/mcp-config.ts +0 -69
- package/src/commands/remotes.ts +0 -253
- package/src/commands/remove.ts +0 -78
- package/src/commands/routines.ts +0 -427
- package/src/commands/run.ts +0 -127
- package/src/commands/scheduler.ts +0 -438
- package/src/commands/search.ts +0 -185
- package/src/commands/secrets.ts +0 -292
- package/src/commands/serve.ts +0 -66
- package/src/commands/start.ts +0 -40
- package/src/commands/update.ts +0 -252
- package/src/core/config.ts +0 -845
- package/src/core/execute.ts +0 -569
- package/src/core/link.ts +0 -246
- package/src/core/lockfile.ts +0 -187
- package/src/core/manifest.ts +0 -327
- package/src/core/registry.ts +0 -165
- package/src/core/remote-client.ts +0 -419
- package/src/core/remotes.ts +0 -268
- package/src/core/routine-engine.ts +0 -895
- package/src/core/routines.ts +0 -171
- package/src/core/scheduler-daemon.ts +0 -94
- package/src/core/scheduler.ts +0 -606
- package/src/core/secrets.ts +0 -430
- package/src/lib/cli.ts +0 -261
- package/src/mcp/adapter.ts +0 -131
- package/src/mcp/config-gen.ts +0 -106
- package/src/mcp/server.ts +0 -365
- 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,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 {};
|