prpm 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +928 -0
- package/dist/commands/add.js +107 -0
- package/dist/commands/collections.js +409 -0
- package/dist/commands/deps.js +92 -0
- package/dist/commands/index.js +124 -0
- package/dist/commands/info.js +81 -0
- package/dist/commands/install.js +252 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/login.js +219 -0
- package/dist/commands/outdated.js +127 -0
- package/dist/commands/popular.js +27 -0
- package/dist/commands/publish.js +217 -0
- package/dist/commands/remove.js +43 -0
- package/dist/commands/search.js +179 -0
- package/dist/commands/telemetry.js +103 -0
- package/dist/commands/trending.js +75 -0
- package/dist/commands/update.js +120 -0
- package/dist/commands/upgrade.js +120 -0
- package/dist/commands/whoami.js +51 -0
- package/dist/core/config.js +91 -0
- package/dist/core/downloader.js +64 -0
- package/dist/core/filesystem.js +94 -0
- package/dist/core/lockfile.js +182 -0
- package/dist/core/registry-client.js +265 -0
- package/dist/core/telemetry.js +170 -0
- package/dist/core/user-config.js +79 -0
- package/dist/index.js +69 -0
- package/dist/types.js +5 -0
- package/package.json +67 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration management for .promptpm.json
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.readConfig = readConfig;
|
|
10
|
+
exports.writeConfig = writeConfig;
|
|
11
|
+
exports.addPackage = addPackage;
|
|
12
|
+
exports.removePackage = removePackage;
|
|
13
|
+
exports.getPackage = getPackage;
|
|
14
|
+
exports.listPackages = listPackages;
|
|
15
|
+
const fs_1 = require("fs");
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const CONFIG_FILE = '.promptpm.json';
|
|
18
|
+
/**
|
|
19
|
+
* Read the configuration file from the current directory
|
|
20
|
+
*/
|
|
21
|
+
async function readConfig() {
|
|
22
|
+
try {
|
|
23
|
+
const configPath = path_1.default.resolve(CONFIG_FILE);
|
|
24
|
+
const data = await fs_1.promises.readFile(configPath, 'utf-8');
|
|
25
|
+
return JSON.parse(data);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// If file doesn't exist, return empty config
|
|
29
|
+
if (error.code === 'ENOENT') {
|
|
30
|
+
return { sources: [] };
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Failed to read config: ${error}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Write the configuration file to the current directory
|
|
37
|
+
*/
|
|
38
|
+
async function writeConfig(config) {
|
|
39
|
+
try {
|
|
40
|
+
const configPath = path_1.default.resolve(CONFIG_FILE);
|
|
41
|
+
const data = JSON.stringify(config, null, 2);
|
|
42
|
+
await fs_1.promises.writeFile(configPath, data, 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new Error(`Failed to write config: ${error}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Add a package to the configuration
|
|
50
|
+
*/
|
|
51
|
+
async function addPackage(pkg) {
|
|
52
|
+
const config = await readConfig();
|
|
53
|
+
// Check if package with same ID already exists
|
|
54
|
+
const existingIndex = config.sources.findIndex(p => p.id === pkg.id);
|
|
55
|
+
if (existingIndex >= 0) {
|
|
56
|
+
// Update existing package
|
|
57
|
+
config.sources[existingIndex] = pkg;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Add new package
|
|
61
|
+
config.sources.push(pkg);
|
|
62
|
+
}
|
|
63
|
+
await writeConfig(config);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Remove a package from the configuration
|
|
67
|
+
*/
|
|
68
|
+
async function removePackage(id) {
|
|
69
|
+
const config = await readConfig();
|
|
70
|
+
const index = config.sources.findIndex(p => p.id === id);
|
|
71
|
+
if (index === -1) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const removed = config.sources.splice(index, 1)[0];
|
|
75
|
+
await writeConfig(config);
|
|
76
|
+
return removed;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get a package by ID
|
|
80
|
+
*/
|
|
81
|
+
async function getPackage(id) {
|
|
82
|
+
const config = await readConfig();
|
|
83
|
+
return config.sources.find(p => p.id === id) || null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* List all packages
|
|
87
|
+
*/
|
|
88
|
+
async function listPackages() {
|
|
89
|
+
const config = await readConfig();
|
|
90
|
+
return config.sources;
|
|
91
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HTTP file downloading functionality
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.downloadFile = downloadFile;
|
|
7
|
+
exports.extractFilename = extractFilename;
|
|
8
|
+
// Use Node.js built-in fetch (available in Node 18+)
|
|
9
|
+
/**
|
|
10
|
+
* Download a file from a URL
|
|
11
|
+
*/
|
|
12
|
+
async function downloadFile(url) {
|
|
13
|
+
try {
|
|
14
|
+
// Validate URL format
|
|
15
|
+
if (!isValidUrl(url)) {
|
|
16
|
+
throw new Error('Invalid URL format');
|
|
17
|
+
}
|
|
18
|
+
const response = await fetch(url);
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
const content = await response.text();
|
|
23
|
+
return content;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
throw new Error(`Failed to download file: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
throw new Error('Failed to download file: Unknown error');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate if URL is a valid raw GitHub URL
|
|
34
|
+
*/
|
|
35
|
+
function isValidUrl(url) {
|
|
36
|
+
try {
|
|
37
|
+
const urlObj = new URL(url);
|
|
38
|
+
// For MVP, only support raw GitHub URLs
|
|
39
|
+
return (urlObj.protocol === 'https:' &&
|
|
40
|
+
(urlObj.hostname === 'raw.githubusercontent.com' ||
|
|
41
|
+
urlObj.hostname === 'github.com' && urlObj.pathname.includes('/raw/')));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Extract filename from URL
|
|
49
|
+
*/
|
|
50
|
+
function extractFilename(url) {
|
|
51
|
+
try {
|
|
52
|
+
const urlObj = new URL(url);
|
|
53
|
+
const pathname = urlObj.pathname;
|
|
54
|
+
const filename = pathname.split('/').pop() || 'unknown';
|
|
55
|
+
// If no extension, assume it's a markdown file
|
|
56
|
+
if (!filename.includes('.')) {
|
|
57
|
+
return `${filename}.md`;
|
|
58
|
+
}
|
|
59
|
+
return filename;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return 'unknown.md';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* File system operations for managing prompt files
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getDestinationDir = getDestinationDir;
|
|
10
|
+
exports.ensureDirectoryExists = ensureDirectoryExists;
|
|
11
|
+
exports.saveFile = saveFile;
|
|
12
|
+
exports.deleteFile = deleteFile;
|
|
13
|
+
exports.fileExists = fileExists;
|
|
14
|
+
exports.generateId = generateId;
|
|
15
|
+
const fs_1 = require("fs");
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
/**
|
|
18
|
+
* Get the destination directory for a package type
|
|
19
|
+
*/
|
|
20
|
+
function getDestinationDir(type) {
|
|
21
|
+
switch (type) {
|
|
22
|
+
case 'cursor':
|
|
23
|
+
return '.cursor/rules';
|
|
24
|
+
case 'claude':
|
|
25
|
+
return '.claude/agents';
|
|
26
|
+
default:
|
|
27
|
+
throw new Error(`Unknown package type: ${type}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Ensure directory exists, creating it if necessary
|
|
32
|
+
*/
|
|
33
|
+
async function ensureDirectoryExists(dirPath) {
|
|
34
|
+
try {
|
|
35
|
+
await fs_1.promises.mkdir(dirPath, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new Error(`Failed to create directory ${dirPath}: ${error}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Save content to a file
|
|
43
|
+
*/
|
|
44
|
+
async function saveFile(filePath, content) {
|
|
45
|
+
try {
|
|
46
|
+
// Ensure parent directory exists
|
|
47
|
+
const dir = path_1.default.dirname(filePath);
|
|
48
|
+
await ensureDirectoryExists(dir);
|
|
49
|
+
// Write file
|
|
50
|
+
await fs_1.promises.writeFile(filePath, content, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new Error(`Failed to save file ${filePath}: ${error}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Delete a file
|
|
58
|
+
*/
|
|
59
|
+
async function deleteFile(filePath) {
|
|
60
|
+
try {
|
|
61
|
+
await fs_1.promises.unlink(filePath);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const err = error;
|
|
65
|
+
if (err.code === 'ENOENT') {
|
|
66
|
+
// File doesn't exist, that's fine
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Failed to delete file ${filePath}: ${error}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if a file exists
|
|
74
|
+
*/
|
|
75
|
+
async function fileExists(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
await fs_1.promises.access(filePath);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Generate a unique ID from filename
|
|
86
|
+
*/
|
|
87
|
+
function generateId(filename) {
|
|
88
|
+
// Remove extension and convert to kebab-case
|
|
89
|
+
const name = filename.replace(/\.[^/.]+$/, '');
|
|
90
|
+
return name
|
|
91
|
+
.toLowerCase()
|
|
92
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
93
|
+
.replace(/^-+|-+$/g, '');
|
|
94
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lock file management for reproducible installations
|
|
4
|
+
* prpm.lock format similar to package-lock.json
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.readLockfile = readLockfile;
|
|
8
|
+
exports.writeLockfile = writeLockfile;
|
|
9
|
+
exports.createLockfile = createLockfile;
|
|
10
|
+
exports.addToLockfile = addToLockfile;
|
|
11
|
+
exports.setPackageIntegrity = setPackageIntegrity;
|
|
12
|
+
exports.verifyPackageIntegrity = verifyPackageIntegrity;
|
|
13
|
+
exports.getLockedVersion = getLockedVersion;
|
|
14
|
+
exports.isLockfileOutOfSync = isLockfileOutOfSync;
|
|
15
|
+
exports.mergeLockfiles = mergeLockfiles;
|
|
16
|
+
exports.pruneLockfile = pruneLockfile;
|
|
17
|
+
const fs_1 = require("fs");
|
|
18
|
+
const path_1 = require("path");
|
|
19
|
+
const crypto_1 = require("crypto");
|
|
20
|
+
const LOCKFILE_NAME = 'prpm.lock';
|
|
21
|
+
const LOCKFILE_VERSION = 1;
|
|
22
|
+
/**
|
|
23
|
+
* Read lock file from current directory
|
|
24
|
+
*/
|
|
25
|
+
async function readLockfile(cwd = process.cwd()) {
|
|
26
|
+
try {
|
|
27
|
+
const lockfilePath = (0, path_1.join)(cwd, LOCKFILE_NAME);
|
|
28
|
+
const content = await fs_1.promises.readFile(lockfilePath, 'utf-8');
|
|
29
|
+
return JSON.parse(content);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code === 'ENOENT') {
|
|
33
|
+
return null; // Lock file doesn't exist
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Failed to read lock file: ${error}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Write lock file to current directory
|
|
40
|
+
*/
|
|
41
|
+
async function writeLockfile(lockfile, cwd = process.cwd()) {
|
|
42
|
+
try {
|
|
43
|
+
const lockfilePath = (0, path_1.join)(cwd, LOCKFILE_NAME);
|
|
44
|
+
const content = JSON.stringify(lockfile, null, 2);
|
|
45
|
+
await fs_1.promises.writeFile(lockfilePath, content, 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new Error(`Failed to write lock file: ${error}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create new lock file
|
|
53
|
+
*/
|
|
54
|
+
function createLockfile() {
|
|
55
|
+
return {
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
lockfileVersion: LOCKFILE_VERSION,
|
|
58
|
+
packages: {},
|
|
59
|
+
generated: new Date().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Add package to lock file
|
|
64
|
+
*/
|
|
65
|
+
function addToLockfile(lockfile, packageId, packageInfo) {
|
|
66
|
+
lockfile.packages[packageId] = {
|
|
67
|
+
version: packageInfo.version,
|
|
68
|
+
resolved: packageInfo.tarballUrl,
|
|
69
|
+
integrity: '', // Will be set after download
|
|
70
|
+
dependencies: packageInfo.dependencies,
|
|
71
|
+
type: packageInfo.type,
|
|
72
|
+
format: packageInfo.format,
|
|
73
|
+
};
|
|
74
|
+
lockfile.generated = new Date().toISOString();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Update package integrity hash after download
|
|
78
|
+
*/
|
|
79
|
+
function setPackageIntegrity(lockfile, packageId, tarballBuffer) {
|
|
80
|
+
if (!lockfile.packages[packageId]) {
|
|
81
|
+
throw new Error(`Package ${packageId} not found in lock file`);
|
|
82
|
+
}
|
|
83
|
+
const hash = (0, crypto_1.createHash)('sha256').update(tarballBuffer).digest('hex');
|
|
84
|
+
lockfile.packages[packageId].integrity = `sha256-${hash}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Verify package integrity
|
|
88
|
+
*/
|
|
89
|
+
function verifyPackageIntegrity(lockfile, packageId, tarballBuffer) {
|
|
90
|
+
const pkg = lockfile.packages[packageId];
|
|
91
|
+
if (!pkg || !pkg.integrity) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const hash = (0, crypto_1.createHash)('sha256').update(tarballBuffer).digest('hex');
|
|
95
|
+
const expectedHash = pkg.integrity.replace('sha256-', '');
|
|
96
|
+
return hash === expectedHash;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get locked version for a package
|
|
100
|
+
*/
|
|
101
|
+
function getLockedVersion(lockfile, packageId) {
|
|
102
|
+
if (!lockfile || !lockfile.packages[packageId]) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return lockfile.packages[packageId].version;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if lock file is out of sync with dependencies
|
|
109
|
+
*/
|
|
110
|
+
function isLockfileOutOfSync(lockfile, requiredPackages) {
|
|
111
|
+
if (!lockfile) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
// Check if all required packages are in lock file
|
|
115
|
+
for (const [pkgId, version] of Object.entries(requiredPackages)) {
|
|
116
|
+
const locked = lockfile.packages[pkgId];
|
|
117
|
+
if (!locked || locked.version !== version) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Merge lock files (for conflict resolution)
|
|
125
|
+
*/
|
|
126
|
+
function mergeLockfiles(base, incoming) {
|
|
127
|
+
const merged = createLockfile();
|
|
128
|
+
// Merge packages from both lock files
|
|
129
|
+
const allPackages = new Set([
|
|
130
|
+
...Object.keys(base.packages),
|
|
131
|
+
...Object.keys(incoming.packages),
|
|
132
|
+
]);
|
|
133
|
+
for (const pkgId of allPackages) {
|
|
134
|
+
const basePkg = base.packages[pkgId];
|
|
135
|
+
const incomingPkg = incoming.packages[pkgId];
|
|
136
|
+
if (!basePkg) {
|
|
137
|
+
merged.packages[pkgId] = incomingPkg;
|
|
138
|
+
}
|
|
139
|
+
else if (!incomingPkg) {
|
|
140
|
+
merged.packages[pkgId] = basePkg;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Both exist - prefer newer version
|
|
144
|
+
const baseVersion = basePkg.version;
|
|
145
|
+
const incomingVersion = incomingPkg.version;
|
|
146
|
+
merged.packages[pkgId] = compareVersions(baseVersion, incomingVersion) >= 0
|
|
147
|
+
? basePkg
|
|
148
|
+
: incomingPkg;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return merged;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Simple semver comparison (returns 1 if a > b, -1 if a < b, 0 if equal)
|
|
155
|
+
*/
|
|
156
|
+
function compareVersions(a, b) {
|
|
157
|
+
const aParts = a.split('.').map(Number);
|
|
158
|
+
const bParts = b.split('.').map(Number);
|
|
159
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
160
|
+
const aVal = aParts[i] || 0;
|
|
161
|
+
const bVal = bParts[i] || 0;
|
|
162
|
+
if (aVal > bVal)
|
|
163
|
+
return 1;
|
|
164
|
+
if (aVal < bVal)
|
|
165
|
+
return -1;
|
|
166
|
+
}
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Prune unused packages from lock file
|
|
171
|
+
*/
|
|
172
|
+
function pruneLockfile(lockfile, requiredPackages) {
|
|
173
|
+
const pruned = { ...lockfile };
|
|
174
|
+
pruned.packages = {};
|
|
175
|
+
for (const pkgId of requiredPackages) {
|
|
176
|
+
if (lockfile.packages[pkgId]) {
|
|
177
|
+
pruned.packages[pkgId] = lockfile.packages[pkgId];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
pruned.generated = new Date().toISOString();
|
|
181
|
+
return pruned;
|
|
182
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Registry API Client
|
|
4
|
+
* Handles all communication with the PRMP Registry
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.RegistryClient = void 0;
|
|
8
|
+
exports.getRegistryClient = getRegistryClient;
|
|
9
|
+
class RegistryClient {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.baseUrl = config.url.replace(/\/$/, ''); // Remove trailing slash
|
|
12
|
+
this.token = config.token;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Search for packages in the registry
|
|
16
|
+
*/
|
|
17
|
+
async search(query, options) {
|
|
18
|
+
const params = new URLSearchParams({ q: query });
|
|
19
|
+
if (options?.type)
|
|
20
|
+
params.append('type', options.type);
|
|
21
|
+
if (options?.tags)
|
|
22
|
+
options.tags.forEach(tag => params.append('tags', tag));
|
|
23
|
+
if (options?.limit)
|
|
24
|
+
params.append('limit', options.limit.toString());
|
|
25
|
+
if (options?.offset)
|
|
26
|
+
params.append('offset', options.offset.toString());
|
|
27
|
+
const response = await this.fetch(`/api/v1/search?${params}`);
|
|
28
|
+
return response.json();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get package information
|
|
32
|
+
*/
|
|
33
|
+
async getPackage(packageId) {
|
|
34
|
+
const response = await this.fetch(`/api/v1/packages/${packageId}`);
|
|
35
|
+
return response.json();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get specific package version
|
|
39
|
+
*/
|
|
40
|
+
async getPackageVersion(packageId, version) {
|
|
41
|
+
const response = await this.fetch(`/api/v1/packages/${packageId}/${version}`);
|
|
42
|
+
return response.json();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get package dependencies
|
|
46
|
+
*/
|
|
47
|
+
async getPackageDependencies(packageId, version) {
|
|
48
|
+
const versionPath = version ? `/${version}` : '';
|
|
49
|
+
const response = await this.fetch(`/api/v1/packages/${packageId}${versionPath}/dependencies`);
|
|
50
|
+
return response.json();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get all versions for a package
|
|
54
|
+
*/
|
|
55
|
+
async getPackageVersions(packageId) {
|
|
56
|
+
const response = await this.fetch(`/api/v1/packages/${packageId}/versions`);
|
|
57
|
+
return response.json();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve dependency tree
|
|
61
|
+
*/
|
|
62
|
+
async resolveDependencies(packageId, version) {
|
|
63
|
+
const params = new URLSearchParams();
|
|
64
|
+
if (version)
|
|
65
|
+
params.append('version', version);
|
|
66
|
+
const response = await this.fetch(`/api/v1/packages/${packageId}/resolve?${params}`);
|
|
67
|
+
return response.json();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Download package tarball
|
|
71
|
+
*/
|
|
72
|
+
async downloadPackage(tarballUrl, options = {}) {
|
|
73
|
+
// If format is specified and tarballUrl is from registry, append format param
|
|
74
|
+
let url = tarballUrl;
|
|
75
|
+
if (options.format && tarballUrl.includes(this.baseUrl)) {
|
|
76
|
+
const urlObj = new URL(tarballUrl);
|
|
77
|
+
urlObj.searchParams.set('format', options.format);
|
|
78
|
+
url = urlObj.toString();
|
|
79
|
+
}
|
|
80
|
+
const response = await fetch(url);
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`Failed to download package: ${response.statusText}`);
|
|
83
|
+
}
|
|
84
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
85
|
+
return Buffer.from(arrayBuffer);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get trending packages
|
|
89
|
+
*/
|
|
90
|
+
async getTrending(type, limit = 20) {
|
|
91
|
+
const params = new URLSearchParams({ limit: limit.toString() });
|
|
92
|
+
if (type)
|
|
93
|
+
params.append('type', type);
|
|
94
|
+
const response = await this.fetch(`/api/v1/search/trending?${params}`);
|
|
95
|
+
const data = await response.json();
|
|
96
|
+
return data.packages;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get featured packages
|
|
100
|
+
*/
|
|
101
|
+
async getFeatured(type, limit = 20) {
|
|
102
|
+
const params = new URLSearchParams({ limit: limit.toString() });
|
|
103
|
+
if (type)
|
|
104
|
+
params.append('type', type);
|
|
105
|
+
const response = await this.fetch(`/api/v1/search/featured?${params}`);
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
return data.packages;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Publish a package (requires authentication)
|
|
111
|
+
*/
|
|
112
|
+
async publish(manifest, tarball) {
|
|
113
|
+
if (!this.token) {
|
|
114
|
+
throw new Error('Authentication required. Run `prpm login` first.');
|
|
115
|
+
}
|
|
116
|
+
const formData = new FormData();
|
|
117
|
+
formData.append('manifest', JSON.stringify(manifest));
|
|
118
|
+
formData.append('tarball', new Blob([tarball]), 'package.tar.gz');
|
|
119
|
+
const response = await this.fetch('/api/v1/packages', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
body: formData,
|
|
122
|
+
});
|
|
123
|
+
return response.json();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Login and get authentication token
|
|
127
|
+
*/
|
|
128
|
+
async login() {
|
|
129
|
+
// This will open browser for GitHub OAuth
|
|
130
|
+
// For now, return placeholder - will implement OAuth flow
|
|
131
|
+
throw new Error('Login not yet implemented. Coming soon!');
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get current user info
|
|
135
|
+
*/
|
|
136
|
+
async whoami() {
|
|
137
|
+
if (!this.token) {
|
|
138
|
+
throw new Error('Not authenticated. Run `prpm login` first.');
|
|
139
|
+
}
|
|
140
|
+
const response = await this.fetch('/api/v1/auth/me');
|
|
141
|
+
return response.json();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get collections
|
|
145
|
+
*/
|
|
146
|
+
async getCollections(options) {
|
|
147
|
+
const params = new URLSearchParams();
|
|
148
|
+
if (options?.category)
|
|
149
|
+
params.append('category', options.category);
|
|
150
|
+
if (options?.tag)
|
|
151
|
+
params.append('tag', options.tag);
|
|
152
|
+
if (options?.official)
|
|
153
|
+
params.append('official', 'true');
|
|
154
|
+
if (options?.scope)
|
|
155
|
+
params.append('scope', options.scope);
|
|
156
|
+
if (options?.limit)
|
|
157
|
+
params.append('limit', options.limit.toString());
|
|
158
|
+
if (options?.offset)
|
|
159
|
+
params.append('offset', options.offset.toString());
|
|
160
|
+
const response = await this.fetch(`/api/v1/collections?${params}`);
|
|
161
|
+
return response.json();
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get collection details
|
|
165
|
+
*/
|
|
166
|
+
async getCollection(scope, id, version) {
|
|
167
|
+
const versionPath = version ? `/${version}` : '/1.0.0';
|
|
168
|
+
const response = await this.fetch(`/api/v1/collections/${scope}/${id}${versionPath}`);
|
|
169
|
+
return response.json();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Install collection (get installation plan)
|
|
173
|
+
*/
|
|
174
|
+
async installCollection(options) {
|
|
175
|
+
const params = new URLSearchParams();
|
|
176
|
+
if (options.format)
|
|
177
|
+
params.append('format', options.format);
|
|
178
|
+
if (options.skipOptional)
|
|
179
|
+
params.append('skipOptional', 'true');
|
|
180
|
+
const versionPath = options.version ? `@${options.version}` : '';
|
|
181
|
+
const response = await this.fetch(`/api/v1/collections/${options.scope}/${options.id}${versionPath}/install?${params}`, { method: 'POST' });
|
|
182
|
+
return response.json();
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Create a collection (requires authentication)
|
|
186
|
+
*/
|
|
187
|
+
async createCollection(data) {
|
|
188
|
+
if (!this.token) {
|
|
189
|
+
throw new Error('Authentication required. Run `prpm login` first.');
|
|
190
|
+
}
|
|
191
|
+
const response = await this.fetch('/api/v1/collections', {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
body: JSON.stringify(data),
|
|
194
|
+
});
|
|
195
|
+
return response.json();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Helper method for making authenticated requests with retry logic
|
|
199
|
+
*/
|
|
200
|
+
async fetch(path, options = {}, retries = 3) {
|
|
201
|
+
const url = `${this.baseUrl}${path}`;
|
|
202
|
+
const headers = {
|
|
203
|
+
'Content-Type': 'application/json',
|
|
204
|
+
...options.headers,
|
|
205
|
+
};
|
|
206
|
+
if (this.token) {
|
|
207
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
208
|
+
}
|
|
209
|
+
let lastError = null;
|
|
210
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
211
|
+
try {
|
|
212
|
+
const response = await fetch(url, {
|
|
213
|
+
...options,
|
|
214
|
+
headers,
|
|
215
|
+
});
|
|
216
|
+
// Handle rate limiting with retry
|
|
217
|
+
if (response.status === 429) {
|
|
218
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
219
|
+
const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000;
|
|
220
|
+
if (attempt < retries - 1) {
|
|
221
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Handle server errors with retry
|
|
226
|
+
if (response.status >= 500 && response.status < 600 && attempt < retries - 1) {
|
|
227
|
+
const waitTime = Math.pow(2, attempt) * 1000;
|
|
228
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
233
|
+
throw new Error(error.error || error.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
234
|
+
}
|
|
235
|
+
return response;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
239
|
+
// Network errors - retry with exponential backoff
|
|
240
|
+
if (attempt < retries - 1 && (lastError.message.includes('fetch failed') ||
|
|
241
|
+
lastError.message.includes('ECONNREFUSED') ||
|
|
242
|
+
lastError.message.includes('ETIMEDOUT'))) {
|
|
243
|
+
const waitTime = Math.pow(2, attempt) * 1000;
|
|
244
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// If it's not a retryable error or we're out of retries, throw
|
|
248
|
+
if (attempt === retries - 1) {
|
|
249
|
+
throw lastError;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
throw lastError || new Error('Request failed after retries');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
exports.RegistryClient = RegistryClient;
|
|
257
|
+
/**
|
|
258
|
+
* Get registry client with configuration
|
|
259
|
+
*/
|
|
260
|
+
function getRegistryClient(config) {
|
|
261
|
+
return new RegistryClient({
|
|
262
|
+
url: config.registryUrl || 'https://registry.prpm.dev',
|
|
263
|
+
token: config.token,
|
|
264
|
+
});
|
|
265
|
+
}
|