cli4ai 0.8.0
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 +275 -0
- package/package.json +49 -0
- package/src/bin.ts +120 -0
- package/src/cli.ts +256 -0
- package/src/commands/add.ts +530 -0
- package/src/commands/browse.ts +449 -0
- package/src/commands/config.ts +126 -0
- package/src/commands/info.ts +102 -0
- package/src/commands/init.test.ts +163 -0
- package/src/commands/init.ts +560 -0
- package/src/commands/list.ts +89 -0
- package/src/commands/mcp-config.ts +59 -0
- package/src/commands/remove.ts +72 -0
- package/src/commands/routines.ts +393 -0
- package/src/commands/run.ts +45 -0
- package/src/commands/search.ts +148 -0
- package/src/commands/secrets.ts +273 -0
- package/src/commands/start.ts +40 -0
- package/src/commands/update.ts +218 -0
- package/src/core/config.test.ts +188 -0
- package/src/core/config.ts +649 -0
- package/src/core/execute.ts +507 -0
- package/src/core/link.test.ts +238 -0
- package/src/core/link.ts +190 -0
- package/src/core/lockfile.test.ts +337 -0
- package/src/core/lockfile.ts +308 -0
- package/src/core/manifest.test.ts +327 -0
- package/src/core/manifest.ts +319 -0
- package/src/core/routine-engine.test.ts +139 -0
- package/src/core/routine-engine.ts +725 -0
- package/src/core/routines.ts +111 -0
- package/src/core/secrets.test.ts +79 -0
- package/src/core/secrets.ts +430 -0
- package/src/lib/cli.ts +234 -0
- package/src/mcp/adapter.test.ts +132 -0
- package/src/mcp/adapter.ts +123 -0
- package/src/mcp/config-gen.test.ts +214 -0
- package/src/mcp/config-gen.ts +106 -0
- package/src/mcp/server.ts +363 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai secrets - Manage secrets for CLI tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createInterface } from 'readline';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
import { existsSync, readFileSync } from 'fs';
|
|
8
|
+
import { output, outputError, log } from '../lib/cli.js';
|
|
9
|
+
import {
|
|
10
|
+
getSecret,
|
|
11
|
+
setSecret,
|
|
12
|
+
deleteSecret,
|
|
13
|
+
listSecretKeys,
|
|
14
|
+
getSecretSource,
|
|
15
|
+
hasSecret
|
|
16
|
+
} from '../core/secrets.js';
|
|
17
|
+
import { findPackage } from '../core/config.js';
|
|
18
|
+
|
|
19
|
+
interface SecretsOptions {
|
|
20
|
+
package?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Prompt for input (hidden for secrets)
|
|
25
|
+
*/
|
|
26
|
+
async function prompt(message: string, hidden = false): Promise<string> {
|
|
27
|
+
const rl = createInterface({
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stderr
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
if (hidden && process.stdin.isTTY) {
|
|
34
|
+
// Hide input for secrets
|
|
35
|
+
process.stderr.write(message);
|
|
36
|
+
let value = '';
|
|
37
|
+
|
|
38
|
+
process.stdin.setRawMode(true);
|
|
39
|
+
process.stdin.resume();
|
|
40
|
+
process.stdin.setEncoding('utf8');
|
|
41
|
+
|
|
42
|
+
const onData = (char: string) => {
|
|
43
|
+
if (char === '\n' || char === '\r') {
|
|
44
|
+
process.stdin.setRawMode(false);
|
|
45
|
+
process.stdin.removeListener('data', onData);
|
|
46
|
+
process.stderr.write('\n');
|
|
47
|
+
rl.close();
|
|
48
|
+
resolve(value);
|
|
49
|
+
} else if (char === '\u0003') {
|
|
50
|
+
// Ctrl+C
|
|
51
|
+
process.exit(1);
|
|
52
|
+
} else if (char === '\u007F') {
|
|
53
|
+
// Backspace
|
|
54
|
+
if (value.length > 0) {
|
|
55
|
+
value = value.slice(0, -1);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
value += char;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
process.stdin.on('data', onData);
|
|
63
|
+
} else {
|
|
64
|
+
rl.question(message, (answer) => {
|
|
65
|
+
rl.close();
|
|
66
|
+
resolve(answer);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* cli4ai secrets set <key> [value]
|
|
74
|
+
*/
|
|
75
|
+
export async function secretsSetCommand(key: string, value?: string): Promise<void> {
|
|
76
|
+
if (!key) {
|
|
77
|
+
outputError('INVALID_INPUT', 'Secret key is required');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Prompt for value if not provided
|
|
81
|
+
if (!value) {
|
|
82
|
+
value = await prompt(`Enter value for ${key}: `, true);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!value) {
|
|
86
|
+
outputError('INVALID_INPUT', 'Secret value cannot be empty');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setSecret(key, value);
|
|
90
|
+
log(`✓ Secret '${key}' saved to vault`);
|
|
91
|
+
|
|
92
|
+
output({ key, status: 'saved' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* cli4ai secrets get <key>
|
|
97
|
+
*/
|
|
98
|
+
export async function secretsGetCommand(key: string): Promise<void> {
|
|
99
|
+
if (!key) {
|
|
100
|
+
outputError('INVALID_INPUT', 'Secret key is required');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const value = getSecret(key);
|
|
104
|
+
const source = getSecretSource(key);
|
|
105
|
+
|
|
106
|
+
if (!value) {
|
|
107
|
+
outputError('NOT_FOUND', `Secret '${key}' not found`, {
|
|
108
|
+
hint: `Set it with: cli4ai secrets set ${key}`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Output value to stdout (for piping)
|
|
113
|
+
console.log(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* cli4ai secrets list
|
|
118
|
+
*/
|
|
119
|
+
export async function secretsListCommand(options: SecretsOptions): Promise<void> {
|
|
120
|
+
const vaultKeys = listSecretKeys();
|
|
121
|
+
|
|
122
|
+
// If package specified, show status of required secrets
|
|
123
|
+
if (options.package) {
|
|
124
|
+
const pkg = findPackage(options.package, process.cwd());
|
|
125
|
+
if (!pkg) {
|
|
126
|
+
outputError('NOT_FOUND', `Package not found: ${options.package}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const cli4aiPath = resolve(pkg.path, 'cli4ai.json');
|
|
130
|
+
if (!existsSync(cli4aiPath)) {
|
|
131
|
+
outputError('NOT_FOUND', 'Package has no cli4ai.json');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const manifest = JSON.parse(readFileSync(cli4aiPath, 'utf-8'));
|
|
135
|
+
const envDefs = manifest.env || {};
|
|
136
|
+
|
|
137
|
+
log(`\nSecrets for ${options.package}:\n`);
|
|
138
|
+
|
|
139
|
+
const secrets: Array<{ key: string; required: boolean; source: string; description?: string }> = [];
|
|
140
|
+
|
|
141
|
+
for (const [key, def] of Object.entries(envDefs) as [string, { required?: boolean; description?: string }][]) {
|
|
142
|
+
const source = getSecretSource(key);
|
|
143
|
+
secrets.push({
|
|
144
|
+
key,
|
|
145
|
+
required: def.required ?? false,
|
|
146
|
+
source,
|
|
147
|
+
description: def.description
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const status = source === 'env' ? '✓ (env)' :
|
|
151
|
+
source === 'vault' ? '✓ (vault)' :
|
|
152
|
+
def.required ? '✗ missing' : '○ optional';
|
|
153
|
+
|
|
154
|
+
log(` ${status.padEnd(12)} ${key}`);
|
|
155
|
+
if (def.description) {
|
|
156
|
+
log(` ${def.description}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
log('');
|
|
161
|
+
output({ package: options.package, secrets });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// List all secrets in vault
|
|
166
|
+
if (vaultKeys.length === 0) {
|
|
167
|
+
log('No secrets stored in vault.\n');
|
|
168
|
+
log('Set a secret: cli4ai secrets set SLACK_BOT_TOKEN');
|
|
169
|
+
log('From env var: Secrets are also read from environment variables\n');
|
|
170
|
+
output({ secrets: [] });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
log('\nSecrets in vault:\n');
|
|
175
|
+
|
|
176
|
+
const secrets = vaultKeys.map(key => {
|
|
177
|
+
const source = getSecretSource(key);
|
|
178
|
+
const overridden = source === 'env';
|
|
179
|
+
log(` ${key}${overridden ? ' (overridden by env)' : ''}`);
|
|
180
|
+
return { key, overriddenByEnv: overridden };
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
log('\n');
|
|
184
|
+
output({ secrets });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* cli4ai secrets delete <key>
|
|
189
|
+
*/
|
|
190
|
+
export async function secretsDeleteCommand(key: string): Promise<void> {
|
|
191
|
+
if (!key) {
|
|
192
|
+
outputError('INVALID_INPUT', 'Secret key is required');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const deleted = deleteSecret(key);
|
|
196
|
+
|
|
197
|
+
if (!deleted) {
|
|
198
|
+
outputError('NOT_FOUND', `Secret '${key}' not found in vault`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
log(`✓ Secret '${key}' deleted from vault`);
|
|
202
|
+
output({ key, status: 'deleted' });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* cli4ai secrets init [package]
|
|
207
|
+
* Interactive setup for a package's required secrets
|
|
208
|
+
*/
|
|
209
|
+
export async function secretsInitCommand(packageName?: string, options?: SecretsOptions): Promise<void> {
|
|
210
|
+
const pkgName = packageName || options?.package;
|
|
211
|
+
|
|
212
|
+
if (!pkgName) {
|
|
213
|
+
outputError('INVALID_INPUT', 'Package name required', {
|
|
214
|
+
hint: 'Usage: cli4ai secrets init <package>'
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const pkg = findPackage(pkgName, process.cwd());
|
|
219
|
+
if (!pkg) {
|
|
220
|
+
outputError('NOT_FOUND', `Package not found: ${pkgName}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const cli4aiPath = resolve(pkg.path, 'cli4ai.json');
|
|
224
|
+
if (!existsSync(cli4aiPath)) {
|
|
225
|
+
outputError('NOT_FOUND', 'Package has no cli4ai.json');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const manifest = JSON.parse(readFileSync(cli4aiPath, 'utf-8'));
|
|
229
|
+
const envDefs = manifest.env || {};
|
|
230
|
+
|
|
231
|
+
const required = Object.entries(envDefs)
|
|
232
|
+
.filter(([_, def]) => (def as { required?: boolean }).required)
|
|
233
|
+
.map(([key, def]) => ({ key, ...(def as { description?: string }) }));
|
|
234
|
+
|
|
235
|
+
if (required.length === 0) {
|
|
236
|
+
log(`\n${pkgName} has no required secrets.\n`);
|
|
237
|
+
output({ package: pkgName, configured: [] });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
log(`\nConfiguring secrets for ${pkgName}:\n`);
|
|
242
|
+
|
|
243
|
+
const configured: string[] = [];
|
|
244
|
+
const skipped: string[] = [];
|
|
245
|
+
|
|
246
|
+
for (const { key, description } of required) {
|
|
247
|
+
const source = getSecretSource(key);
|
|
248
|
+
|
|
249
|
+
if (source !== 'missing') {
|
|
250
|
+
log(` ✓ ${key} (already set via ${source})`);
|
|
251
|
+
skipped.push(key);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (description) {
|
|
256
|
+
log(`\n ${key}: ${description}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const value = await prompt(` Enter ${key}: `, true);
|
|
260
|
+
|
|
261
|
+
if (value) {
|
|
262
|
+
setSecret(key, value);
|
|
263
|
+
log(` ✓ ${key} saved`);
|
|
264
|
+
configured.push(key);
|
|
265
|
+
} else {
|
|
266
|
+
log(` ○ ${key} skipped`);
|
|
267
|
+
skipped.push(key);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
log('\n');
|
|
272
|
+
output({ package: pkgName, configured, skipped });
|
|
273
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai start - Start MCP server for a package
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { outputError } from '../lib/cli.js';
|
|
6
|
+
import { findPackage } from '../core/config.js';
|
|
7
|
+
import { loadManifest } from '../core/manifest.js';
|
|
8
|
+
import { startMcpServer } from '../mcp/server.js';
|
|
9
|
+
|
|
10
|
+
interface StartOptions {
|
|
11
|
+
stdio?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function startCommand(
|
|
15
|
+
packageName: string,
|
|
16
|
+
options: StartOptions
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
|
|
20
|
+
// Find the package
|
|
21
|
+
const pkg = findPackage(packageName, cwd);
|
|
22
|
+
if (!pkg) {
|
|
23
|
+
outputError('NOT_FOUND', `Package not found: ${packageName}`, {
|
|
24
|
+
hint: 'Install the package first with "cli4ai add <package>"'
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Load manifest
|
|
29
|
+
const manifest = loadManifest(pkg!.path);
|
|
30
|
+
|
|
31
|
+
// Check if MCP is enabled
|
|
32
|
+
if (!manifest.mcp?.enabled) {
|
|
33
|
+
outputError('INVALID_INPUT', `Package ${packageName} does not have MCP enabled`, {
|
|
34
|
+
hint: 'Add "mcp": { "enabled": true } to cli4ai.json'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Start MCP server (stdio mode is default/only mode for now)
|
|
39
|
+
await startMcpServer(manifest, pkg!.path);
|
|
40
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai update - Update installed packages and cli4ai itself
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { log, outputError } from '../lib/cli.js';
|
|
7
|
+
import {
|
|
8
|
+
getNpmGlobalPackages,
|
|
9
|
+
getGlobalPackages,
|
|
10
|
+
getLocalPackages
|
|
11
|
+
} from '../core/config.js';
|
|
12
|
+
|
|
13
|
+
interface UpdateOptions {
|
|
14
|
+
self?: boolean;
|
|
15
|
+
all?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UpdateResult {
|
|
19
|
+
name: string;
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
status: 'updated' | 'current' | 'failed';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ANSI colors
|
|
26
|
+
const RESET = '\x1B[0m';
|
|
27
|
+
const BOLD = '\x1B[1m';
|
|
28
|
+
const DIM = '\x1B[2m';
|
|
29
|
+
const CYAN = '\x1B[36m';
|
|
30
|
+
const GREEN = '\x1B[32m';
|
|
31
|
+
const YELLOW = '\x1B[33m';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get latest version from npm registry API (fast!)
|
|
35
|
+
*/
|
|
36
|
+
async function getLatestVersion(packageName: string): Promise<string | null> {
|
|
37
|
+
try {
|
|
38
|
+
// URL-encode the package name to handle scoped packages like @cli4ai/foo
|
|
39
|
+
// npm registry expects: @scope%2Fpackage (slash encoded, @ not encoded)
|
|
40
|
+
const encodedName = packageName.replace('/', '%2F');
|
|
41
|
+
const response = await fetch(`https://registry.npmjs.org/${encodedName}/latest`, {
|
|
42
|
+
headers: { 'Accept': 'application/json' }
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) return null;
|
|
45
|
+
const data = await response.json() as { version: string };
|
|
46
|
+
return data.version;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get latest versions for multiple packages in parallel
|
|
54
|
+
*/
|
|
55
|
+
async function getLatestVersions(packageNames: string[]): Promise<Map<string, string | null>> {
|
|
56
|
+
const results = new Map<string, string | null>();
|
|
57
|
+
const promises = packageNames.map(async (name) => {
|
|
58
|
+
const version = await getLatestVersion(name);
|
|
59
|
+
results.set(name, version);
|
|
60
|
+
});
|
|
61
|
+
await Promise.all(promises);
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Update a single package (async)
|
|
67
|
+
*/
|
|
68
|
+
async function updatePackageAsync(packageName: string): Promise<boolean> {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const proc = spawn('npm', ['install', '-g', `${packageName}@latest`], {
|
|
71
|
+
stdio: 'pipe'
|
|
72
|
+
});
|
|
73
|
+
proc.on('close', (code) => {
|
|
74
|
+
resolve(code === 0);
|
|
75
|
+
});
|
|
76
|
+
proc.on('error', () => {
|
|
77
|
+
resolve(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check for cli4ai framework updates
|
|
84
|
+
*/
|
|
85
|
+
export async function checkForUpdates(): Promise<{ hasUpdate: boolean; current: string; latest: string }> {
|
|
86
|
+
const { VERSION } = await import('../lib/cli.js');
|
|
87
|
+
const latest = await getLatestVersion('cli4ai');
|
|
88
|
+
|
|
89
|
+
if (!latest) {
|
|
90
|
+
return { hasUpdate: false, current: VERSION, latest: VERSION };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hasUpdate = latest !== VERSION;
|
|
94
|
+
return { hasUpdate, current: VERSION, latest };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get all updateable packages
|
|
99
|
+
*/
|
|
100
|
+
function getUpdateablePackages(): Array<{ name: string; npmName: string; version: string }> {
|
|
101
|
+
const packages: Array<{ name: string; npmName: string; version: string }> = [];
|
|
102
|
+
|
|
103
|
+
// Get npm global @cli4ai packages
|
|
104
|
+
for (const pkg of getNpmGlobalPackages()) {
|
|
105
|
+
packages.push({
|
|
106
|
+
name: pkg.name,
|
|
107
|
+
npmName: `@cli4ai/${pkg.name}`,
|
|
108
|
+
version: pkg.version
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return packages;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function updateCommand(options: UpdateOptions): Promise<void> {
|
|
116
|
+
const results: UpdateResult[] = [];
|
|
117
|
+
|
|
118
|
+
// Update cli4ai itself first if --self or --all
|
|
119
|
+
if (options.self || options.all) {
|
|
120
|
+
log(`\n${BOLD}Checking cli4ai framework...${RESET}`);
|
|
121
|
+
|
|
122
|
+
const { hasUpdate, current, latest } = await checkForUpdates();
|
|
123
|
+
|
|
124
|
+
if (hasUpdate) {
|
|
125
|
+
log(` ${CYAN}↑${RESET} cli4ai ${DIM}${current}${RESET} → ${GREEN}${latest}${RESET}`);
|
|
126
|
+
const success = await updatePackageAsync('cli4ai');
|
|
127
|
+
|
|
128
|
+
if (success) {
|
|
129
|
+
log(` ${GREEN}✓${RESET} cli4ai updated\n`);
|
|
130
|
+
results.push({ name: 'cli4ai', from: current, to: latest, status: 'updated' });
|
|
131
|
+
} else {
|
|
132
|
+
log(` ${YELLOW}✗${RESET} Failed to update cli4ai\n`);
|
|
133
|
+
results.push({ name: 'cli4ai', from: current, to: latest, status: 'failed' });
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
log(` ${GREEN}✓${RESET} cli4ai is up to date (${current})\n`);
|
|
137
|
+
results.push({ name: 'cli4ai', from: current, to: current, status: 'current' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.self && !options.all) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Get all installed packages
|
|
146
|
+
const packages = getUpdateablePackages();
|
|
147
|
+
|
|
148
|
+
if (packages.length === 0) {
|
|
149
|
+
log(`${DIM}No @cli4ai packages installed.${RESET}`);
|
|
150
|
+
log(`${DIM}Run ${RESET}cli4ai browse${DIM} to install packages.${RESET}\n`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
log(`${BOLD}Checking ${packages.length} package${packages.length !== 1 ? 's' : ''} for updates...${RESET}\n`);
|
|
155
|
+
|
|
156
|
+
// Fetch all versions in parallel (fast!)
|
|
157
|
+
const npmNames = packages.map(p => p.npmName);
|
|
158
|
+
const latestVersions = await getLatestVersions(npmNames);
|
|
159
|
+
|
|
160
|
+
// Separate packages that need updates vs current
|
|
161
|
+
const toUpdate: Array<{ name: string; npmName: string; from: string; to: string }> = [];
|
|
162
|
+
|
|
163
|
+
for (const pkg of packages) {
|
|
164
|
+
const latest = latestVersions.get(pkg.npmName);
|
|
165
|
+
|
|
166
|
+
if (!latest) {
|
|
167
|
+
log(` ${DIM}${pkg.name}${RESET} - ${YELLOW}could not check${RESET}`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (latest === pkg.version) {
|
|
172
|
+
log(` ${GREEN}✓${RESET} ${pkg.name} ${DIM}${pkg.version}${RESET}`);
|
|
173
|
+
results.push({ name: pkg.name, from: pkg.version, to: pkg.version, status: 'current' });
|
|
174
|
+
} else {
|
|
175
|
+
log(` ${CYAN}↑${RESET} ${pkg.name} ${DIM}${pkg.version}${RESET} → ${GREEN}${latest}${RESET}`);
|
|
176
|
+
toUpdate.push({ name: pkg.name, npmName: pkg.npmName, from: pkg.version, to: latest });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Update all packages in parallel
|
|
181
|
+
if (toUpdate.length > 0) {
|
|
182
|
+
log(`\n${BOLD}Updating ${toUpdate.length} package${toUpdate.length !== 1 ? 's' : ''} in parallel...${RESET}`);
|
|
183
|
+
|
|
184
|
+
const updatePromises = toUpdate.map(async (pkg) => {
|
|
185
|
+
const success = await updatePackageAsync(pkg.npmName);
|
|
186
|
+
return { ...pkg, success };
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const updateResults = await Promise.all(updatePromises);
|
|
190
|
+
|
|
191
|
+
log('');
|
|
192
|
+
for (const result of updateResults) {
|
|
193
|
+
if (result.success) {
|
|
194
|
+
log(` ${GREEN}✓${RESET} ${result.name} updated`);
|
|
195
|
+
results.push({ name: result.name, from: result.from, to: result.to, status: 'updated' });
|
|
196
|
+
} else {
|
|
197
|
+
log(` ${YELLOW}✗${RESET} ${result.name} failed`);
|
|
198
|
+
results.push({ name: result.name, from: result.from, to: result.to, status: 'failed' });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Summary
|
|
204
|
+
const updated = results.filter(r => r.status === 'updated').length;
|
|
205
|
+
const failed = results.filter(r => r.status === 'failed').length;
|
|
206
|
+
|
|
207
|
+
console.log('');
|
|
208
|
+
if (updated > 0) {
|
|
209
|
+
log(`${GREEN}${updated} package${updated !== 1 ? 's' : ''} updated${RESET}`);
|
|
210
|
+
}
|
|
211
|
+
if (failed > 0) {
|
|
212
|
+
log(`${YELLOW}${failed} package${failed !== 1 ? 's' : ''} failed to update${RESET}`);
|
|
213
|
+
}
|
|
214
|
+
if (updated === 0 && failed === 0) {
|
|
215
|
+
log(`${GREEN}All packages are up to date${RESET}`);
|
|
216
|
+
}
|
|
217
|
+
console.log('');
|
|
218
|
+
}
|