wyvrnpm 1.1.2 → 1.2.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/bin/wyvrn.js +6 -0
- package/package.json +1 -1
- package/src/commands/publish.js +14 -1
- package/src/providers/base.js +11 -0
- package/src/providers/file.js +13 -1
- package/src/providers/http.js +55 -0
- package/src/providers/s3.js +45 -0
- package/src/resolve.js +42 -0
package/bin/wyvrn.js
CHANGED
|
@@ -86,6 +86,12 @@ yargs
|
|
|
86
86
|
.option('token', {
|
|
87
87
|
type: 'string',
|
|
88
88
|
description: 'Bearer token for HTTP server authentication (HTTP only)',
|
|
89
|
+
})
|
|
90
|
+
.option('force', {
|
|
91
|
+
alias: 'f',
|
|
92
|
+
type: 'boolean',
|
|
93
|
+
description: 'Overwrite an existing published version',
|
|
94
|
+
default: false,
|
|
89
95
|
});
|
|
90
96
|
},
|
|
91
97
|
(argv) => publish(argv),
|
package/package.json
CHANGED
package/src/commands/publish.js
CHANGED
|
@@ -72,7 +72,7 @@ function addFolderFiltered(zip, dir, base, ignoreRe) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
async function publish(argv) {
|
|
75
|
-
let { source, profile, platform, path: publishPath, token, manifest: manifestArg } = argv;
|
|
75
|
+
let { source, profile, platform, path: publishPath, token, manifest: manifestArg, force } = argv;
|
|
76
76
|
|
|
77
77
|
// Resolve source + auth: CLI > config entry by name > first config entry > raw URL
|
|
78
78
|
if (source) {
|
|
@@ -137,6 +137,19 @@ async function publish(argv) {
|
|
|
137
137
|
`[wyvrn] Publishing ${name}@${version} (${targetPlatform}) via ${provider.constructor.providerName} provider`,
|
|
138
138
|
);
|
|
139
139
|
|
|
140
|
+
// Prevent overwriting an already-published version unless --force is passed
|
|
141
|
+
const alreadyExists = await provider.exists({ source, platform: targetPlatform, name, version, profile, token });
|
|
142
|
+
if (alreadyExists) {
|
|
143
|
+
if (force) {
|
|
144
|
+
console.warn(`[wyvrn] Warning: ${name}@${version} already exists — overwriting (--force)`);
|
|
145
|
+
} else {
|
|
146
|
+
console.error(
|
|
147
|
+
`[wyvrn] Error: ${name}@${version} already exists. Bump the version or use --force to overwrite.`,
|
|
148
|
+
);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
140
153
|
// Load .wyvrnignore patterns from the publish directory
|
|
141
154
|
const ignoreFile = path.join(srcDir, '.wyvrnignore');
|
|
142
155
|
const ignorePatterns = loadIgnorePatterns(ignoreFile);
|
package/src/providers/base.js
CHANGED
|
@@ -36,6 +36,17 @@ class BaseProvider {
|
|
|
36
36
|
throw new Error(`publish() is not implemented in ${this.constructor.name}`);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Returns true if the given package version already exists at the destination.
|
|
41
|
+
* Providers should override this; the default assumes it does not exist.
|
|
42
|
+
*
|
|
43
|
+
* @param {{ source: string, platform: string, name: string, version: string, profile?: string, token?: string }} _options
|
|
44
|
+
* @returns {Promise<boolean>}
|
|
45
|
+
*/
|
|
46
|
+
async exists(_options) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
/** Human-readable provider name shown in log output. */
|
|
40
51
|
static get providerName() {
|
|
41
52
|
return 'BaseProvider';
|
package/src/providers/file.js
CHANGED
|
@@ -40,14 +40,26 @@ class FileProvider extends BaseProvider {
|
|
|
40
40
|
return path.resolve(source);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
async exists(options) {
|
|
44
|
+
const { source, platform, name, version } = options;
|
|
45
|
+
const destDir = path.join(this._resolvePath(source), platform, name, version);
|
|
46
|
+
return fs.existsSync(path.join(destDir, 'wyvrn.json'));
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
async publish(files, options) {
|
|
44
50
|
const { source, platform, name, version } = options;
|
|
45
51
|
|
|
46
|
-
const
|
|
52
|
+
const packageDir = path.join(this._resolvePath(source), platform, name);
|
|
53
|
+
const destDir = path.join(packageDir, version);
|
|
47
54
|
fs.mkdirSync(destDir, { recursive: true });
|
|
48
55
|
|
|
49
56
|
this._copy(files.manifest, path.join(destDir, 'wyvrn.json'));
|
|
50
57
|
this._copy(files.zip, path.join(destDir, 'wyvrn.zip'));
|
|
58
|
+
|
|
59
|
+
// Write latest.json at the package root so consumers can resolve "latest"
|
|
60
|
+
const latestPath = path.join(packageDir, 'latest.json');
|
|
61
|
+
console.log(`[wyvrn] → ${latestPath}`);
|
|
62
|
+
fs.writeFileSync(latestPath, JSON.stringify({ version }), 'utf8');
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
_copy(src, dest) {
|
package/src/providers/http.js
CHANGED
|
@@ -14,13 +14,33 @@ class HttpProvider extends BaseProvider {
|
|
|
14
14
|
return 'HTTP';
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
async exists(options) {
|
|
18
|
+
const { source, platform, name, version } = options;
|
|
19
|
+
const base = source.replace(/\/$/, '');
|
|
20
|
+
const url = `${base}/${platform}/${name}/${version}/wyvrn.json`;
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const parsed = new URL(url);
|
|
23
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
24
|
+
const req = lib.request(
|
|
25
|
+
{ hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'HEAD' },
|
|
26
|
+
(res) => { res.resume(); resolve(res.statusCode >= 200 && res.statusCode < 300); },
|
|
27
|
+
);
|
|
28
|
+
req.on('error', reject);
|
|
29
|
+
req.end();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
async publish(files, options) {
|
|
18
34
|
const { source, platform, name, version, token } = options;
|
|
19
35
|
const base = source.replace(/\/$/, '');
|
|
20
36
|
const urlBase = `${base}/${platform}/${name}/${version}`;
|
|
37
|
+
const packageBase = `${base}/${platform}/${name}`;
|
|
21
38
|
|
|
22
39
|
await this._put(`${urlBase}/wyvrn.json`, files.manifest, 'application/json', token);
|
|
23
40
|
await this._put(`${urlBase}/wyvrn.zip`, files.zip, 'application/zip', token);
|
|
41
|
+
|
|
42
|
+
// Write latest.json at the package root so consumers can resolve "latest"
|
|
43
|
+
await this._putBuffer(`${packageBase}/latest.json`, Buffer.from(JSON.stringify({ version })), 'application/json', token);
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
_put(url, filePath, contentType, token) {
|
|
@@ -58,6 +78,41 @@ class HttpProvider extends BaseProvider {
|
|
|
58
78
|
req.end();
|
|
59
79
|
});
|
|
60
80
|
}
|
|
81
|
+
|
|
82
|
+
_putBuffer(url, buffer, contentType, token) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const parsed = new URL(url);
|
|
85
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
86
|
+
|
|
87
|
+
const headers = {
|
|
88
|
+
'Content-Type': contentType,
|
|
89
|
+
'Content-Length': buffer.length,
|
|
90
|
+
};
|
|
91
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
92
|
+
|
|
93
|
+
console.log(`[wyvrn] → PUT ${url}`);
|
|
94
|
+
const req = lib.request(
|
|
95
|
+
{
|
|
96
|
+
hostname: parsed.hostname,
|
|
97
|
+
port: parsed.port || undefined,
|
|
98
|
+
path: parsed.pathname,
|
|
99
|
+
method: 'PUT',
|
|
100
|
+
headers,
|
|
101
|
+
},
|
|
102
|
+
(res) => {
|
|
103
|
+
res.resume();
|
|
104
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
105
|
+
resolve();
|
|
106
|
+
} else {
|
|
107
|
+
reject(new Error(`HTTP PUT ${url} failed: ${res.statusCode} ${res.statusMessage}`));
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
req.on('error', reject);
|
|
112
|
+
req.write(buffer);
|
|
113
|
+
req.end();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
61
116
|
}
|
|
62
117
|
|
|
63
118
|
module.exports = HttpProvider;
|
package/src/providers/s3.js
CHANGED
|
@@ -49,6 +49,33 @@ class S3Provider extends BaseProvider {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
async exists(options) {
|
|
53
|
+
let S3Client, HeadObjectCommand, fromSSO;
|
|
54
|
+
try {
|
|
55
|
+
({ S3Client, HeadObjectCommand } = require('@aws-sdk/client-s3'));
|
|
56
|
+
({ fromSSO } = require('@aws-sdk/credential-providers'));
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'AWS SDK packages are required for S3 publishing.\n' +
|
|
60
|
+
'Run: npm install @aws-sdk/client-s3 @aws-sdk/credential-providers',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { source, platform, name, version, profile } = options;
|
|
65
|
+
const { bucket, prefix } = this._parseSource(source);
|
|
66
|
+
const key = [prefix, platform, name, version, 'wyvrn.json'].filter(Boolean).join('/');
|
|
67
|
+
|
|
68
|
+
const clientConfig = profile ? { credentials: fromSSO({ profile }) } : {};
|
|
69
|
+
const client = new S3Client(clientConfig);
|
|
70
|
+
try {
|
|
71
|
+
await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
72
|
+
return true;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) return false;
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
async publish(files, options) {
|
|
53
80
|
// Lazy-load AWS SDK so the tool works without it when only HTTP is used.
|
|
54
81
|
let S3Client, PutObjectCommand, fromSSO;
|
|
@@ -67,12 +94,18 @@ class S3Provider extends BaseProvider {
|
|
|
67
94
|
|
|
68
95
|
// Destination key base: [prefix/]platform/name/version
|
|
69
96
|
const keyBase = [prefix, platform, name, version].filter(Boolean).join('/');
|
|
97
|
+
// Package root (without version) for latest.json
|
|
98
|
+
const packageBase = [prefix, platform, name].filter(Boolean).join('/');
|
|
70
99
|
|
|
71
100
|
const clientConfig = profile ? { credentials: fromSSO({ profile }) } : {};
|
|
72
101
|
const client = new S3Client(clientConfig);
|
|
73
102
|
|
|
74
103
|
await this._put(client, bucket, `${keyBase}/wyvrn.json`, files.manifest, 'application/json', PutObjectCommand);
|
|
75
104
|
await this._put(client, bucket, `${keyBase}/wyvrn.zip`, files.zip, 'application/zip', PutObjectCommand);
|
|
105
|
+
|
|
106
|
+
// Write latest.json at the package root so consumers can resolve "latest"
|
|
107
|
+
const latestContent = Buffer.from(JSON.stringify({ version }));
|
|
108
|
+
await this._putBuffer(client, bucket, `${packageBase}/latest.json`, latestContent, 'application/json', PutObjectCommand);
|
|
76
109
|
}
|
|
77
110
|
|
|
78
111
|
async _put(client, bucket, key, filePath, contentType, PutObjectCommand) {
|
|
@@ -86,6 +119,18 @@ class S3Provider extends BaseProvider {
|
|
|
86
119
|
}),
|
|
87
120
|
);
|
|
88
121
|
}
|
|
122
|
+
|
|
123
|
+
async _putBuffer(client, bucket, key, content, contentType, PutObjectCommand) {
|
|
124
|
+
console.log(`[wyvrn] → s3://${bucket}/${key}`);
|
|
125
|
+
await client.send(
|
|
126
|
+
new PutObjectCommand({
|
|
127
|
+
Bucket: bucket,
|
|
128
|
+
Key: key,
|
|
129
|
+
Body: content,
|
|
130
|
+
ContentType: contentType,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
89
134
|
}
|
|
90
135
|
|
|
91
136
|
module.exports = S3Provider;
|
package/src/resolve.js
CHANGED
|
@@ -22,6 +22,36 @@ function compareVersions(a, b) {
|
|
|
22
22
|
return 0;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the "latest" tag for a package by fetching latest.json from the registry.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} name
|
|
29
|
+
* @param {string[]} packageSources
|
|
30
|
+
* @param {string} platform
|
|
31
|
+
* @param {Function} httpClient
|
|
32
|
+
* @returns {Promise<string>} The resolved version string.
|
|
33
|
+
*/
|
|
34
|
+
async function resolveLatestVersion(name, packageSources, platform, httpClient) {
|
|
35
|
+
for (const source of packageSources) {
|
|
36
|
+
const base = source.replace(/\/$/, '');
|
|
37
|
+
const candidates = [
|
|
38
|
+
`${base}/${platform}/${name}/latest.json`,
|
|
39
|
+
`${base}/common/${name}/latest.json`,
|
|
40
|
+
];
|
|
41
|
+
for (const url of candidates) {
|
|
42
|
+
try {
|
|
43
|
+
const response = await httpClient(url);
|
|
44
|
+
if (!response.ok) continue;
|
|
45
|
+
const data = await response.json();
|
|
46
|
+
if (data && data.version) return data.version;
|
|
47
|
+
} catch {
|
|
48
|
+
// Try next candidate.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Could not resolve "latest" version for "${name}" from any source`);
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
/**
|
|
26
56
|
* Fetches razer.json for a single package from the first source that responds.
|
|
27
57
|
*
|
|
@@ -96,6 +126,17 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
96
126
|
version = lockedVersions.get(name);
|
|
97
127
|
}
|
|
98
128
|
|
|
129
|
+
// Resolve the "latest" tag to a concrete version before proceeding.
|
|
130
|
+
if (version === 'latest') {
|
|
131
|
+
try {
|
|
132
|
+
version = await resolveLatestVersion(name, packageSources, platform, httpClient);
|
|
133
|
+
console.log(`[wyvrn] Resolved "${name}@latest" → ${version}`);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.warn(`[wyvrn] Warning: ${err.message}`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
99
140
|
if (resolved.has(name)) {
|
|
100
141
|
const existing = resolved.get(name);
|
|
101
142
|
if (existing === version) {
|
|
@@ -154,5 +195,6 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
154
195
|
|
|
155
196
|
module.exports = {
|
|
156
197
|
compareVersions,
|
|
198
|
+
resolveLatestVersion,
|
|
157
199
|
resolveDependencies,
|
|
158
200
|
};
|