@cnrai/pave 0.3.34 → 0.3.50

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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -218
  3. package/package.json +32 -35
  4. package/pave.js +3 -0
  5. package/sandbox/SandboxRunner.js +1 -0
  6. package/sandbox/pave-run.js +2 -0
  7. package/sandbox/permission.js +1 -0
  8. package/sandbox/utils/yaml.js +1 -0
  9. package/MARKETPLACE.md +0 -406
  10. package/build-binary.js +0 -591
  11. package/build-npm.js +0 -537
  12. package/build.js +0 -230
  13. package/check-binary.js +0 -26
  14. package/deploy.sh +0 -95
  15. package/index.js +0 -5775
  16. package/lib/agent-registry.js +0 -1037
  17. package/lib/args-parser.js +0 -837
  18. package/lib/blessed-widget-patched.js +0 -93
  19. package/lib/cli-markdown.js +0 -590
  20. package/lib/compaction.js +0 -153
  21. package/lib/duration.js +0 -94
  22. package/lib/hash.js +0 -22
  23. package/lib/marketplace.js +0 -866
  24. package/lib/memory-config.js +0 -166
  25. package/lib/skill-manager.js +0 -891
  26. package/lib/soul.js +0 -31
  27. package/lib/tool-output-formatter.js +0 -180
  28. package/start-pave.sh +0 -149
  29. package/status.js +0 -271
  30. package/test/abort-stream.test.js +0 -445
  31. package/test/agent-auto-compaction.test.js +0 -552
  32. package/test/agent-comm-abort.test.js +0 -95
  33. package/test/agent-comm.test.js +0 -598
  34. package/test/agent-inbox.test.js +0 -576
  35. package/test/agent-init.test.js +0 -264
  36. package/test/agent-interrupt.test.js +0 -314
  37. package/test/agent-lifecycle.test.js +0 -520
  38. package/test/agent-log-files.test.js +0 -349
  39. package/test/agent-mode.manual-test.js +0 -392
  40. package/test/agent-parsing.test.js +0 -228
  41. package/test/agent-post-stream-idle.test.js +0 -762
  42. package/test/agent-registry.test.js +0 -359
  43. package/test/agent-rm.test.js +0 -442
  44. package/test/agent-spawn.test.js +0 -933
  45. package/test/agent-status-api.test.js +0 -624
  46. package/test/agent-update.test.js +0 -435
  47. package/test/args-parser.test.js +0 -391
  48. package/test/auto-compaction-chat.manual-test.js +0 -227
  49. package/test/auto-compaction.test.js +0 -941
  50. package/test/build-config.test.js +0 -120
  51. package/test/build-npm.test.js +0 -388
  52. package/test/chat-command.test.js +0 -137
  53. package/test/chat-leading-lines.test.js +0 -159
  54. package/test/config-flag.test.js +0 -272
  55. package/test/cursor-drift.test.js +0 -135
  56. package/test/debug-require.js +0 -23
  57. package/test/dir-migration.test.js +0 -323
  58. package/test/duration.test.js +0 -229
  59. package/test/ghostty-term.test.js +0 -202
  60. package/test/http500-backoff.test.js +0 -854
  61. package/test/integration.test.js +0 -86
  62. package/test/memory-guard-env.test.js +0 -220
  63. package/test/pr233-fixes.test.js +0 -259
  64. package/test/run-agent-init.js +0 -297
  65. package/test/run-all.js +0 -64
  66. package/test/run-config-flag.js +0 -159
  67. package/test/run-cursor-drift.js +0 -82
  68. package/test/run-session-path.js +0 -154
  69. package/test/run-tests.js +0 -643
  70. package/test/sandbox-redirect.test.js +0 -202
  71. package/test/session-path.test.js +0 -132
  72. package/test/shebang-strip.test.js +0 -241
  73. package/test/soul-reinject.test.js +0 -1027
  74. package/test/soul-reread.test.js +0 -281
  75. package/test/tool-output-formatter.test.js +0 -486
  76. package/test/tool-output-gating.test.js +0 -143
  77. package/test/tool-states.test.js +0 -167
  78. package/test/tools-flag.test.js +0 -65
  79. package/test/tui-attach.test.js +0 -1255
  80. package/test/tui-compaction.test.js +0 -354
  81. package/test/tui-wrap.test.js +0 -568
  82. package/test-binary.js +0 -52
  83. package/test-binary2.js +0 -36
