@cnrai/pave 0.3.35 → 0.3.51
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 -5776
- 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/marketplace.js
DELETED
|
@@ -1,866 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* OpenPave Skill Marketplace
|
|
4
|
-
* Fetch, search, and publish skills to the marketplace registry
|
|
5
|
-
*
|
|
6
|
-
* Node 16 compatible - runs on iSH iOS
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const https = require('https');
|
|
10
|
-
const http = require('http');
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const yaml = require('js-yaml');
|
|
14
|
-
|
|
15
|
-
// Default paths (can be overridden with custom config)
|
|
16
|
-
const DEFAULT_HOME_DIR = process.env.HOME || '/root';
|
|
17
|
-
const DEFAULT_PAVE_DIR = path.join(DEFAULT_HOME_DIR, '.pave');
|
|
18
|
-
|
|
19
|
-
// Module-level config (can be set via setPaveHome)
|
|
20
|
-
let PAVE_HOME = DEFAULT_PAVE_DIR;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Set the PAVE home directory
|
|
24
|
-
* @param {string|null} configPath - Custom .pave directory path, or null for default
|
|
25
|
-
* @throws {Error} If path is invalid (empty string after resolution)
|
|
26
|
-
*/
|
|
27
|
-
function setPaveHome(configPath) {
|
|
28
|
-
if (configPath) {
|
|
29
|
-
const resolved = path.resolve(configPath);
|
|
30
|
-
// Validate that path.resolve() produced a valid path
|
|
31
|
-
if (!resolved || resolved === '' || resolved === '/') {
|
|
32
|
-
throw new Error(`Invalid config path: "${configPath}" resolves to "${resolved}"`);
|
|
33
|
-
}
|
|
34
|
-
PAVE_HOME = resolved;
|
|
35
|
-
} else {
|
|
36
|
-
PAVE_HOME = DEFAULT_PAVE_DIR;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Get current PAVE home directory
|
|
42
|
-
* @returns {string}
|
|
43
|
-
*/
|
|
44
|
-
function getPaveHome() {
|
|
45
|
-
return PAVE_HOME;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get cache paths based on current PAVE_HOME
|
|
50
|
-
*/
|
|
51
|
-
function getCachePaths() {
|
|
52
|
-
const cacheDir = path.join(PAVE_HOME, 'cache');
|
|
53
|
-
return {
|
|
54
|
-
cacheDir,
|
|
55
|
-
cacheFile: path.join(cacheDir, 'registry.yaml'),
|
|
56
|
-
cacheMetaFile: path.join(cacheDir, 'registry.meta.json'),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const _HOME_DIR = DEFAULT_HOME_DIR;
|
|
61
|
-
|
|
62
|
-
// Default registry URL
|
|
63
|
-
const DEFAULT_REGISTRY_URL = 'https://raw.githubusercontent.com/cnrai/openpave-marketplace/main/registry.yaml';
|
|
64
|
-
|
|
65
|
-
// Cache TTL in milliseconds (1 hour default)
|
|
66
|
-
const _DEFAULT_CACHE_TTL = parseInt(process.env.PAVE_CACHE_TTL || '3600', 10) * 1000;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Ensure a directory exists
|
|
70
|
-
*/
|
|
71
|
-
function ensureDir(dirPath) {
|
|
72
|
-
if (!fs.existsSync(dirPath)) {
|
|
73
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get the registry URL from environment or default
|
|
79
|
-
*/
|
|
80
|
-
function getRegistryUrl() {
|
|
81
|
-
return process.env.PAVE_REGISTRY_URL || DEFAULT_REGISTRY_URL;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Check if we should use a local registry file
|
|
86
|
-
*/
|
|
87
|
-
function getLocalRegistryFile() {
|
|
88
|
-
return process.env.PAVE_REGISTRY_FILE || null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Read cache metadata
|
|
93
|
-
*/
|
|
94
|
-
function readCacheMeta() {
|
|
95
|
-
const { cacheMetaFile } = getCachePaths();
|
|
96
|
-
try {
|
|
97
|
-
if (fs.existsSync(cacheMetaFile)) {
|
|
98
|
-
return JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8'));
|
|
99
|
-
}
|
|
100
|
-
} catch (e) {
|
|
101
|
-
// Ignore errors
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Write cache metadata
|
|
108
|
-
*/
|
|
109
|
-
function writeCacheMeta(meta) {
|
|
110
|
-
const { cacheDir, cacheMetaFile } = getCachePaths();
|
|
111
|
-
ensureDir(cacheDir);
|
|
112
|
-
fs.writeFileSync(cacheMetaFile, JSON.stringify(meta, null, 2));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Check if cache is still valid
|
|
117
|
-
*/
|
|
118
|
-
function isCacheValid() {
|
|
119
|
-
const meta = readCacheMeta();
|
|
120
|
-
if (!meta || !meta.timestamp) return false;
|
|
121
|
-
|
|
122
|
-
const age = Date.now() - meta.timestamp;
|
|
123
|
-
const ttl = parseInt(process.env.PAVE_CACHE_TTL || '3600', 10) * 1000;
|
|
124
|
-
const { cacheFile } = getCachePaths();
|
|
125
|
-
|
|
126
|
-
return age < ttl && fs.existsSync(cacheFile);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Read cached registry
|
|
131
|
-
*/
|
|
132
|
-
function readCachedRegistry() {
|
|
133
|
-
const { cacheFile } = getCachePaths();
|
|
134
|
-
try {
|
|
135
|
-
if (fs.existsSync(cacheFile)) {
|
|
136
|
-
const content = fs.readFileSync(cacheFile, 'utf8');
|
|
137
|
-
return yaml.load(content);
|
|
138
|
-
}
|
|
139
|
-
} catch (e) {
|
|
140
|
-
// Ignore errors
|
|
141
|
-
}
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Write registry to cache
|
|
147
|
-
*/
|
|
148
|
-
function writeRegistryCache(content) {
|
|
149
|
-
const { cacheDir, cacheFile } = getCachePaths();
|
|
150
|
-
ensureDir(cacheDir);
|
|
151
|
-
fs.writeFileSync(cacheFile, content);
|
|
152
|
-
writeCacheMeta({
|
|
153
|
-
timestamp: Date.now(),
|
|
154
|
-
url: getRegistryUrl(),
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Fetch URL content (Node 16 compatible, no fetch API)
|
|
160
|
-
* @param {string} url - URL to fetch
|
|
161
|
-
* @returns {Promise<string>} - Response body
|
|
162
|
-
*/
|
|
163
|
-
function fetchUrl(url) {
|
|
164
|
-
return new Promise((resolve, reject) => {
|
|
165
|
-
const client = url.startsWith('https') ? https : http;
|
|
166
|
-
|
|
167
|
-
const request = client.get(url, {
|
|
168
|
-
headers: {
|
|
169
|
-
'User-Agent': 'pave-cli/1.0',
|
|
170
|
-
},
|
|
171
|
-
timeout: 30000,
|
|
172
|
-
}, (response) => {
|
|
173
|
-
// Handle redirects
|
|
174
|
-
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
175
|
-
fetchUrl(response.headers.location).then(resolve).catch(reject);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (response.statusCode !== 200) {
|
|
180
|
-
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let data = '';
|
|
185
|
-
response.on('data', (chunk) => { data += chunk; });
|
|
186
|
-
response.on('end', () => resolve(data));
|
|
187
|
-
response.on('error', reject);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
request.on('error', reject);
|
|
191
|
-
request.on('timeout', () => {
|
|
192
|
-
request.destroy();
|
|
193
|
-
reject(new Error('Request timeout'));
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Fetch the marketplace registry
|
|
200
|
-
* Uses cache if available and not expired
|
|
201
|
-
*
|
|
202
|
-
* @param {Object} options - { force: boolean, verbose: boolean }
|
|
203
|
-
* @returns {Promise<Object>} - Registry object
|
|
204
|
-
*/
|
|
205
|
-
async function fetchRegistry(options = {}) {
|
|
206
|
-
const { force = false, verbose = false } = options;
|
|
207
|
-
|
|
208
|
-
// Check for local file override
|
|
209
|
-
const localFile = getLocalRegistryFile();
|
|
210
|
-
if (localFile) {
|
|
211
|
-
if (verbose) {
|
|
212
|
-
console.log(`Using local registry file: ${localFile}`);
|
|
213
|
-
}
|
|
214
|
-
try {
|
|
215
|
-
const content = fs.readFileSync(localFile, 'utf8');
|
|
216
|
-
return yaml.load(content);
|
|
217
|
-
} catch (e) {
|
|
218
|
-
throw new Error(`Failed to read local registry: ${e.message}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Check cache unless force refresh
|
|
223
|
-
if (!force && isCacheValid()) {
|
|
224
|
-
if (verbose) {
|
|
225
|
-
console.log('Using cached registry');
|
|
226
|
-
}
|
|
227
|
-
const cached = readCachedRegistry();
|
|
228
|
-
if (cached) {
|
|
229
|
-
return cached;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Fetch from remote
|
|
234
|
-
const url = getRegistryUrl();
|
|
235
|
-
if (verbose) {
|
|
236
|
-
console.log(`Fetching registry from ${url}`);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
const content = await fetchUrl(url);
|
|
241
|
-
|
|
242
|
-
// Parse and validate
|
|
243
|
-
const registry = yaml.load(content);
|
|
244
|
-
if (!registry || !registry.skills) {
|
|
245
|
-
throw new Error('Invalid registry format: missing skills');
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Cache the result
|
|
249
|
-
writeRegistryCache(content);
|
|
250
|
-
|
|
251
|
-
if (verbose) {
|
|
252
|
-
const skillCount = Object.keys(registry.skills).length;
|
|
253
|
-
console.log(`Registry loaded: ${skillCount} skills available`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return registry;
|
|
257
|
-
} catch (e) {
|
|
258
|
-
// Try to use cache as fallback
|
|
259
|
-
const cached = readCachedRegistry();
|
|
260
|
-
if (cached) {
|
|
261
|
-
if (verbose) {
|
|
262
|
-
console.log(`Network error, using cached registry: ${e.message}`);
|
|
263
|
-
}
|
|
264
|
-
return cached;
|
|
265
|
-
}
|
|
266
|
-
throw new Error(`Failed to fetch registry: ${e.message}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Search skills in the registry
|
|
272
|
-
*
|
|
273
|
-
* @param {string} query - Search query (name, description, keywords)
|
|
274
|
-
* @param {Object} options - { all: boolean, category: string, verbose: boolean }
|
|
275
|
-
* @returns {Promise<Array>} - Matching skills
|
|
276
|
-
*/
|
|
277
|
-
async function searchSkills(query, options = {}) {
|
|
278
|
-
const { all = false, category = null, verbose = false } = options;
|
|
279
|
-
|
|
280
|
-
const registry = await fetchRegistry({ verbose });
|
|
281
|
-
const skills = Object.values(registry.skills || {});
|
|
282
|
-
|
|
283
|
-
// Return all skills if --all flag
|
|
284
|
-
if (all || !query) {
|
|
285
|
-
let results = skills;
|
|
286
|
-
|
|
287
|
-
// Filter by category if specified
|
|
288
|
-
if (category) {
|
|
289
|
-
results = results.filter((s) => s.category === category);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Search by query
|
|
296
|
-
const queryLower = query.toLowerCase();
|
|
297
|
-
|
|
298
|
-
const results = skills.filter((skill) => {
|
|
299
|
-
// Match name
|
|
300
|
-
if (skill.name.toLowerCase().includes(queryLower)) {
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Match description
|
|
305
|
-
if (skill.description && skill.description.toLowerCase().includes(queryLower)) {
|
|
306
|
-
return true;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Match keywords
|
|
310
|
-
if (skill.keywords && Array.isArray(skill.keywords)) {
|
|
311
|
-
for (const keyword of skill.keywords) {
|
|
312
|
-
if (keyword.toLowerCase().includes(queryLower)) {
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Match category
|
|
319
|
-
if (skill.category && skill.category.toLowerCase().includes(queryLower)) {
|
|
320
|
-
return true;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Match author
|
|
324
|
-
if (skill.author && skill.author.toLowerCase().includes(queryLower)) {
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return false;
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// Filter by category if specified
|
|
332
|
-
if (category) {
|
|
333
|
-
return results.filter((s) => s.category === category);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Sort by relevance (exact name match first, then alphabetical)
|
|
337
|
-
return results.sort((a, b) => {
|
|
338
|
-
const aExact = a.name.toLowerCase() === queryLower;
|
|
339
|
-
const bExact = b.name.toLowerCase() === queryLower;
|
|
340
|
-
if (aExact && !bExact) return -1;
|
|
341
|
-
if (!aExact && bExact) return 1;
|
|
342
|
-
return a.name.localeCompare(b.name);
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Lookup a specific skill in the registry
|
|
348
|
-
*
|
|
349
|
-
* @param {string} name - Skill name
|
|
350
|
-
* @param {Object} options - { verbose: boolean }
|
|
351
|
-
* @returns {Promise<Object|null>} - Skill metadata or null
|
|
352
|
-
*/
|
|
353
|
-
async function lookupSkill(name, options = {}) {
|
|
354
|
-
const { verbose = false } = options;
|
|
355
|
-
|
|
356
|
-
const registry = await fetchRegistry({ verbose });
|
|
357
|
-
|
|
358
|
-
if (registry.skills && registry.skills[name]) {
|
|
359
|
-
return registry.skills[name];
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return null;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Validate a skill directory for publishing
|
|
367
|
-
*
|
|
368
|
-
* @param {string} skillPath - Path to skill directory
|
|
369
|
-
* @returns {Object} - { valid: boolean, errors: string[], manifest: Object }
|
|
370
|
-
*/
|
|
371
|
-
function validateSkillForPublish(skillPath) {
|
|
372
|
-
const errors = [];
|
|
373
|
-
let manifest = null;
|
|
374
|
-
|
|
375
|
-
// Check path exists
|
|
376
|
-
if (!fs.existsSync(skillPath)) {
|
|
377
|
-
errors.push(`Path not found: ${skillPath}`);
|
|
378
|
-
return { valid: false, errors, manifest: null };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Check skill.yaml exists
|
|
382
|
-
const yamlPath = path.join(skillPath, 'skill.yaml');
|
|
383
|
-
const jsonPath = path.join(skillPath, 'skill.json');
|
|
384
|
-
|
|
385
|
-
if (fs.existsSync(yamlPath)) {
|
|
386
|
-
try {
|
|
387
|
-
manifest = yaml.load(fs.readFileSync(yamlPath, 'utf8'));
|
|
388
|
-
} catch (e) {
|
|
389
|
-
errors.push(`Invalid skill.yaml: ${e.message}`);
|
|
390
|
-
}
|
|
391
|
-
} else if (fs.existsSync(jsonPath)) {
|
|
392
|
-
try {
|
|
393
|
-
manifest = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
394
|
-
} catch (e) {
|
|
395
|
-
errors.push(`Invalid skill.json: ${e.message}`);
|
|
396
|
-
}
|
|
397
|
-
} else {
|
|
398
|
-
errors.push('Missing skill.yaml or skill.json');
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (!manifest) {
|
|
402
|
-
return { valid: false, errors, manifest: null };
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Validate required fields
|
|
406
|
-
const required = ['name', 'version', 'description', 'entrypoint'];
|
|
407
|
-
for (const field of required) {
|
|
408
|
-
if (!manifest[field]) {
|
|
409
|
-
errors.push(`Missing required field: ${field}`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Validate name format
|
|
414
|
-
if (manifest.name && !/^[a-z0-9-]+$/.test(manifest.name)) {
|
|
415
|
-
errors.push(`Invalid skill name "${manifest.name}": must be lowercase alphanumeric with hyphens`);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Validate version format
|
|
419
|
-
if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) {
|
|
420
|
-
errors.push(`Invalid version "${manifest.version}": must be semver format`);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Check entrypoint exists
|
|
424
|
-
if (manifest.entrypoint) {
|
|
425
|
-
const entrypointPath = path.join(skillPath, manifest.entrypoint);
|
|
426
|
-
if (!fs.existsSync(entrypointPath)) {
|
|
427
|
-
errors.push(`Entrypoint not found: ${manifest.entrypoint}`);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Check for README
|
|
432
|
-
const readmePath = path.join(skillPath, 'README.md');
|
|
433
|
-
if (!fs.existsSync(readmePath)) {
|
|
434
|
-
errors.push('Missing README.md (recommended for publishing)');
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Check for LICENSE
|
|
438
|
-
const licensePath = path.join(skillPath, 'LICENSE');
|
|
439
|
-
if (!fs.existsSync(licensePath)) {
|
|
440
|
-
errors.push('Missing LICENSE file (recommended for publishing)');
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Validate repository field
|
|
444
|
-
if (!manifest.repository) {
|
|
445
|
-
errors.push('Missing repository field (required for publishing)');
|
|
446
|
-
} else if (!/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/.test(manifest.repository)) {
|
|
447
|
-
errors.push(`Invalid repository format "${manifest.repository}": should be owner/repo`);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Check for category
|
|
451
|
-
const validCategories = ['communication', 'storage', 'search', 'productivity', 'development', 'other'];
|
|
452
|
-
if (!manifest.category) {
|
|
453
|
-
errors.push('Missing category field');
|
|
454
|
-
} else if (!validCategories.includes(manifest.category)) {
|
|
455
|
-
errors.push(`Invalid category "${manifest.category}": must be one of ${validCategories.join(', ')}`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
valid: errors.length === 0,
|
|
460
|
-
errors,
|
|
461
|
-
manifest,
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Generate registry entry from skill manifest
|
|
467
|
-
*
|
|
468
|
-
* @param {Object} manifest - Skill manifest
|
|
469
|
-
* @returns {Object} - Registry entry
|
|
470
|
-
*/
|
|
471
|
-
function generateRegistryEntry(manifest) {
|
|
472
|
-
return {
|
|
473
|
-
name: manifest.name,
|
|
474
|
-
version: manifest.version,
|
|
475
|
-
description: manifest.description,
|
|
476
|
-
author: manifest.author?.name || manifest.repository?.split('/')[0] || 'unknown',
|
|
477
|
-
repository: manifest.repository,
|
|
478
|
-
category: manifest.category || 'other',
|
|
479
|
-
icon: manifest.icon || null,
|
|
480
|
-
keywords: manifest.keywords || [],
|
|
481
|
-
published: new Date(), // Use Date object directly for unquoted YAML output
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Prepare a skill for publishing
|
|
487
|
-
* Returns the registry entry YAML that would be added
|
|
488
|
-
*
|
|
489
|
-
* @param {string} skillPath - Path to skill directory
|
|
490
|
-
* @param {Object} options - { verbose: boolean }
|
|
491
|
-
* @returns {Object} - { valid: boolean, errors: string[], entry: Object, yaml: string }
|
|
492
|
-
*/
|
|
493
|
-
function prepareForPublish(skillPath, options = {}) {
|
|
494
|
-
const { _verbose = false } = options;
|
|
495
|
-
|
|
496
|
-
const absPath = path.resolve(skillPath);
|
|
497
|
-
const validation = validateSkillForPublish(absPath);
|
|
498
|
-
|
|
499
|
-
if (!validation.valid) {
|
|
500
|
-
return {
|
|
501
|
-
valid: false,
|
|
502
|
-
errors: validation.errors,
|
|
503
|
-
entry: null,
|
|
504
|
-
yaml: null,
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const entry = generateRegistryEntry(validation.manifest);
|
|
509
|
-
|
|
510
|
-
// Generate YAML snippet for the registry
|
|
511
|
-
// Note: Don't use quotingType to avoid quoting timestamps
|
|
512
|
-
const yamlEntry = yaml.dump({ [entry.name]: entry }, {
|
|
513
|
-
indent: 2,
|
|
514
|
-
lineWidth: 120,
|
|
515
|
-
forceQuotes: false,
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
return {
|
|
519
|
-
valid: true,
|
|
520
|
-
errors: [],
|
|
521
|
-
entry,
|
|
522
|
-
yaml: yamlEntry,
|
|
523
|
-
manifest: validation.manifest,
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Get instructions for manual publishing
|
|
529
|
-
*
|
|
530
|
-
* @param {Object} publishInfo - Result from prepareForPublish
|
|
531
|
-
* @returns {string} - Instructions text
|
|
532
|
-
*/
|
|
533
|
-
function getPublishInstructions(publishInfo) {
|
|
534
|
-
if (!publishInfo.valid) {
|
|
535
|
-
return `Cannot publish: fix the following errors first:\n${publishInfo.errors.map((e) => ` - ${e}`).join('\n')}`;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const entry = publishInfo.entry;
|
|
539
|
-
|
|
540
|
-
return `
|
|
541
|
-
To publish "${entry.name}" to the OpenPave Marketplace:
|
|
542
|
-
|
|
543
|
-
1. Ensure your repository is public: https://github.com/${entry.repository}
|
|
544
|
-
|
|
545
|
-
2. Fork the marketplace repo:
|
|
546
|
-
https://github.com/cnrai/openpave-marketplace
|
|
547
|
-
|
|
548
|
-
3. Add your skill to registry.yaml under the "skills:" section:
|
|
549
|
-
|
|
550
|
-
${publishInfo.yaml}
|
|
551
|
-
|
|
552
|
-
4. Create a pull request with title:
|
|
553
|
-
"Add skill: ${entry.name}"
|
|
554
|
-
|
|
555
|
-
5. Wait for review and approval from maintainers.
|
|
556
|
-
|
|
557
|
-
Alternatively, run "pave publish --create-pr" to automatically create the PR (requires gh CLI).
|
|
558
|
-
`.trim();
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Clear the registry cache
|
|
563
|
-
*/
|
|
564
|
-
function clearCache() {
|
|
565
|
-
const { cacheFile, cacheMetaFile } = getCachePaths();
|
|
566
|
-
try {
|
|
567
|
-
if (fs.existsSync(cacheFile)) {
|
|
568
|
-
fs.unlinkSync(cacheFile);
|
|
569
|
-
}
|
|
570
|
-
if (fs.existsSync(cacheMetaFile)) {
|
|
571
|
-
fs.unlinkSync(cacheMetaFile);
|
|
572
|
-
}
|
|
573
|
-
return true;
|
|
574
|
-
} catch (e) {
|
|
575
|
-
return false;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Get cache info
|
|
581
|
-
*/
|
|
582
|
-
function getCacheInfo() {
|
|
583
|
-
const meta = readCacheMeta();
|
|
584
|
-
if (!meta) {
|
|
585
|
-
return { cached: false };
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const { cacheFile } = getCachePaths();
|
|
589
|
-
return {
|
|
590
|
-
cached: true,
|
|
591
|
-
timestamp: meta.timestamp,
|
|
592
|
-
age: Date.now() - meta.timestamp,
|
|
593
|
-
url: meta.url,
|
|
594
|
-
valid: isCacheValid(),
|
|
595
|
-
file: cacheFile,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Create a PR to add or upgrade a skill in the marketplace registry
|
|
601
|
-
* Uses gh CLI to fork, create branch, commit, and open PR
|
|
602
|
-
*
|
|
603
|
-
* @param {Object} publishInfo - Result from prepareForPublish
|
|
604
|
-
* @param {Object} options - { verbose: boolean, upgrade: boolean }
|
|
605
|
-
* @returns {Promise<Object>} - { success: boolean, prUrl: string, error: string }
|
|
606
|
-
*/
|
|
607
|
-
async function createPublishPR(publishInfo, options = {}) {
|
|
608
|
-
const { verbose = false, upgrade = false } = options;
|
|
609
|
-
const { execSync } = require('child_process');
|
|
610
|
-
|
|
611
|
-
if (!publishInfo.valid) {
|
|
612
|
-
return {
|
|
613
|
-
success: false,
|
|
614
|
-
error: `Cannot publish: ${publishInfo.errors.join(', ')}`,
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const entry = publishInfo.entry;
|
|
619
|
-
const skillName = entry.name;
|
|
620
|
-
const branchName = upgrade ? `upgrade-skill-${skillName}` : `add-skill-${skillName}`;
|
|
621
|
-
|
|
622
|
-
// Check if gh CLI is available
|
|
623
|
-
try {
|
|
624
|
-
execSync('gh --version', { stdio: 'pipe' });
|
|
625
|
-
} catch (e) {
|
|
626
|
-
return {
|
|
627
|
-
success: false,
|
|
628
|
-
error: 'GitHub CLI (gh) is not installed. Install from https://cli.github.com/',
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Check if gh is authenticated
|
|
633
|
-
try {
|
|
634
|
-
execSync('gh auth status', { stdio: 'pipe' });
|
|
635
|
-
} catch (e) {
|
|
636
|
-
return {
|
|
637
|
-
success: false,
|
|
638
|
-
error: 'GitHub CLI is not authenticated. Run: gh auth login',
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// Check if skill repo exists and is accessible
|
|
643
|
-
try {
|
|
644
|
-
if (verbose) console.log(`Checking repository: ${entry.repository}`);
|
|
645
|
-
execSync(`gh repo view ${entry.repository}`, { stdio: 'pipe' });
|
|
646
|
-
} catch (e) {
|
|
647
|
-
return {
|
|
648
|
-
success: false,
|
|
649
|
-
error: `Repository not found or not accessible: ${entry.repository}\nMake sure the repo exists and is public.`,
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const tempDir = path.join(require('os').tmpdir(), `pave-publish-${Date.now()}`);
|
|
654
|
-
|
|
655
|
-
try {
|
|
656
|
-
// Clone the marketplace repo
|
|
657
|
-
if (verbose) console.log('Cloning marketplace repository...');
|
|
658
|
-
execSync(`git clone --depth 1 https://github.com/cnrai/openpave-marketplace.git "${tempDir}"`, {
|
|
659
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
// Read current registry
|
|
663
|
-
const registryPath = path.join(tempDir, 'registry.yaml');
|
|
664
|
-
const registryContent = fs.readFileSync(registryPath, 'utf8');
|
|
665
|
-
const registry = yaml.load(registryContent);
|
|
666
|
-
|
|
667
|
-
// Check if skill already exists
|
|
668
|
-
const skillExists = registry.skills && registry.skills[skillName];
|
|
669
|
-
|
|
670
|
-
if (skillExists && !upgrade) {
|
|
671
|
-
// Clean up
|
|
672
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
673
|
-
return {
|
|
674
|
-
success: false,
|
|
675
|
-
error: `Skill "${skillName}" already exists in the marketplace registry. Use --upgrade to update it.`,
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (!skillExists && upgrade) {
|
|
680
|
-
// Clean up
|
|
681
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
682
|
-
return {
|
|
683
|
-
success: false,
|
|
684
|
-
error: `Skill "${skillName}" does not exist in the marketplace registry. Remove --upgrade to add it as new.`,
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Store old version for PR description (if upgrading)
|
|
689
|
-
const oldVersion = skillExists ? registry.skills[skillName].version : null;
|
|
690
|
-
|
|
691
|
-
// Add or update the skill entry
|
|
692
|
-
if (!registry.skills) {
|
|
693
|
-
registry.skills = {};
|
|
694
|
-
}
|
|
695
|
-
registry.skills[skillName] = entry;
|
|
696
|
-
registry.updated = new Date(); // Use Date object directly for unquoted YAML output
|
|
697
|
-
|
|
698
|
-
// Write updated registry
|
|
699
|
-
// Note: Don't use quotingType to avoid quoting timestamps
|
|
700
|
-
const updatedYaml = yaml.dump(registry, {
|
|
701
|
-
indent: 2,
|
|
702
|
-
lineWidth: 120,
|
|
703
|
-
sortKeys: false,
|
|
704
|
-
forceQuotes: false,
|
|
705
|
-
});
|
|
706
|
-
fs.writeFileSync(registryPath, updatedYaml);
|
|
707
|
-
|
|
708
|
-
// Create branch and commit
|
|
709
|
-
if (verbose) console.log(`Creating branch: ${branchName}`);
|
|
710
|
-
execSync(`git checkout -b ${branchName}`, {
|
|
711
|
-
cwd: tempDir,
|
|
712
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
execSync(`git add registry.yaml`, {
|
|
716
|
-
cwd: tempDir,
|
|
717
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
const commitMsg = upgrade
|
|
721
|
-
? `Upgrade skill: ${skillName} (${oldVersion} � ${entry.version})`
|
|
722
|
-
: `Add skill: ${skillName}`;
|
|
723
|
-
execSync(`git commit -m "${commitMsg}"`, {
|
|
724
|
-
cwd: tempDir,
|
|
725
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
// Fork the repo (if not already forked) and push
|
|
729
|
-
if (verbose) console.log('Forking marketplace repo (if needed)...');
|
|
730
|
-
try {
|
|
731
|
-
execSync('gh repo fork cnrai/openpave-marketplace --clone=false', {
|
|
732
|
-
cwd: tempDir,
|
|
733
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
734
|
-
});
|
|
735
|
-
} catch (e) {
|
|
736
|
-
// Fork might already exist, continue
|
|
737
|
-
if (verbose) console.log('Fork may already exist, continuing...');
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Get current user
|
|
741
|
-
const ghUser = execSync('gh api user -q .login', { encoding: 'utf8' }).trim();
|
|
742
|
-
if (verbose) console.log(`GitHub user: ${ghUser}`);
|
|
743
|
-
|
|
744
|
-
// Add fork as remote and push
|
|
745
|
-
try {
|
|
746
|
-
execSync(`git remote add fork https://github.com/${ghUser}/openpave-marketplace.git`, {
|
|
747
|
-
cwd: tempDir,
|
|
748
|
-
stdio: 'pipe',
|
|
749
|
-
});
|
|
750
|
-
} catch (e) {
|
|
751
|
-
// Remote might already exist
|
|
752
|
-
execSync(`git remote set-url fork https://github.com/${ghUser}/openpave-marketplace.git`, {
|
|
753
|
-
cwd: tempDir,
|
|
754
|
-
stdio: 'pipe',
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
if (verbose) console.log('Pushing to fork...');
|
|
759
|
-
execSync(`git push -u fork ${branchName} --force`, {
|
|
760
|
-
cwd: tempDir,
|
|
761
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
// Create PR
|
|
765
|
-
if (verbose) console.log('Creating pull request...');
|
|
766
|
-
|
|
767
|
-
const prTitle = upgrade
|
|
768
|
-
? `Upgrade skill: ${skillName} (${oldVersion} � ${entry.version})`
|
|
769
|
-
: `Add skill: ${skillName}`;
|
|
770
|
-
|
|
771
|
-
const prBody = upgrade ? `## Upgrade Skill: ${skillName}
|
|
772
|
-
|
|
773
|
-
**Version Update:** ${oldVersion} � ${entry.version}
|
|
774
|
-
|
|
775
|
-
**Skill Details:**
|
|
776
|
-
- **Name:** ${entry.name}
|
|
777
|
-
- **Version:** ${entry.version}
|
|
778
|
-
- **Description:** ${entry.description}
|
|
779
|
-
- **Category:** ${entry.category}
|
|
780
|
-
- **Repository:** [${entry.repository}](https://github.com/${entry.repository})
|
|
781
|
-
|
|
782
|
-
**Update command:**
|
|
783
|
-
\`\`\`bash
|
|
784
|
-
pave update ${skillName}
|
|
785
|
-
\`\`\`
|
|
786
|
-
|
|
787
|
-
---
|
|
788
|
-
*This PR was automatically generated by \`pave publish --create-pr --upgrade\`*
|
|
789
|
-
` : `## Add Skill: ${skillName}
|
|
790
|
-
|
|
791
|
-
**Skill Details:**
|
|
792
|
-
- **Name:** ${entry.name}
|
|
793
|
-
- **Version:** ${entry.version}
|
|
794
|
-
- **Description:** ${entry.description}
|
|
795
|
-
- **Category:** ${entry.category}
|
|
796
|
-
- **Repository:** [${entry.repository}](https://github.com/${entry.repository})
|
|
797
|
-
|
|
798
|
-
**Install command:**
|
|
799
|
-
\`\`\`bash
|
|
800
|
-
pave install ${skillName}
|
|
801
|
-
\`\`\`
|
|
802
|
-
|
|
803
|
-
---
|
|
804
|
-
*This PR was automatically generated by \`pave publish --create-pr\`*
|
|
805
|
-
`;
|
|
806
|
-
|
|
807
|
-
// Write PR body to temp file to handle special characters
|
|
808
|
-
const prBodyFile = path.join(tempDir, 'pr-body.md');
|
|
809
|
-
fs.writeFileSync(prBodyFile, prBody);
|
|
810
|
-
|
|
811
|
-
const prResult = execSync(
|
|
812
|
-
`gh pr create --repo cnrai/openpave-marketplace --title "${prTitle}" --body-file "${prBodyFile}" --head ${ghUser}:${branchName}`,
|
|
813
|
-
{
|
|
814
|
-
cwd: tempDir,
|
|
815
|
-
encoding: 'utf8',
|
|
816
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
817
|
-
},
|
|
818
|
-
).trim();
|
|
819
|
-
|
|
820
|
-
// Clean up temp directory
|
|
821
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
822
|
-
|
|
823
|
-
return {
|
|
824
|
-
success: true,
|
|
825
|
-
prUrl: prResult,
|
|
826
|
-
skillName,
|
|
827
|
-
repository: entry.repository,
|
|
828
|
-
};
|
|
829
|
-
} catch (e) {
|
|
830
|
-
// Clean up on error
|
|
831
|
-
try {
|
|
832
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
833
|
-
} catch (cleanupErr) {
|
|
834
|
-
// Ignore cleanup errors
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
return {
|
|
838
|
-
success: false,
|
|
839
|
-
error: e.message || String(e),
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// Export functions
|
|
845
|
-
module.exports = {
|
|
846
|
-
fetchRegistry,
|
|
847
|
-
searchSkills,
|
|
848
|
-
lookupSkill,
|
|
849
|
-
validateSkillForPublish,
|
|
850
|
-
prepareForPublish,
|
|
851
|
-
getPublishInstructions,
|
|
852
|
-
generateRegistryEntry,
|
|
853
|
-
createPublishPR,
|
|
854
|
-
clearCache,
|
|
855
|
-
getCacheInfo,
|
|
856
|
-
getRegistryUrl,
|
|
857
|
-
// Config path management
|
|
858
|
-
setPaveHome,
|
|
859
|
-
getPaveHome,
|
|
860
|
-
getCachePaths,
|
|
861
|
-
// Constants
|
|
862
|
-
DEFAULT_REGISTRY_URL,
|
|
863
|
-
// Deprecated: use getCachePaths() instead. Kept for backward compatibility.
|
|
864
|
-
get CACHE_DIR() { return getCachePaths().cacheDir; },
|
|
865
|
-
get CACHE_FILE() { return getCachePaths().cacheFile; },
|
|
866
|
-
};
|