cli4ai 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +39 -0
  2. package/dist/bin.d.ts +6 -0
  3. package/dist/bin.js +105 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +335 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.js +459 -0
  8. package/dist/commands/browse.d.ts +4 -0
  9. package/dist/commands/browse.js +379 -0
  10. package/dist/commands/config.d.ts +10 -0
  11. package/dist/commands/config.js +121 -0
  12. package/dist/commands/info.d.ts +9 -0
  13. package/dist/commands/info.js +122 -0
  14. package/dist/commands/init.d.ts +10 -0
  15. package/dist/commands/init.js +458 -0
  16. package/dist/commands/list.d.ts +10 -0
  17. package/dist/commands/list.js +76 -0
  18. package/dist/commands/mcp-config.d.ts +10 -0
  19. package/dist/commands/mcp-config.js +49 -0
  20. package/dist/commands/remotes.d.ts +22 -0
  21. package/dist/commands/remotes.js +196 -0
  22. package/dist/commands/remove.d.ts +8 -0
  23. package/dist/commands/remove.js +61 -0
  24. package/dist/commands/routines.d.ts +29 -0
  25. package/dist/commands/routines.js +363 -0
  26. package/dist/commands/run.d.ts +12 -0
  27. package/dist/commands/run.js +104 -0
  28. package/dist/commands/scheduler.d.ts +27 -0
  29. package/dist/commands/scheduler.js +350 -0
  30. package/dist/commands/search.d.ts +9 -0
  31. package/dist/commands/search.js +159 -0
  32. package/dist/commands/secrets.d.ts +28 -0
  33. package/dist/commands/secrets.js +236 -0
  34. package/dist/commands/serve.d.ts +13 -0
  35. package/dist/commands/serve.js +49 -0
  36. package/dist/commands/start.d.ts +8 -0
  37. package/dist/commands/start.js +27 -0
  38. package/dist/commands/update.d.ts +17 -0
  39. package/dist/commands/update.js +210 -0
  40. package/dist/core/config.d.ts +91 -0
  41. package/dist/core/config.js +738 -0
  42. package/dist/core/execute.d.ts +51 -0
  43. package/dist/core/execute.js +475 -0
  44. package/dist/core/link.d.ts +39 -0
  45. package/dist/core/link.js +214 -0
  46. package/dist/core/lockfile.d.ts +63 -0
  47. package/dist/core/lockfile.js +140 -0
  48. package/dist/core/manifest.d.ts +96 -0
  49. package/dist/core/manifest.js +224 -0
  50. package/dist/core/registry.d.ts +74 -0
  51. package/dist/core/registry.js +116 -0
  52. package/dist/core/remote-client.d.ts +98 -0
  53. package/dist/core/remote-client.js +252 -0
  54. package/dist/core/remotes.d.ts +88 -0
  55. package/dist/core/remotes.js +206 -0
  56. package/dist/core/routine-engine.d.ts +124 -0
  57. package/dist/core/routine-engine.js +699 -0
  58. package/dist/core/routines.d.ts +36 -0
  59. package/dist/core/routines.js +132 -0
  60. package/dist/core/scheduler-daemon.d.ts +10 -0
  61. package/dist/core/scheduler-daemon.js +77 -0
  62. package/dist/core/scheduler.d.ts +131 -0
  63. package/dist/core/scheduler.js +492 -0
  64. package/dist/core/secrets.d.ts +48 -0
  65. package/dist/core/secrets.js +384 -0
  66. package/dist/lib/cli.d.ts +84 -0
  67. package/dist/lib/cli.js +216 -0
  68. package/dist/mcp/adapter.d.ts +35 -0
  69. package/dist/mcp/adapter.js +94 -0
  70. package/dist/mcp/config-gen.d.ts +31 -0
  71. package/dist/mcp/config-gen.js +75 -0
  72. package/dist/mcp/server.d.ts +41 -0
  73. package/dist/mcp/server.js +296 -0
  74. package/dist/server/service.d.ts +85 -0
  75. package/dist/server/service.js +304 -0
  76. package/package.json +6 -3
  77. package/src/bin.ts +0 -118
  78. package/src/cli.ts +0 -412
  79. package/src/commands/add.ts +0 -562
  80. package/src/commands/browse.ts +0 -449
  81. package/src/commands/config.ts +0 -154
  82. package/src/commands/info.ts +0 -133
  83. package/src/commands/init.ts +0 -514
  84. package/src/commands/list.ts +0 -95
  85. package/src/commands/mcp-config.ts +0 -69
  86. package/src/commands/remotes.ts +0 -253
  87. package/src/commands/remove.ts +0 -78
  88. package/src/commands/routines.ts +0 -427
  89. package/src/commands/run.ts +0 -127
  90. package/src/commands/scheduler.ts +0 -438
  91. package/src/commands/search.ts +0 -185
  92. package/src/commands/secrets.ts +0 -292
  93. package/src/commands/serve.ts +0 -66
  94. package/src/commands/start.ts +0 -40
  95. package/src/commands/update.ts +0 -252
  96. package/src/core/config.ts +0 -845
  97. package/src/core/execute.ts +0 -569
  98. package/src/core/link.ts +0 -246
  99. package/src/core/lockfile.ts +0 -187
  100. package/src/core/manifest.ts +0 -327
  101. package/src/core/registry.ts +0 -165
  102. package/src/core/remote-client.ts +0 -419
  103. package/src/core/remotes.ts +0 -268
  104. package/src/core/routine-engine.ts +0 -895
  105. package/src/core/routines.ts +0 -171
  106. package/src/core/scheduler-daemon.ts +0 -94
  107. package/src/core/scheduler.ts +0 -606
  108. package/src/core/secrets.ts +0 -430
  109. package/src/lib/cli.ts +0 -261
  110. package/src/mcp/adapter.ts +0 -131
  111. package/src/mcp/config-gen.ts +0 -106
  112. package/src/mcp/server.ts +0 -365
  113. package/src/server/service.ts +0 -434
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * cli4ai browse - Interactive package browser
3
+ */
4
+ export declare function browseCommand(): Promise<void>;