wyvrnpm 1.3.2 → 2.0.2
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 +704 -413
- package/bin/wyvrn.js +158 -105
- package/package.json +1 -1
- package/src/commands/clean.js +40 -40
- package/src/commands/configure.js +80 -80
- package/src/commands/init.js +33 -33
- package/src/commands/install.js +78 -31
- package/src/commands/link.js +316 -316
- package/src/commands/profile.js +171 -0
- package/src/commands/publish.js +116 -64
- package/src/config.js +8 -6
- package/src/download.js +252 -89
- package/src/index.js +5 -4
- package/src/link-utils.js +95 -95
- package/src/manifest.js +58 -2
- package/src/profile.js +377 -0
- package/src/providers/base.js +89 -12
- package/src/providers/file.js +123 -13
- package/src/providers/http.js +217 -67
- package/src/providers/index.js +43 -43
- package/src/providers/s3.js +207 -70
- package/src/resolve.js +19 -5
package/src/providers/s3.js
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
4
5
|
const BaseProvider = require('./base');
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
const RE_S3_URI = /^s3:\/\//;
|
|
8
|
-
// Matches: https://bucket.s3.amazonaws.com/... or https://bucket.s3.us-east-1.amazonaws.com/...
|
|
7
|
+
const RE_S3_URI = /^s3:\/\//;
|
|
9
8
|
const RE_S3_VIRTUAL_HOSTED = /^https?:\/\/([^.]+)\.s3[.-][^/]*\.amazonaws\.com/;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const RE_S3_PATH_STYLE = /^https?:\/\/s3[.-][^/]*\.amazonaws\.com\/([^/]+)/;
|
|
10
|
+
|
|
11
|
+
function pinDependencies(rawDeps, lockedDeps) {
|
|
12
|
+
if (!rawDeps || !lockedDeps || Object.keys(lockedDeps).length === 0) return rawDeps;
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const [pkgName, value] of Object.entries(rawDeps)) {
|
|
15
|
+
const locked = lockedDeps[pkgName];
|
|
16
|
+
if (locked) {
|
|
17
|
+
result[pkgName] = typeof value === 'object' && value !== null
|
|
18
|
+
? { ...value, version: locked }
|
|
19
|
+
: locked;
|
|
20
|
+
} else {
|
|
21
|
+
result[pkgName] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
12
26
|
|
|
13
27
|
class S3Provider extends BaseProvider {
|
|
14
28
|
static canHandle(source) {
|
|
@@ -23,50 +37,101 @@ class S3Provider extends BaseProvider {
|
|
|
23
37
|
return 'S3';
|
|
24
38
|
}
|
|
25
39
|
|
|
26
|
-
|
|
40
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
27
42
|
_parseSource(source) {
|
|
28
43
|
if (RE_S3_URI.test(source)) {
|
|
29
|
-
const rest
|
|
44
|
+
const rest = source.slice(5);
|
|
30
45
|
const slash = rest.indexOf('/');
|
|
31
46
|
if (slash === -1) return { bucket: rest, prefix: '' };
|
|
32
|
-
return {
|
|
33
|
-
bucket: rest.slice(0, slash),
|
|
34
|
-
prefix: rest.slice(slash + 1).replace(/\/$/, ''),
|
|
35
|
-
};
|
|
47
|
+
return { bucket: rest.slice(0, slash), prefix: rest.slice(slash + 1).replace(/\/$/, '') };
|
|
36
48
|
}
|
|
37
|
-
|
|
38
|
-
const url = new URL(source);
|
|
49
|
+
const url = new URL(source);
|
|
39
50
|
const vhMatch = url.hostname.match(/^([^.]+)\.s3/);
|
|
40
51
|
if (vhMatch) {
|
|
41
52
|
return { bucket: vhMatch[1], prefix: url.pathname.slice(1).replace(/\/$/, '') };
|
|
42
53
|
}
|
|
43
|
-
|
|
44
|
-
// path-style: first path segment is the bucket
|
|
45
54
|
const parts = url.pathname.slice(1).split('/');
|
|
46
|
-
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
return { bucket: parts[0], prefix: parts.slice(1).join('/').replace(/\/$/, '') };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_key(prefix, ...parts) {
|
|
59
|
+
return [prefix, ...parts].filter(Boolean).join('/');
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
let S3Client, HeadObjectCommand, fromSSO;
|
|
62
|
+
_loadSdk() {
|
|
54
63
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
const { S3Client, PutObjectCommand, HeadObjectCommand, GetObjectCommand } =
|
|
65
|
+
require('@aws-sdk/client-s3');
|
|
66
|
+
const { fromSSO } = require('@aws-sdk/credential-providers');
|
|
67
|
+
return { S3Client, PutObjectCommand, HeadObjectCommand, GetObjectCommand, fromSSO };
|
|
57
68
|
} catch {
|
|
58
69
|
throw new Error(
|
|
59
70
|
'AWS SDK packages are required for S3 publishing.\n' +
|
|
60
71
|
'Run: npm install @aws-sdk/client-s3 @aws-sdk/credential-providers',
|
|
61
72
|
);
|
|
62
73
|
}
|
|
74
|
+
}
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
const {
|
|
66
|
-
|
|
76
|
+
_makeClient(awsProfile) {
|
|
77
|
+
const { S3Client, fromSSO } = this._loadSdk();
|
|
78
|
+
return new S3Client(awsProfile ? { credentials: fromSSO({ profile: awsProfile }) } : {});
|
|
79
|
+
}
|
|
67
80
|
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
async _putFile(client, bucket, key, filePath, contentType, PutObjectCommand) {
|
|
82
|
+
console.log(`[wyvrn] → s3://${bucket}/${key}`);
|
|
83
|
+
await client.send(new PutObjectCommand({
|
|
84
|
+
Bucket: bucket, Key: key,
|
|
85
|
+
Body: fs.readFileSync(filePath),
|
|
86
|
+
ContentType: contentType,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async _putBuffer(client, bucket, key, content, contentType, PutObjectCommand) {
|
|
91
|
+
console.log(`[wyvrn] → s3://${bucket}/${key}`);
|
|
92
|
+
await client.send(new PutObjectCommand({
|
|
93
|
+
Bucket: bucket, Key: key,
|
|
94
|
+
Body: content,
|
|
95
|
+
ContentType: contentType,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async _getJson(client, bucket, key, GetObjectCommand) {
|
|
100
|
+
try {
|
|
101
|
+
const resp = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
102
|
+
const chunks = [];
|
|
103
|
+
for await (const chunk of resp.Body) chunks.push(chunk);
|
|
104
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err.name === 'NoSuchKey' || err.$metadata?.httpStatusCode === 404) return null;
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async _downloadToFile(client, bucket, key, destPath, GetObjectCommand) {
|
|
112
|
+
try {
|
|
113
|
+
const resp = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
114
|
+
const out = fs.createWriteStream(destPath);
|
|
115
|
+
await new Promise((resolve, reject) => {
|
|
116
|
+
resp.Body.pipe(out);
|
|
117
|
+
out.on('finish', resolve);
|
|
118
|
+
out.on('error', reject);
|
|
119
|
+
resp.Body.on('error', reject);
|
|
120
|
+
});
|
|
121
|
+
return true;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err.name === 'NoSuchKey' || err.$metadata?.httpStatusCode === 404) return false;
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── v1 (legacy) API ───────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
async exists({ source, platform, name, version, awsProfile }) {
|
|
131
|
+
const { HeadObjectCommand } = this._loadSdk();
|
|
132
|
+
const client = this._makeClient(awsProfile);
|
|
133
|
+
const { bucket, prefix } = this._parseSource(source);
|
|
134
|
+
const key = this._key(prefix, platform, name, version, 'wyvrn.json');
|
|
70
135
|
try {
|
|
71
136
|
await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
72
137
|
return true;
|
|
@@ -76,61 +141,133 @@ class S3Provider extends BaseProvider {
|
|
|
76
141
|
}
|
|
77
142
|
}
|
|
78
143
|
|
|
79
|
-
async publish(files,
|
|
80
|
-
|
|
81
|
-
|
|
144
|
+
async publish(files, { source, platform, name, version, awsProfile }) {
|
|
145
|
+
const { PutObjectCommand } = this._loadSdk();
|
|
146
|
+
const client = this._makeClient(awsProfile);
|
|
147
|
+
const { bucket, prefix } = this._parseSource(source);
|
|
148
|
+
|
|
149
|
+
const keyBase = this._key(prefix, platform, name, version);
|
|
150
|
+
const packageBase = this._key(prefix, platform, name);
|
|
151
|
+
|
|
152
|
+
await this._putFile(client, bucket, `${keyBase}/wyvrn.json`, files.manifest, 'application/json', PutObjectCommand);
|
|
153
|
+
await this._putFile(client, bucket, `${keyBase}/wyvrn.zip`, files.zip, 'application/zip', PutObjectCommand);
|
|
154
|
+
await this._putBuffer(
|
|
155
|
+
client, bucket, `${packageBase}/latest.json`,
|
|
156
|
+
Buffer.from(JSON.stringify({ version })), 'application/json', PutObjectCommand,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── v2 API ────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
async v2Exists({ source, name, version, profileHash, awsProfile }) {
|
|
163
|
+
const { HeadObjectCommand } = this._loadSdk();
|
|
164
|
+
const client = this._makeClient(awsProfile);
|
|
165
|
+
const { bucket, prefix } = this._parseSource(source);
|
|
166
|
+
const key = this._key(prefix, 'v2', name, version, profileHash, 'wyvrn.json');
|
|
82
167
|
try {
|
|
83
|
-
({
|
|
84
|
-
|
|
85
|
-
} catch {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'Run: npm install @aws-sdk/client-s3 @aws-sdk/credential-providers',
|
|
89
|
-
);
|
|
168
|
+
await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
169
|
+
return true;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) return false;
|
|
172
|
+
throw err;
|
|
90
173
|
}
|
|
174
|
+
}
|
|
91
175
|
|
|
92
|
-
|
|
176
|
+
async v2Publish(files, options) {
|
|
177
|
+
const {
|
|
178
|
+
source, name, version, profileHash, buildSettings,
|
|
179
|
+
contentSha256, gitSha, gitRepo, lockedDependencies, awsProfile,
|
|
180
|
+
} = options;
|
|
181
|
+
const { PutObjectCommand, GetObjectCommand } = this._loadSdk();
|
|
182
|
+
const client = this._makeClient(awsProfile);
|
|
93
183
|
const { bucket, prefix } = this._parseSource(source);
|
|
94
184
|
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
// Package root (without version) for latest.json
|
|
98
|
-
const packageBase = [prefix, platform, name].filter(Boolean).join('/');
|
|
185
|
+
const v2root = this._key(prefix, 'v2', name);
|
|
186
|
+
const buildRoot = `${v2root}/${version}/${profileHash}`;
|
|
99
187
|
|
|
100
|
-
|
|
101
|
-
|
|
188
|
+
// 1) Upload zip
|
|
189
|
+
await this._putFile(client, bucket, `${buildRoot}/wyvrn.zip`, files.zip, 'application/zip', PutObjectCommand);
|
|
102
190
|
|
|
103
|
-
|
|
104
|
-
|
|
191
|
+
// 2) Upload enriched wyvrn.json
|
|
192
|
+
// buildSettings is stored for debugging purposes only.
|
|
193
|
+
// Dependencies are replaced with locked versions so the server never sees "latest".
|
|
194
|
+
const rawManifest = JSON.parse(fs.readFileSync(files.manifest, 'utf8'));
|
|
195
|
+
const pinnedDeps = pinDependencies(rawManifest.dependencies, lockedDependencies);
|
|
196
|
+
const v2Meta = {
|
|
197
|
+
...rawManifest,
|
|
198
|
+
...(pinnedDeps !== undefined ? { dependencies: pinnedDeps } : {}),
|
|
199
|
+
schemaVersion: 2,
|
|
200
|
+
profileHash,
|
|
201
|
+
buildSettings,
|
|
202
|
+
contentSha256,
|
|
203
|
+
gitSha: gitSha ?? null,
|
|
204
|
+
gitRepo: gitRepo ?? null,
|
|
205
|
+
publishedAt: new Date().toISOString(),
|
|
206
|
+
};
|
|
207
|
+
await this._putBuffer(
|
|
208
|
+
client, bucket, `${buildRoot}/wyvrn.json`,
|
|
209
|
+
Buffer.from(JSON.stringify(v2Meta, null, 2)), 'application/json', PutObjectCommand,
|
|
210
|
+
);
|
|
105
211
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
212
|
+
// 3) source.json
|
|
213
|
+
if (gitSha || gitRepo) {
|
|
214
|
+
await this._putBuffer(
|
|
215
|
+
client, bucket, `${v2root}/${version}/source.json`,
|
|
216
|
+
Buffer.from(JSON.stringify({ gitRepo: gitRepo ?? null, gitSha: gitSha ?? null }, null, 2)),
|
|
217
|
+
'application/json', PutObjectCommand,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
110
220
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
221
|
+
// 4) Read-modify-write versions.json
|
|
222
|
+
let versionsIdx = await this._getJson(client, bucket, `${v2root}/versions.json`, GetObjectCommand)
|
|
223
|
+
?? { name, latest: version, versions: {} };
|
|
224
|
+
if (!versionsIdx.versions) versionsIdx.versions = {};
|
|
225
|
+
if (!versionsIdx.versions[version]) versionsIdx.versions[version] = { profiles: {} };
|
|
226
|
+
versionsIdx.versions[version].profiles[profileHash] = {
|
|
227
|
+
contentSha256,
|
|
228
|
+
publishedAt: new Date().toISOString(),
|
|
229
|
+
buildSettings,
|
|
230
|
+
};
|
|
231
|
+
if (gitSha || gitRepo) {
|
|
232
|
+
versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
233
|
+
}
|
|
234
|
+
versionsIdx.latest = version;
|
|
235
|
+
await this._putBuffer(
|
|
236
|
+
client, bucket, `${v2root}/versions.json`,
|
|
237
|
+
Buffer.from(JSON.stringify(versionsIdx, null, 2)), 'application/json', PutObjectCommand,
|
|
120
238
|
);
|
|
121
|
-
}
|
|
122
239
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
Key: key,
|
|
129
|
-
Body: content,
|
|
130
|
-
ContentType: contentType,
|
|
131
|
-
}),
|
|
240
|
+
// 5) latest.json
|
|
241
|
+
await this._putBuffer(
|
|
242
|
+
client, bucket, `${v2root}/latest.json`,
|
|
243
|
+
Buffer.from(JSON.stringify({ version, profileHash, contentSha256 })),
|
|
244
|
+
'application/json', PutObjectCommand,
|
|
132
245
|
);
|
|
133
246
|
}
|
|
247
|
+
|
|
248
|
+
async v2GetMeta({ source, name, version, profileHash, awsProfile }) {
|
|
249
|
+
const { GetObjectCommand } = this._loadSdk();
|
|
250
|
+
const client = this._makeClient(awsProfile);
|
|
251
|
+
const { bucket, prefix } = this._parseSource(source);
|
|
252
|
+
const key = this._key(prefix, 'v2', name, version, profileHash, 'wyvrn.json');
|
|
253
|
+
return this._getJson(client, bucket, key, GetObjectCommand);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async v2GetVersionsIndex({ source, name, awsProfile }) {
|
|
257
|
+
const { GetObjectCommand } = this._loadSdk();
|
|
258
|
+
const client = this._makeClient(awsProfile);
|
|
259
|
+
const { bucket, prefix } = this._parseSource(source);
|
|
260
|
+
const key = this._key(prefix, 'v2', name, 'versions.json');
|
|
261
|
+
return this._getJson(client, bucket, key, GetObjectCommand);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async v2DownloadZip({ source, name, version, profileHash, awsProfile }, destPath) {
|
|
265
|
+
const { GetObjectCommand } = this._loadSdk();
|
|
266
|
+
const client = this._makeClient(awsProfile);
|
|
267
|
+
const { bucket, prefix } = this._parseSource(source);
|
|
268
|
+
const key = this._key(prefix, 'v2', name, version, profileHash, 'wyvrn.zip');
|
|
269
|
+
return this._downloadToFile(client, bucket, key, destPath, GetObjectCommand);
|
|
270
|
+
}
|
|
134
271
|
}
|
|
135
272
|
|
|
136
273
|
module.exports = S3Provider;
|
package/src/resolve.js
CHANGED
|
@@ -73,9 +73,12 @@ function compareVersions(a, b) {
|
|
|
73
73
|
* @returns {Promise<string>} The resolved version string.
|
|
74
74
|
*/
|
|
75
75
|
async function resolveLatestVersion(name, packageSources, platform, httpClient) {
|
|
76
|
+
let best = null;
|
|
77
|
+
|
|
76
78
|
for (const source of packageSources) {
|
|
77
79
|
const base = source.replace(/\/$/, '');
|
|
78
80
|
const candidates = [
|
|
81
|
+
`${base}/v2/${name}/latest.json`,
|
|
79
82
|
`${base}/${platform}/${name}/latest.json`,
|
|
80
83
|
`${base}/common/${name}/latest.json`,
|
|
81
84
|
];
|
|
@@ -84,12 +87,18 @@ async function resolveLatestVersion(name, packageSources, platform, httpClient)
|
|
|
84
87
|
const response = await httpClient(url);
|
|
85
88
|
if (!response.ok) continue;
|
|
86
89
|
const data = await response.json();
|
|
87
|
-
if (data
|
|
90
|
+
if (data?.version) {
|
|
91
|
+
if (!best || compareVersions(data.version, best) > 0) {
|
|
92
|
+
best = data.version;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
88
95
|
} catch {
|
|
89
96
|
// Try next candidate.
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
99
|
}
|
|
100
|
+
|
|
101
|
+
if (best) return best;
|
|
93
102
|
throw new Error(`Could not resolve "latest" version for "${name}" from any source`);
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -229,8 +238,9 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
229
238
|
}
|
|
230
239
|
|
|
231
240
|
// Support both formats:
|
|
232
|
-
// PascalCase array: { Dependencies: [ { Name, Version }, ... ] }
|
|
233
|
-
//
|
|
241
|
+
// PascalCase array : { Dependencies: [ { Name, Version }, ... ] }
|
|
242
|
+
// string object : { dependencies: { name: "version", ... } }
|
|
243
|
+
// object object : { dependencies: { name: { version, settings }, ... } }
|
|
234
244
|
let deps = [];
|
|
235
245
|
if (Array.isArray(manifest.Dependencies)) {
|
|
236
246
|
deps = manifest.Dependencies.filter((d) => d.Name && d.Version).map((d) => ({
|
|
@@ -238,10 +248,14 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
238
248
|
version: d.Version,
|
|
239
249
|
}));
|
|
240
250
|
} else if (manifest.dependencies && typeof manifest.dependencies === 'object') {
|
|
241
|
-
deps = Object.entries(manifest.dependencies).map(([
|
|
251
|
+
deps = Object.entries(manifest.dependencies).map(([depName, depValue]) => ({
|
|
252
|
+
name: depName,
|
|
253
|
+
// Handle both "1.0.0" and { version: "1.0.0", settings: {...} }
|
|
254
|
+
version: typeof depValue === 'string' ? depValue : (depValue?.version ?? depValue?.Version ?? ''),
|
|
255
|
+
}));
|
|
242
256
|
}
|
|
243
257
|
for (const dep of deps) {
|
|
244
|
-
queue.push({ name: dep.name, version: dep.version });
|
|
258
|
+
if (dep.version) queue.push({ name: dep.name, version: dep.version });
|
|
245
259
|
}
|
|
246
260
|
}
|
|
247
261
|
|