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