@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.
- package/LICENSE +21 -0
- package/README.md +21 -218
- package/package.json +32 -35
- package/pave.js +3 -0
- package/sandbox/SandboxRunner.js +1 -0
- package/sandbox/pave-run.js +2 -0
- package/sandbox/permission.js +1 -0
- package/sandbox/utils/yaml.js +1 -0
- package/MARKETPLACE.md +0 -406
- package/build-binary.js +0 -591
- package/build-npm.js +0 -537
- package/build.js +0 -230
- package/check-binary.js +0 -26
- package/deploy.sh +0 -95
- package/index.js +0 -5775
- package/lib/agent-registry.js +0 -1037
- package/lib/args-parser.js +0 -837
- package/lib/blessed-widget-patched.js +0 -93
- package/lib/cli-markdown.js +0 -590
- package/lib/compaction.js +0 -153
- package/lib/duration.js +0 -94
- package/lib/hash.js +0 -22
- package/lib/marketplace.js +0 -866
- package/lib/memory-config.js +0 -166
- package/lib/skill-manager.js +0 -891
- package/lib/soul.js +0 -31
- package/lib/tool-output-formatter.js +0 -180
- package/start-pave.sh +0 -149
- package/status.js +0 -271
- package/test/abort-stream.test.js +0 -445
- package/test/agent-auto-compaction.test.js +0 -552
- package/test/agent-comm-abort.test.js +0 -95
- package/test/agent-comm.test.js +0 -598
- package/test/agent-inbox.test.js +0 -576
- package/test/agent-init.test.js +0 -264
- package/test/agent-interrupt.test.js +0 -314
- package/test/agent-lifecycle.test.js +0 -520
- package/test/agent-log-files.test.js +0 -349
- package/test/agent-mode.manual-test.js +0 -392
- package/test/agent-parsing.test.js +0 -228
- package/test/agent-post-stream-idle.test.js +0 -762
- package/test/agent-registry.test.js +0 -359
- package/test/agent-rm.test.js +0 -442
- package/test/agent-spawn.test.js +0 -933
- package/test/agent-status-api.test.js +0 -624
- package/test/agent-update.test.js +0 -435
- package/test/args-parser.test.js +0 -391
- package/test/auto-compaction-chat.manual-test.js +0 -227
- package/test/auto-compaction.test.js +0 -941
- package/test/build-config.test.js +0 -120
- package/test/build-npm.test.js +0 -388
- package/test/chat-command.test.js +0 -137
- package/test/chat-leading-lines.test.js +0 -159
- package/test/config-flag.test.js +0 -272
- package/test/cursor-drift.test.js +0 -135
- package/test/debug-require.js +0 -23
- package/test/dir-migration.test.js +0 -323
- package/test/duration.test.js +0 -229
- package/test/ghostty-term.test.js +0 -202
- package/test/http500-backoff.test.js +0 -854
- package/test/integration.test.js +0 -86
- package/test/memory-guard-env.test.js +0 -220
- package/test/pr233-fixes.test.js +0 -259
- package/test/run-agent-init.js +0 -297
- package/test/run-all.js +0 -64
- package/test/run-config-flag.js +0 -159
- package/test/run-cursor-drift.js +0 -82
- package/test/run-session-path.js +0 -154
- package/test/run-tests.js +0 -643
- package/test/sandbox-redirect.test.js +0 -202
- package/test/session-path.test.js +0 -132
- package/test/shebang-strip.test.js +0 -241
- package/test/soul-reinject.test.js +0 -1027
- package/test/soul-reread.test.js +0 -281
- package/test/tool-output-formatter.test.js +0 -486
- package/test/tool-output-gating.test.js +0 -143
- package/test/tool-states.test.js +0 -167
- package/test/tools-flag.test.js +0 -65
- package/test/tui-attach.test.js +0 -1255
- package/test/tui-compaction.test.js +0 -354
- package/test/tui-wrap.test.js +0 -568
- package/test-binary.js +0 -52
- package/test-binary2.js +0 -36
package/lib/skill-manager.js
DELETED
|
@@ -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
|
-
};
|