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,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai add - Install packages
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, symlinkSync, mkdirSync, cpSync, rmSync, readdirSync, unlinkSync, lstatSync } from 'fs';
|
|
5
|
+
import { resolve, dirname, join, normalize } from 'path';
|
|
6
|
+
import { createInterface } from 'readline';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
8
|
+
import { spawnSync } from 'child_process';
|
|
9
|
+
import { output, outputError, log } from '../lib/cli.js';
|
|
10
|
+
import { loadManifest, tryLoadManifest } from '../core/manifest.js';
|
|
11
|
+
import { ensureCli4aiHome, ensureLocalDir, PACKAGES_DIR, LOCAL_PACKAGES_DIR, loadConfig } from '../core/config.js';
|
|
12
|
+
import { lockPackage } from '../core/lockfile.js';
|
|
13
|
+
import { linkPackageDirect, isBinInPath, getPathInstructions } from '../core/link.js';
|
|
14
|
+
const PKG_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
15
|
+
const CLI4AI_SCOPED_PKG_PATTERN = /^@cli4ai\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
16
|
+
const URL_LIKE_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//;
|
|
17
|
+
function validatePackageSpecifier(pkg) {
|
|
18
|
+
if (URL_LIKE_PATTERN.test(pkg)) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = new URL(pkg);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
outputError('INVALID_INPUT', 'Invalid URL', { url: pkg });
|
|
25
|
+
}
|
|
26
|
+
if (parsed.protocol !== 'https:') {
|
|
27
|
+
outputError('INVALID_INPUT', 'Unsupported URL protocol', {
|
|
28
|
+
url: pkg,
|
|
29
|
+
protocol: parsed.protocol,
|
|
30
|
+
allowed: ['https:']
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
outputError('INVALID_INPUT', 'Installing from URLs is not supported', {
|
|
34
|
+
url: pkg,
|
|
35
|
+
hint: 'Use a local path (./path) or a package name (e.g. github, @cli4ai/github)'
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (pkg.includes('\\') || pkg.includes('..')) {
|
|
39
|
+
outputError('INVALID_INPUT', 'Invalid package specifier', { package: pkg });
|
|
40
|
+
}
|
|
41
|
+
if (pkg.startsWith('@cli4ai/')) {
|
|
42
|
+
if (!CLI4AI_SCOPED_PKG_PATTERN.test(pkg)) {
|
|
43
|
+
outputError('INVALID_INPUT', 'Invalid package name', { package: pkg });
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!PKG_NAME_PATTERN.test(pkg)) {
|
|
48
|
+
outputError('INVALID_INPUT', 'Invalid package name', { package: pkg });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Prompt user for confirmation
|
|
53
|
+
*/
|
|
54
|
+
async function confirm(message) {
|
|
55
|
+
const rl = createInterface({
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
output: process.stderr
|
|
58
|
+
});
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
61
|
+
rl.close();
|
|
62
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate extracted tarball paths to prevent path traversal attacks
|
|
68
|
+
*/
|
|
69
|
+
function validateTarballPaths(extractedDir) {
|
|
70
|
+
function checkPath(dir) {
|
|
71
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const fullPath = join(dir, entry.name);
|
|
74
|
+
const normalizedPath = normalize(fullPath);
|
|
75
|
+
// Ensure path doesn't escape the extraction directory
|
|
76
|
+
if (!normalizedPath.startsWith(normalize(extractedDir))) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
// Check for suspicious names
|
|
80
|
+
if (entry.name.includes('..') || entry.name.startsWith('/')) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
if (!checkPath(fullPath))
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return checkPath(extractedDir);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Download and extract package from npm registry
|
|
94
|
+
*/
|
|
95
|
+
async function downloadFromNpm(packageName, targetDir) {
|
|
96
|
+
const tmpDir = join(tmpdir(), `cli4ai-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
97
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
98
|
+
try {
|
|
99
|
+
// Use npm pack to download the tarball - using spawnSync to prevent command injection
|
|
100
|
+
log(`Downloading ${packageName} from npm...`);
|
|
101
|
+
const packResult = spawnSync('npm', ['pack', packageName, `--pack-destination=${tmpDir}`], {
|
|
102
|
+
stdio: 'pipe',
|
|
103
|
+
cwd: tmpDir,
|
|
104
|
+
encoding: 'utf-8'
|
|
105
|
+
});
|
|
106
|
+
if (packResult.error) {
|
|
107
|
+
throw new Error(`npm not found or failed to execute: ${packResult.error.message}`);
|
|
108
|
+
}
|
|
109
|
+
if (packResult.status !== 0) {
|
|
110
|
+
throw new Error(`npm pack failed: ${packResult.stderr || packResult.stdout || 'unknown error'}`);
|
|
111
|
+
}
|
|
112
|
+
// Find the tarball (it will be named like cli4ai-slack-1.0.2.tgz)
|
|
113
|
+
const files = readdirSync(tmpDir);
|
|
114
|
+
const tarball = files.find(f => f.endsWith('.tgz'));
|
|
115
|
+
if (!tarball) {
|
|
116
|
+
throw new Error('Failed to download package tarball');
|
|
117
|
+
}
|
|
118
|
+
// Extract the tarball using spawnSync to prevent command injection
|
|
119
|
+
// Use --strip-components=1 equivalent by extracting to a subdir
|
|
120
|
+
const tarPath = join(tmpDir, tarball);
|
|
121
|
+
const extractResult = spawnSync('tar', ['-xzf', tarPath, '-C', tmpDir], {
|
|
122
|
+
stdio: 'pipe',
|
|
123
|
+
encoding: 'utf-8'
|
|
124
|
+
});
|
|
125
|
+
if (extractResult.status !== 0) {
|
|
126
|
+
throw new Error(`Failed to extract package: ${extractResult.stderr || 'tar extraction failed'}`);
|
|
127
|
+
}
|
|
128
|
+
// The extracted content is in a 'package' folder
|
|
129
|
+
const extractedPath = join(tmpDir, 'package');
|
|
130
|
+
if (!existsSync(extractedPath)) {
|
|
131
|
+
throw new Error('Failed to extract package');
|
|
132
|
+
}
|
|
133
|
+
// Validate extracted paths to prevent path traversal attacks
|
|
134
|
+
if (!validateTarballPaths(extractedPath)) {
|
|
135
|
+
throw new Error('Package contains suspicious paths - possible path traversal attack');
|
|
136
|
+
}
|
|
137
|
+
// Get package name without scope for target directory
|
|
138
|
+
const shortName = packageName.replace('@cli4ai/', '');
|
|
139
|
+
const pkgTargetDir = join(targetDir, shortName);
|
|
140
|
+
// Remove existing if present
|
|
141
|
+
if (existsSync(pkgTargetDir)) {
|
|
142
|
+
rmSync(pkgTargetDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
// Copy to target directory
|
|
145
|
+
mkdirSync(dirname(pkgTargetDir), { recursive: true });
|
|
146
|
+
cpSync(extractedPath, pkgTargetDir, { recursive: true });
|
|
147
|
+
// Clean up temp directory
|
|
148
|
+
rmSync(tmpDir, { recursive: true });
|
|
149
|
+
return pkgTargetDir;
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
// Clean up on error
|
|
153
|
+
if (existsSync(tmpDir)) {
|
|
154
|
+
rmSync(tmpDir, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check if a CLI tool is available
|
|
161
|
+
*/
|
|
162
|
+
function checkCliTool(name) {
|
|
163
|
+
try {
|
|
164
|
+
const result = spawnSync('which', [name], { stdio: 'pipe' });
|
|
165
|
+
return result.status === 0;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Install npm dependencies
|
|
173
|
+
*/
|
|
174
|
+
async function installNpmDependencies(pkgPath, dependencies) {
|
|
175
|
+
const deps = Object.entries(dependencies).map(([name, version]) => `${name}@${version}`);
|
|
176
|
+
if (deps.length === 0)
|
|
177
|
+
return;
|
|
178
|
+
log(`Installing npm dependencies: ${deps.join(', ')}`);
|
|
179
|
+
const result = spawnSync('npm', ['install', ...deps], {
|
|
180
|
+
cwd: pkgPath,
|
|
181
|
+
stdio: 'inherit'
|
|
182
|
+
});
|
|
183
|
+
if (result.status !== 0) {
|
|
184
|
+
throw new Error(`Failed to install dependencies (exit code: ${result.status})`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Display installation plan
|
|
189
|
+
*/
|
|
190
|
+
function displayInstallPlan(plans) {
|
|
191
|
+
log('\n┌─────────────────────────────────────────────────────┐');
|
|
192
|
+
log('│ INSTALLATION PLAN │');
|
|
193
|
+
log('└─────────────────────────────────────────────────────┘\n');
|
|
194
|
+
for (const plan of plans) {
|
|
195
|
+
log(`📦 ${plan.manifest.name}@${plan.manifest.version}`);
|
|
196
|
+
if (plan.manifest.description) {
|
|
197
|
+
log(` ${plan.manifest.description}`);
|
|
198
|
+
}
|
|
199
|
+
if (plan.npmDependencies.length > 0) {
|
|
200
|
+
log('\n npm dependencies to install:');
|
|
201
|
+
for (const dep of plan.npmDependencies) {
|
|
202
|
+
log(` • ${dep}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (plan.peerDependencies.length > 0) {
|
|
206
|
+
log('\n ⚠️ peer dependencies (you must install these yourself):');
|
|
207
|
+
for (const peer of plan.peerDependencies) {
|
|
208
|
+
log(` • ${peer.name} ${peer.version}`);
|
|
209
|
+
if (peer.description) {
|
|
210
|
+
log(` ${peer.description}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (plan.manifest.env) {
|
|
215
|
+
const requiredEnvs = Object.entries(plan.manifest.env)
|
|
216
|
+
.filter(([_, def]) => def.required)
|
|
217
|
+
.map(([name, def]) => ({ name, ...def }));
|
|
218
|
+
if (requiredEnvs.length > 0) {
|
|
219
|
+
log('\n 🔐 required environment variables:');
|
|
220
|
+
for (const envVar of requiredEnvs) {
|
|
221
|
+
log(` • ${envVar.name}`);
|
|
222
|
+
if (envVar.description) {
|
|
223
|
+
log(` ${envVar.description}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
log('');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
export async function addCommand(packages, options) {
|
|
232
|
+
const results = [];
|
|
233
|
+
const errors = [];
|
|
234
|
+
const targetDir = options.global ? PACKAGES_DIR : resolve(process.cwd(), LOCAL_PACKAGES_DIR);
|
|
235
|
+
// Ensure target directory exists
|
|
236
|
+
if (options.global) {
|
|
237
|
+
ensureCli4aiHome();
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
ensureLocalDir(process.cwd());
|
|
241
|
+
}
|
|
242
|
+
const projectDir = process.cwd();
|
|
243
|
+
// Build installation plans
|
|
244
|
+
const plans = [];
|
|
245
|
+
for (const pkg of packages) {
|
|
246
|
+
try {
|
|
247
|
+
const { manifest, path: pkgPath, fromNpm } = await resolvePackage(pkg, options, targetDir);
|
|
248
|
+
// Build npm dependencies list
|
|
249
|
+
const npmDependencies = [];
|
|
250
|
+
if (manifest.dependencies) {
|
|
251
|
+
for (const [name, version] of Object.entries(manifest.dependencies)) {
|
|
252
|
+
npmDependencies.push(`${name}@${version}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Build peer dependencies list
|
|
256
|
+
const peerDependencies = [];
|
|
257
|
+
if (manifest.peerDependencies) {
|
|
258
|
+
for (const [name, version] of Object.entries(manifest.peerDependencies)) {
|
|
259
|
+
peerDependencies.push({ name, version, description: String(version) });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
plans.push({
|
|
263
|
+
package: pkg,
|
|
264
|
+
manifest,
|
|
265
|
+
path: pkgPath,
|
|
266
|
+
npmDependencies,
|
|
267
|
+
peerDependencies,
|
|
268
|
+
fromNpm
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
errors.push({
|
|
273
|
+
package: pkg,
|
|
274
|
+
error: err instanceof Error ? err.message : String(err)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (plans.length === 0) {
|
|
279
|
+
if (errors.length > 0) {
|
|
280
|
+
outputError('INSTALL_ERROR', 'Failed to resolve packages', { errors });
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// Display installation plan
|
|
285
|
+
displayInstallPlan(plans);
|
|
286
|
+
// Check peer dependencies availability
|
|
287
|
+
const peerWarnings = [];
|
|
288
|
+
for (const plan of plans) {
|
|
289
|
+
for (const peer of plan.peerDependencies) {
|
|
290
|
+
// Check if it's a CLI tool (not a cli4ai package reference)
|
|
291
|
+
if (!peer.version.includes('cli4ai')) {
|
|
292
|
+
const available = checkCliTool(peer.name);
|
|
293
|
+
if (!available) {
|
|
294
|
+
peerWarnings.push(`${peer.name} is not installed on your system`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (peerWarnings.length > 0) {
|
|
300
|
+
log('⚠️ Warning: Some peer dependencies are missing:');
|
|
301
|
+
for (const warning of peerWarnings) {
|
|
302
|
+
log(` • ${warning}`);
|
|
303
|
+
}
|
|
304
|
+
log('');
|
|
305
|
+
}
|
|
306
|
+
// Ask for confirmation unless --yes flag is provided
|
|
307
|
+
if (!options.yes) {
|
|
308
|
+
const confirmed = await confirm('Proceed with installation?');
|
|
309
|
+
if (!confirmed) {
|
|
310
|
+
log('Installation cancelled.');
|
|
311
|
+
process.exit(0);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Proceed with installation
|
|
315
|
+
for (const plan of plans) {
|
|
316
|
+
try {
|
|
317
|
+
let result;
|
|
318
|
+
if (plan.fromNpm) {
|
|
319
|
+
// Package already downloaded to target directory
|
|
320
|
+
result = {
|
|
321
|
+
name: plan.manifest.name,
|
|
322
|
+
version: plan.manifest.version,
|
|
323
|
+
path: plan.path,
|
|
324
|
+
source: 'registry'
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
// Local package - need to symlink/copy
|
|
329
|
+
result = await installPackage(plan.path, targetDir, plan.manifest, options);
|
|
330
|
+
}
|
|
331
|
+
// Install npm dependencies
|
|
332
|
+
if (plan.manifest.dependencies && Object.keys(plan.manifest.dependencies).length > 0) {
|
|
333
|
+
await installNpmDependencies(result.path, plan.manifest.dependencies);
|
|
334
|
+
}
|
|
335
|
+
// Link to PATH for global installs
|
|
336
|
+
if (options.global) {
|
|
337
|
+
result.binPath = linkPackageDirect(plan.manifest, result.path);
|
|
338
|
+
log(`✓ ${result.name}@${result.version} (linked to ${result.binPath})`);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
log(`✓ ${result.name}@${result.version}`);
|
|
342
|
+
// Update lockfile (only for local/project installs)
|
|
343
|
+
const lockedPkg = {
|
|
344
|
+
name: result.name,
|
|
345
|
+
version: result.version,
|
|
346
|
+
resolved: result.source === 'local' ? result.path : result.path
|
|
347
|
+
};
|
|
348
|
+
lockPackage(projectDir, lockedPkg);
|
|
349
|
+
}
|
|
350
|
+
result.dependencies = plan.manifest.dependencies;
|
|
351
|
+
result.peerDependencies = plan.manifest.peerDependencies;
|
|
352
|
+
results.push(result);
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
errors.push({
|
|
356
|
+
package: plan.package,
|
|
357
|
+
error: err instanceof Error ? err.message : String(err)
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (errors.length > 0 && results.length === 0) {
|
|
362
|
+
outputError('INSTALL_ERROR', 'Failed to install packages', { errors });
|
|
363
|
+
}
|
|
364
|
+
// Show PATH instructions for global installs if not already in PATH
|
|
365
|
+
if (options.global && results.length > 0 && !isBinInPath()) {
|
|
366
|
+
console.error('\n' + getPathInstructions() + '\n');
|
|
367
|
+
}
|
|
368
|
+
output({
|
|
369
|
+
installed: results,
|
|
370
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
371
|
+
location: options.global ? 'global' : 'local'
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
async function resolvePackage(pkg, options, targetDir) {
|
|
375
|
+
// Check if it's a local path
|
|
376
|
+
if (options.local || pkg.startsWith('./') || pkg.startsWith('/') || pkg.startsWith('../')) {
|
|
377
|
+
const absolutePath = resolve(pkg);
|
|
378
|
+
if (!existsSync(absolutePath)) {
|
|
379
|
+
throw new Error(`Path does not exist: ${absolutePath}`);
|
|
380
|
+
}
|
|
381
|
+
const manifest = loadManifest(absolutePath);
|
|
382
|
+
return { manifest, path: absolutePath };
|
|
383
|
+
}
|
|
384
|
+
validatePackageSpecifier(pkg);
|
|
385
|
+
// Check local registries first
|
|
386
|
+
const config = loadConfig();
|
|
387
|
+
for (const registryPath of config.localRegistries) {
|
|
388
|
+
const pkgPath = resolve(registryPath, pkg);
|
|
389
|
+
const manifest = tryLoadManifest(pkgPath);
|
|
390
|
+
if (manifest) {
|
|
391
|
+
return { manifest, path: pkgPath };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Check if it's a @cli4ai package - download from npm
|
|
395
|
+
if (pkg.startsWith('@cli4ai/')) {
|
|
396
|
+
try {
|
|
397
|
+
const pkgPath = await downloadFromNpm(pkg, targetDir);
|
|
398
|
+
const manifest = loadManifest(pkgPath);
|
|
399
|
+
return { manifest, path: pkgPath, fromNpm: true };
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
outputError('NPM_ERROR', `Failed to download ${pkg} from npm`, {
|
|
403
|
+
hint: err instanceof Error ? err.message : String(err)
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Auto-resolve to @cli4ai/ scope and try npm
|
|
408
|
+
const scopedName = `@cli4ai/${pkg}`;
|
|
409
|
+
let npmError;
|
|
410
|
+
try {
|
|
411
|
+
log(`Resolving ${pkg} as ${scopedName}...`);
|
|
412
|
+
const pkgPath = await downloadFromNpm(scopedName, targetDir);
|
|
413
|
+
const manifest = loadManifest(pkgPath);
|
|
414
|
+
return { manifest, path: pkgPath, fromNpm: true };
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
npmError = err instanceof Error ? err.message : String(err);
|
|
418
|
+
}
|
|
419
|
+
outputError('NOT_FOUND', `Package not found: ${pkg}`, {
|
|
420
|
+
hint: `Tried @cli4ai/${pkg} on npm. Use --local flag for local paths, or add a local registry with "cli4ai config --add-registry <path>"`,
|
|
421
|
+
npmError
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
async function installPackage(sourcePath, targetDir, manifest, options) {
|
|
425
|
+
// Create target path
|
|
426
|
+
const pkgDir = resolve(targetDir, manifest.name);
|
|
427
|
+
// Remove existing if present
|
|
428
|
+
if (existsSync(pkgDir)) {
|
|
429
|
+
try {
|
|
430
|
+
const stat = lstatSync(pkgDir);
|
|
431
|
+
if (stat.isSymbolicLink()) {
|
|
432
|
+
unlinkSync(pkgDir);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
rmSync(pkgDir, { recursive: true });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
throw new Error(`Failed to remove existing package at ${pkgDir}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Ensure parent directory exists
|
|
443
|
+
mkdirSync(dirname(pkgDir), { recursive: true });
|
|
444
|
+
// Create symlink to source
|
|
445
|
+
try {
|
|
446
|
+
symlinkSync(sourcePath, pkgDir, 'dir');
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
// If symlink fails (Windows?), copy instead
|
|
450
|
+
log(`Symlink failed, copying instead...`);
|
|
451
|
+
cpSync(sourcePath, pkgDir, { recursive: true });
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
name: manifest.name,
|
|
455
|
+
version: manifest.version,
|
|
456
|
+
path: pkgDir,
|
|
457
|
+
source: 'local'
|
|
458
|
+
};
|
|
459
|
+
}
|