@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.
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 -5776
  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,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
- };