@@ -1,891 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * PAVE Skill Manager
4
- * Install, list, and remove skills from the OpenPave Skill Marketplace
5
- *
6
- * Supports:
7
- * - Local paths: pave install /path/to/skill
8
- * - GitHub repos: pave install github:user/repo
9
- * - GitHub URLs: pave install https://github.com/user/repo
10
- * - Short form: pave install user/repo (assumes GitHub)
11
- *
12
- * Node 16 compatible - runs on iSH iOS
13
- */
14
-
15
- const fs = require('fs');
16
- const path = require('path');
17
- const { execSync } = require('child_process');
18
- const os = require('os');
19
- const yaml = require('js-yaml');
20
-
21
- // Default paths (can be overridden with custom config)
22
- const DEFAULT_HOME_DIR = process.env.HOME || '/root';
23
- const DEFAULT_PAVE_DIR = path.join(DEFAULT_HOME_DIR, '.pave');
24
- const TEMP_DIR = path.join(os.tmpdir(), 'pave-install');
25
-
26
- // Module-level config (can be set via setPaveHome)
27
- let PAVE_HOME = DEFAULT_PAVE_DIR;
28
-
29
- /**
30
- * Set the PAVE home directory
31
- * @param {string|null} configPath - Custom .pave directory path, or null for default
32
- * @throws {Error} If path is invalid (empty string after resolution)
33
- */
34
- function setPaveHome(configPath) {
35
- if (configPath) {
36
- const resolved = path.resolve(configPath);
37
- // Validate that path.resolve() produced a valid path
38
- if (!resolved || resolved === '' || resolved === '/') {
39
- throw new Error(`Invalid config path: "${configPath}" resolves to "${resolved}"`);
40
- }
41
- PAVE_HOME = resolved;
42
- } else {
43
- PAVE_HOME = DEFAULT_PAVE_DIR;
44
- }
45
- }
46
-
47
- /**
48
- * Get current PAVE home directory
49
- * @returns {string}
50
- */
51
- function getPaveHome() {
52
- return PAVE_HOME;
53
- }
54
-
55
- /**
56
- * Get paths based on current PAVE_HOME
57
- */
58
- function getPaths() {
59
- return {
60
- skillsDir: path.join(PAVE_HOME, 'skills'),
61
- lockFile: path.join(PAVE_HOME, 'skills.lock.json'),
62
- permissionsFile: path.join(PAVE_HOME, 'permissions.yaml'),
63
- };
64
- }
65
-
66
- const _HOME_DIR = DEFAULT_HOME_DIR;
67
-
68
- /**
69
- * Parse a source string to determine if it's a GitHub repo
70
- * Supports:
71
- * - github:user/repo
72
- * - https://github.com/user/repo
73
- * - https://github.com/user/repo.git
74
- * - user/repo (short form, assumes GitHub)
75
- *
76
- * @param {string} source - The source string
77
- * @returns {Object|null} - { type: 'github', owner, repo, url } or null if not GitHub
78
- */
79
- function parseGitHubSource(source) {
80
- // github:user/repo
81
- if (source.startsWith('github:')) {
82
- const parts = source.slice(7).split('/');
83
- if (parts.length === 2) {
84
- return {
85
- type: 'github',
86
- owner: parts[0],
87
- repo: parts[1].replace(/\.git$/, ''),
88
- url: `https://github.com/${parts[0]}/${parts[1].replace(/\.git$/, '')}.git`,
89
- };
90
- }
91
- }
92
-
93
- // https://github.com/user/repo
94
- const githubUrlMatch = source.match(/^https?:\/\/github\.com\/([^\/]+)\/([^\/]+?)(\.git)?$/);
95
- if (githubUrlMatch) {
96
- return {
97
- type: 'github',
98
- owner: githubUrlMatch[1],
99
- repo: githubUrlMatch[2],
100
- url: `https://github.com/${githubUrlMatch[1]}/${githubUrlMatch[2]}.git`,
101
- };
102
- }
103
-
104
- // user/repo short form (must be exactly user/repo, no slashes in names)
105
- if (/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/.test(source)) {
106
- const parts = source.split('/');
107
- return {
108
- type: 'github',
109
- owner: parts[0],
110
- repo: parts[1],
111
- url: `https://github.com/${parts[0]}/${parts[1]}.git`,
112
- };
113
- }
114
-
115
- return null;
116
- }
117
-
118
- /**
119
- * Clone a GitHub repository to a temp directory
120
- * @param {Object} githubInfo - { owner, repo, url }
121
- * @param {Object} options - { verbose, branch }
122
- * @returns {string} - Path to cloned directory
123
- */
124
- function cloneFromGitHub(githubInfo, options = {}) {
125
- const { verbose = false, branch = null } = options;
126
-
127
- // Create temp directory
128
- ensureDir(TEMP_DIR);
129
- const cloneDir = path.join(TEMP_DIR, `${githubInfo.owner}-${githubInfo.repo}-${Date.now()}`);
130
-
131
- // Clean up if exists
132
- if (fs.existsSync(cloneDir)) {
133
- removeDir(cloneDir);
134
- }
135
-
136
- // Build git clone command
137
- let gitCmd = `git clone --depth 1`;
138
- if (branch) {
139
- gitCmd += ` --branch ${branch}`;
140
- }
141
- gitCmd += ` ${githubInfo.url} "${cloneDir}"`;
142
-
143
- if (verbose) {
144
- console.log(`Cloning from ${githubInfo.url}...`);
145
- }
146
-
147
- try {
148
- execSync(gitCmd, {
149
- stdio: verbose ? 'inherit' : 'pipe',
150
- timeout: 60000, // 60 second timeout
151
- });
152
- } catch (error) {
153
- // Try to provide helpful error message
154
- if (error.message.includes('not found') || error.status === 128) {
155
- throw new Error(`Repository not found: ${githubInfo.url}`);
156
- }
157
- throw new Error(`Failed to clone: ${error.message}`);
158
- }
159
-
160
- // Verify clone succeeded
161
- if (!fs.existsSync(cloneDir)) {
162
- throw new Error(`Clone failed: directory not created`);
163
- }
164
-
165
- // Verify skill.yaml or skill.json exists
166
- const hasYaml = fs.existsSync(path.join(cloneDir, 'skill.yaml'));
167
- const hasJson = fs.existsSync(path.join(cloneDir, 'skill.json'));
168
- if (!hasYaml && !hasJson) {
169
- removeDir(cloneDir);
170
- throw new Error(`Not a valid skill: no skill.yaml or skill.json found in ${githubInfo.owner}/${githubInfo.repo}`);
171
- }
172
-
173
- return cloneDir;
174
- }
175
-
176
- /**
177
- * Clean up temp directories
178
- */
179
- function cleanupTemp() {
180
- try {
181
- if (fs.existsSync(TEMP_DIR)) {
182
- removeDir(TEMP_DIR);
183
- }
184
- } catch (e) {
185
- // Ignore cleanup errors
186
- }
187
- }
188
-
189
- /**
190
- * Ensure a directory exists
191
- */
192
- function ensureDir(dirPath) {
193
- if (!fs.existsSync(dirPath)) {
194
- fs.mkdirSync(dirPath, { recursive: true });
195
- }
196
- }
197
-
198
- /**
199
- * Read the lock file
200
- */
201
- function readLockFile() {
202
- const { lockFile } = getPaths();
203
- try {
204
- if (fs.existsSync(lockFile)) {
205
- return JSON.parse(fs.readFileSync(lockFile, 'utf8'));
206
- }
207
- } catch (e) {
208
- // Ignore errors, return empty
209
- }
210
- return { version: 1, skills: {} };
211
- }
212
-
213
- /**
214
- * Write the lock file
215
- */
216
- function writeLockFile(lockData) {
217
- const { lockFile } = getPaths();
218
- ensureDir(path.dirname(lockFile));
219
- fs.writeFileSync(lockFile, JSON.stringify(lockData, null, 2));
220
- }
221
-
222
- /**
223
- * Read a skill.yaml or skill.json from a directory
224
- * Prefers skill.yaml, falls back to skill.json for backwards compatibility
225
- */
226
- function readSkillManifest(skillPath) {
227
- const yamlPath = path.join(skillPath, 'skill.yaml');
228
- const jsonPath = path.join(skillPath, 'skill.json');
229
-
230
- // Prefer YAML
231
- if (fs.existsSync(yamlPath)) {
232
- try {
233
- const content = fs.readFileSync(yamlPath, 'utf8');
234
- return yaml.load(content);
235
- } catch (e) {
236
- throw new Error(`Invalid skill.yaml: ${e.message}`);
237
- }
238
- }
239
-
240
- // Fall back to JSON for backwards compatibility
241
- if (fs.existsSync(jsonPath)) {
242
- try {
243
- return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
244
- } catch (e) {
245
- throw new Error(`Invalid skill.json: ${e.message}`);
246
- }
247
- }
248
-
249
- throw new Error(`No skill.yaml or skill.json found in ${skillPath}`);
250
- }
251
-
252
- /**
253
- * Validate a skill manifest
254
- */
255
- function validateManifest(manifest) {
256
- const required = ['name', 'version', 'entrypoint'];
257
- const missing = required.filter((f) => !manifest[f]);
258
-
259
- if (missing.length > 0) {
260
- throw new Error(`skill.json missing required fields: ${missing.join(', ')}`);
261
- }
262
-
263
- // Validate name (alphanumeric, hyphens only)
264
- if (!/^[a-z0-9-]+$/.test(manifest.name)) {
265
- throw new Error(`Invalid skill name "${manifest.name}": must be lowercase alphanumeric with hyphens`);
266
- }
267
-
268
- // Validate version (semver-ish)
269
- if (!/^\d+\.\d+\.\d+/.test(manifest.version)) {
270
- throw new Error(`Invalid version "${manifest.version}": must be semver format (e.g., 1.0.0)`);
271
- }
272
-
273
- return true;
274
- }
275
-
276
- /**
277
- * Copy a directory recursively
278
- * Falls back to `cp -a` on iSH/Alpine where copyFileSync may fail with EPERM
279
- */
280
- function copyDir(src, dest) {
281
- ensureDir(dest);
282
-
283
- // Try native copy first, fall back to cp -a if EPERM
284
- try {
285
- copyDirNative(src, dest);
286
- } catch (error) {
287
- if (error.code === 'EPERM') {
288
- // iSH workaround: use cp -a command
289
- copyDirWithCp(src, dest);
290
- } else {
291
- throw error;
292
- }
293
- }
294
- }
295
-
296
- /**
297
- * Native Node.js directory copy
298
- */
299
- function copyDirNative(src, dest) {
300
- ensureDir(dest);
301
- const entries = fs.readdirSync(src, { withFileTypes: true });
302
-
303
- for (const entry of entries) {
304
- const srcPath = path.join(src, entry.name);
305
- const destPath = path.join(dest, entry.name);
306
-
307
- // Skip node_modules, .git, etc.
308
- if (['node_modules', '.git', '.DS_Store'].includes(entry.name)) {
309
- continue;
310
- }
311
-
312
- if (entry.isDirectory()) {
313
- copyDirNative(srcPath, destPath);
314
- } else {
315
- fs.copyFileSync(srcPath, destPath);
316
- }
317
- }
318
- }
319
-
320
- /**
321
- * Copy directory using cp -a command (iSH workaround)
322
- * Excludes node_modules and .git
323
- */
324
- function copyDirWithCp(src, dest) {
325
- ensureDir(dest);
326
-
327
- // Get list of items to copy (excluding node_modules, .git, .DS_Store)
328
- const entries = fs.readdirSync(src);
329
- const itemsToCopy = entries.filter((name) =>
330
- !['node_modules', '.git', '.DS_Store'].includes(name),
331
- );
332
-
333
- if (itemsToCopy.length === 0) {
334
- return;
335
- }
336
-
337
- // Copy each item using cp -a
338
- for (const item of itemsToCopy) {
339
- const srcPath = path.join(src, item);
340
- const destPath = path.join(dest, item);
341
-
342
- try {
343
- // Use cp -a for full copy (preserves attributes, handles all file types)
344
- execSync(`cp -a "${srcPath}" "${destPath}"`, { stdio: 'pipe' });
345
- } catch (error) {
346
- throw new Error(`Failed to copy ${item}: ${error.message}`);
347
- }
348
- }
349
- }
350
-
351
- /**
352
- * Remove a directory recursively
353
- */
354
- function removeDir(dirPath) {
355
- if (!fs.existsSync(dirPath)) return;
356
-
357
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
358
- for (const entry of entries) {
359
- const fullPath = path.join(dirPath, entry.name);
360
- if (entry.isDirectory()) {
361
- removeDir(fullPath);
362
- } else {
363
- fs.unlinkSync(fullPath);
364
- }
365
- }
366
- fs.rmdirSync(dirPath);
367
- }
368
-
369
- /**
370
- * Install a skill from a local directory, GitHub, or marketplace
371
- * @param {string} source - Local path, GitHub URL/shorthand, or skill name from marketplace
372
- * @param {Object} options - { force, verbose, branch }
373
- */
374
- async function installSkill(source, options = {}) {
375
- const { force = false, verbose = false, branch = null } = options;
376
-
377
- let absPath;
378
- let sourceType = 'local';
379
- let githubInfo = null;
380
-
381
- // Check if it's a GitHub source
382
- githubInfo = parseGitHubSource(source);
383
-
384
- if (githubInfo) {
385
- sourceType = 'github';
386
- if (verbose) {
387
- console.log(`Detected GitHub source: ${githubInfo.owner}/${githubInfo.repo}`);
388
- }
389
-
390
- // Clone from GitHub
391
- absPath = cloneFromGitHub(githubInfo, { verbose, branch });
392
- } else if (fs.existsSync(path.resolve(source))) {
393
- // Local path exists
394
- absPath = path.resolve(source);
395
-
396
- if (!fs.statSync(absPath).isDirectory()) {
397
- throw new Error(`Not a directory: ${absPath}`);
398
- }
399
- } else if (/^[a-z0-9-]+$/.test(source)) {
400
- // Looks like a skill name - try marketplace lookup
401
- sourceType = 'marketplace';
402
- if (verbose) {
403
- console.log(`Looking up "${source}" in marketplace...`);
404
- }
405
-
406
- // Import marketplace module
407
- const marketplace = require('./marketplace');
408
-
409
- try {
410
- const skillInfo = await marketplace.lookupSkill(source, { verbose });
411
-
412
- if (!skillInfo) {
413
- throw new Error(
414
- `Skill "${source}" not found in marketplace.\n` +
415
- ` Try: pave search ${source}\n` +
416
- ` Or install from GitHub: pave install owner/repo`,
417
- );
418
- }
419
-
420
- if (verbose) {
421
- console.log(`Found in marketplace: ${skillInfo.repository}`);
422
- }
423
-
424
- // Parse the repository as GitHub source
425
- githubInfo = parseGitHubSource(skillInfo.repository);
426
-
427
- if (!githubInfo) {
428
- throw new Error(`Invalid repository in marketplace: ${skillInfo.repository}`);
429
- }
430
-
431
- sourceType = 'github';
432
-
433
- // Clone from GitHub
434
- absPath = cloneFromGitHub(githubInfo, { verbose, branch });
435
- } catch (e) {
436
- if (e.message.includes('not found in marketplace')) {
437
- throw e;
438
- }
439
- throw new Error(`Failed to lookup in marketplace: ${e.message}`);
440
- }
441
- } else {
442
- // Not a valid path, GitHub source, or skill name
443
- throw new Error(
444
- `Invalid source: "${source}"\n` +
445
- ` Use one of:\n` +
446
- ` pave install <skill-name> (from marketplace)\n` +
447
- ` pave install <owner/repo> (from GitHub)\n` +
448
- ` pave install <local-path> (from local directory)`,
449
- );
450
- }
451
-
452
- // Read and validate manifest
453
- const manifest = readSkillManifest(absPath);
454
- validateManifest(manifest);
455
-
456
- const skillName = manifest.name;
457
- const skillVersion = manifest.version;
458
- const { skillsDir } = getPaths();
459
- const destPath = path.join(skillsDir, skillName);
460
-
461
- if (verbose) {
462
- console.log(`Installing ${skillName}@${skillVersion}${sourceType === 'github' ? ` from ${githubInfo.owner}/${githubInfo.repo}` : ''}`);
463
- }
464
-
465
- // Check if already installed
466
- const lockData = readLockFile();
467
- const existing = lockData.skills[skillName];
468
-
469
- if (existing && !force) {
470
- // Cleanup temp if from GitHub
471
- if (sourceType === 'github') {
472
- cleanupTemp();
473
- }
474
- throw new Error(
475
- `Skill "${skillName}" already installed (v${existing.version}). ` +
476
- `Use --force to overwrite.`,
477
- );
478
- }
479
-
480
- // Remove existing if force
481
- if (existing && force) {
482
- if (verbose) {
483
- console.log(`Removing existing ${skillName}@${existing.version}`);
484
- }
485
- removeDir(destPath);
486
- }
487
-
488
- // Copy skill files
489
- if (verbose) {
490
- console.log(`Copying files to ${destPath}`);
491
- }
492
- copyDir(absPath, destPath);
493
-
494
- // Determine source string for lock file
495
- const sourceString = sourceType === 'github'
496
- ? `github:${githubInfo.owner}/${githubInfo.repo}`
497
- : absPath;
498
-
499
- // Update lock file
500
- lockData.skills[skillName] = {
501
- version: skillVersion,
502
- source: sourceString,
503
- sourceType,
504
- installedAt: new Date().toISOString(),
505
- manifest: {
506
- name: manifest.name,
507
- version: manifest.version,
508
- description: manifest.description || '',
509
- entrypoint: manifest.entrypoint,
510
- commands: manifest.commands || [],
511
- pattern: manifest.pattern || 'sandbox',
512
- },
513
- };
514
-
515
- // Add GitHub info if applicable
516
- if (sourceType === 'github') {
517
- lockData.skills[skillName].github = {
518
- owner: githubInfo.owner,
519
- repo: githubInfo.repo,
520
- url: githubInfo.url,
521
- };
522
- }
523
-
524
- writeLockFile(lockData);
525
-
526
- // Cleanup temp directory if from GitHub
527
- if (sourceType === 'github') {
528
- cleanupTemp();
529
- }
530
-
531
- // Return installation info
532
- return {
533
- name: skillName,
534
- version: skillVersion,
535
- path: destPath,
536
- source: sourceString,
537
- sourceType,
538
- commands: manifest.commands || [],
539
- tokens: manifest.tokens || {},
540
- };
541
- }
542
-
543
- /**
544
- * List installed skills
545
- */
546
- function listSkills(options = {}) {
547
- const { json = false } = options;
548
- const lockData = readLockFile();
549
- const { skillsDir } = getPaths();
550
-
551
- const skills = Object.entries(lockData.skills).map(([name, info]) => {
552
- // Handle commands as either array or object format
553
- let commandNames = [];
554
- const commands = info.manifest?.commands;
555
- if (Array.isArray(commands)) {
556
- commandNames = commands.map((c) => c.name);
557
- } else if (commands && typeof commands === 'object') {
558
- // Object format: { "post": {...}, "get": {...} } -> ["post", "get"]
559
- commandNames = Object.keys(commands);
560
- }
561
-
562
- return {
563
- name,
564
- version: info.version,
565
- description: info.manifest?.description || '',
566
- commands: commandNames,
567
- installedAt: info.installedAt,
568
- path: path.join(skillsDir, name),
569
- };
570
- });
571
-
572
- if (json) {
573
- return skills;
574
- }
575
-
576
- return skills;
577
- }
578
-
579
- /**
580
- * Remove an installed skill
581
- */
582
- function removeSkill(skillName, options = {}) {
583
- const { verbose = false } = options;
584
-
585
- const lockData = readLockFile();
586
- const existing = lockData.skills[skillName];
587
-
588
- if (!existing) {
589
- throw new Error(`Skill "${skillName}" is not installed`);
590
- }
591
-
592
- const { skillsDir } = getPaths();
593
- const skillPath = path.join(skillsDir, skillName);
594
-
595
- if (verbose) {
596
- console.log(`Removing ${skillName}@${existing.version} from ${skillPath}`);
597
- }
598
-
599
- // Remove the skill directory
600
- removeDir(skillPath);
601
-
602
- // Update lock file
603
- delete lockData.skills[skillName];
604
- writeLockFile(lockData);
605
-
606
- return {
607
- name: skillName,
608
- version: existing.version,
609
- removed: true,
610
- };
611
- }
612
-
613
- /**
614
- * Get info about a skill (installed or from path)
615
- */
616
- function getSkillInfo(nameOrPath, options = {}) {
617
- // Check if it's an installed skill name
618
- const lockData = readLockFile();
619
- const { skillsDir } = getPaths();
620
-
621
- if (lockData.skills[nameOrPath]) {
622
- const info = lockData.skills[nameOrPath];
623
- const skillPath = path.join(skillsDir, nameOrPath);
624
-
625
- // Read full manifest from installed location
626
- let fullManifest = info.manifest;
627
- try {
628
- fullManifest = readSkillManifest(skillPath);
629
- } catch (e) {
630
- // Use cached manifest from lock file
631
- }
632
-
633
- return {
634
- installed: true,
635
- name: nameOrPath,
636
- version: info.version,
637
- path: skillPath,
638
- source: info.source,
639
- installedAt: info.installedAt,
640
- manifest: fullManifest,
641
- };
642
- }
643
-
644
- // Check if it's a path
645
- const absPath = path.resolve(nameOrPath);
646
- if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) {
647
- try {
648
- const manifest = readSkillManifest(absPath);
649
- return {
650
- installed: false,
651
- name: manifest.name,
652
- version: manifest.version,
653
- path: absPath,
654
- manifest,
655
- };
656
- } catch (e) {
657
- throw new Error(`Not a valid skill: ${e.message}`);
658
- }
659
- }
660
-
661
- throw new Error(`Skill "${nameOrPath}" not found (not installed and not a valid path)`);
662
- }
663
-
664
- /**
665
- * Get the script path and command for running a skill
666
- * Returns { scriptPath, skillArgs } for use with pave-run or sandbox
667
- */
668
- function getSkillCommand(skillName, commandArgs = []) {
669
- const lockData = readLockFile();
670
-
671
- if (!lockData.skills[skillName]) {
672
- throw new Error(`Skill "${skillName}" is not installed. Install with: pave install <path>`);
673
- }
674
-
675
- const info = lockData.skills[skillName];
676
- const { skillsDir } = getPaths();
677
- const skillPath = path.join(skillsDir, skillName);
678
-
679
- // Get entrypoint from manifest
680
- let entrypoint = 'index.js';
681
- try {
682
- const manifest = readSkillManifest(skillPath);
683
- entrypoint = manifest.entrypoint || 'index.js';
684
- } catch (e) {
685
- // Use default
686
- }
687
-
688
- const scriptPath = path.join(skillPath, entrypoint);
689
-
690
- if (!fs.existsSync(scriptPath)) {
691
- throw new Error(`Skill entrypoint not found: ${scriptPath}`);
692
- }
693
-
694
- return {
695
- skillName,
696
- version: info.version,
697
- scriptPath,
698
- skillPath,
699
- skillArgs: commandArgs,
700
- };
701
- }
702
-
703
- /**
704
- * Update an installed skill to the latest version
705
- * @param {string} skillName - Name of installed skill
706
- * @param {Object} options - { force, verbose, checkOnly }
707
- * @returns {Object} - Update result
708
- */
709
- async function updateSkill(skillName, options = {}) {
710
- const { verbose = false, checkOnly = false } = options;
711
-
712
- const lockData = readLockFile();
713
- const existing = lockData.skills[skillName];
714
-
715
- if (!existing) {
716
- throw new Error(`Skill "${skillName}" is not installed`);
717
- }
718
-
719
- // Need GitHub source to update
720
- if (existing.sourceType !== 'github' && !existing.github) {
721
- throw new Error(
722
- `Cannot update "${skillName}": installed from local path.\n` +
723
- ` Source: ${existing.source}\n` +
724
- ` Reinstall from GitHub to enable updates.`,
725
- );
726
- }
727
-
728
- const githubInfo = existing.github || parseGitHubSource(existing.source);
729
-
730
- if (!githubInfo) {
731
- throw new Error(`Cannot determine GitHub source for "${skillName}"`);
732
- }
733
-
734
- if (verbose) {
735
- console.log(`Checking ${githubInfo.owner}/${githubInfo.repo} for updates...`);
736
- }
737
-
738
- // Clone to temp to check version
739
- let tempPath;
740
- try {
741
- tempPath = cloneFromGitHub(githubInfo, { verbose: false });
742
- } catch (e) {
743
- throw new Error(`Failed to fetch latest version: ${e.message}`);
744
- }
745
-
746
- // Read new manifest
747
- let newManifest;
748
- try {
749
- newManifest = readSkillManifest(tempPath);
750
- } catch (e) {
751
- cleanupTemp();
752
- throw new Error(`Failed to read updated manifest: ${e.message}`);
753
- }
754
-
755
- const currentVersion = existing.version;
756
- const latestVersion = newManifest.version;
757
-
758
- // Compare versions
759
- const hasUpdate = latestVersion !== currentVersion;
760
-
761
- if (checkOnly) {
762
- cleanupTemp();
763
- return {
764
- name: skillName,
765
- currentVersion,
766
- latestVersion,
767
- hasUpdate,
768
- repository: `${githubInfo.owner}/${githubInfo.repo}`,
769
- };
770
- }
771
-
772
- if (!hasUpdate) {
773
- cleanupTemp();
774
- if (verbose) {
775
- console.log(`${skillName} is already at latest version (${currentVersion})`);
776
- }
777
- return {
778
- name: skillName,
779
- currentVersion,
780
- latestVersion,
781
- hasUpdate: false,
782
- updated: false,
783
- };
784
- }
785
-
786
- if (verbose) {
787
- console.log(`Updating ${skillName}: ${currentVersion} � ${latestVersion}`);
788
- }
789
-
790
- // Remove old version
791
- const { skillsDir } = getPaths();
792
- const destPath = path.join(skillsDir, skillName);
793
- removeDir(destPath);
794
-
795
- // Copy new version
796
- copyDir(tempPath, destPath);
797
-
798
- // Update lock file
799
- lockData.skills[skillName] = {
800
- version: latestVersion,
801
- source: `github:${githubInfo.owner}/${githubInfo.repo}`,
802
- sourceType: 'github',
803
- installedAt: new Date().toISOString(),
804
- updatedAt: new Date().toISOString(),
805
- previousVersion: currentVersion,
806
- manifest: {
807
- name: newManifest.name,
808
- version: newManifest.version,
809
- description: newManifest.description || '',
810
- entrypoint: newManifest.entrypoint,
811
- commands: newManifest.commands || [],
812
- pattern: newManifest.pattern || 'sandbox',
813
- },
814
- github: {
815
- owner: githubInfo.owner,
816
- repo: githubInfo.repo,
817
- url: githubInfo.url,
818
- },
819
- };
820
-
821
- writeLockFile(lockData);
822
- cleanupTemp();
823
-
824
- return {
825
- name: skillName,
826
- currentVersion,
827
- latestVersion,
828
- hasUpdate: true,
829
- updated: true,
830
- path: destPath,
831
- };
832
- }
833
-
834
- /**
835
- * Check for updates on all installed skills
836
- * @param {Object} options - { verbose }
837
- * @returns {Array} - Skills with updates available
838
- */
839
- async function checkAllUpdates(options = {}) {
840
- const { verbose = false } = options;
841
-
842
- const lockData = readLockFile();
843
- const results = [];
844
-
845
- for (const [name, info] of Object.entries(lockData.skills)) {
846
- // Skip local installs
847
- if (info.sourceType !== 'github' && !info.github) {
848
- if (verbose) {
849
- console.log(`Skipping ${name}: installed from local path`);
850
- }
851
- continue;
852
- }
853
-
854
- try {
855
- const result = await updateSkill(name, { ...options, checkOnly: true });
856
- results.push(result);
857
- } catch (e) {
858
- if (verbose) {
859
- console.log(`Error checking ${name}: ${e.message}`);
860
- }
861
- results.push({
862
- name,
863
- error: e.message,
864
- });
865
- }
866
- }
867
-
868
- return results;
869
- }
870
-
871
- // Export functions
872
- module.exports = {
873
- installSkill,
874
- listSkills,
875
- removeSkill,
876
- getSkillInfo,
877
- getSkillCommand,
878
- updateSkill,
879
- checkAllUpdates,
880
- readSkillManifest,
881
- validateManifest,
882
- parseGitHubSource,
883
- cleanupTemp,
884
- // Config path management
885
- setPaveHome,
886
- getPaveHome,
887
- getPaths,
888
- // Deprecated: use getPaths() instead. Kept for backward compatibility.
889
- get SKILLS_DIR() { return getPaths().skillsDir; },
890
- get LOCK_FILE() { return getPaths().lockFile; },
891
- };