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/file.js
CHANGED
|
@@ -15,6 +15,22 @@ const BaseProvider = require('./base');
|
|
|
15
15
|
* //server/share/path (SMB UNC — forward slash)
|
|
16
16
|
* file:///path (file URI)
|
|
17
17
|
*/
|
|
18
|
+
function pinDependencies(rawDeps, lockedDeps) {
|
|
19
|
+
if (!rawDeps || !lockedDeps || Object.keys(lockedDeps).length === 0) return rawDeps;
|
|
20
|
+
const result = {};
|
|
21
|
+
for (const [pkgName, value] of Object.entries(rawDeps)) {
|
|
22
|
+
const locked = lockedDeps[pkgName];
|
|
23
|
+
if (locked) {
|
|
24
|
+
result[pkgName] = typeof value === 'object' && value !== null
|
|
25
|
+
? { ...value, version: locked }
|
|
26
|
+
: locked;
|
|
27
|
+
} else {
|
|
28
|
+
result[pkgName] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
class FileProvider extends BaseProvider {
|
|
19
35
|
static canHandle(source) {
|
|
20
36
|
return (
|
|
@@ -24,7 +40,7 @@ class FileProvider extends BaseProvider {
|
|
|
24
40
|
source.startsWith('/') ||
|
|
25
41
|
source.startsWith('./') ||
|
|
26
42
|
source.startsWith('../') ||
|
|
27
|
-
/^[a-zA-Z]:[/\\]/.test(source)
|
|
43
|
+
/^[a-zA-Z]:[/\\]/.test(source)
|
|
28
44
|
);
|
|
29
45
|
}
|
|
30
46
|
|
|
@@ -32,7 +48,6 @@ class FileProvider extends BaseProvider {
|
|
|
32
48
|
return 'File';
|
|
33
49
|
}
|
|
34
50
|
|
|
35
|
-
/** Normalise a source string to an absolute file system path. */
|
|
36
51
|
_resolvePath(source) {
|
|
37
52
|
if (source.startsWith('file://')) {
|
|
38
53
|
return path.resolve(new URL(source).pathname);
|
|
@@ -40,31 +55,126 @@ class FileProvider extends BaseProvider {
|
|
|
40
55
|
return path.resolve(source);
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
_copy(src, dest) {
|
|
59
|
+
console.log(`[wyvrn] → ${dest}`);
|
|
60
|
+
fs.copyFileSync(src, dest);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_writeJson(filePath, data) {
|
|
64
|
+
console.log(`[wyvrn] → ${filePath}`);
|
|
65
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_readJson(filePath) {
|
|
69
|
+
if (!fs.existsSync(filePath)) return null;
|
|
70
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
71
|
+
catch { return null; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── v1 (legacy) API ───────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
async exists({ source, platform, name, version }) {
|
|
45
77
|
const destDir = path.join(this._resolvePath(source), platform, name, version);
|
|
46
78
|
return fs.existsSync(path.join(destDir, 'wyvrn.json'));
|
|
47
79
|
}
|
|
48
80
|
|
|
49
|
-
async publish(files,
|
|
50
|
-
const { source, platform, name, version } = options;
|
|
51
|
-
|
|
81
|
+
async publish(files, { source, platform, name, version }) {
|
|
52
82
|
const packageDir = path.join(this._resolvePath(source), platform, name);
|
|
53
|
-
const destDir
|
|
83
|
+
const destDir = path.join(packageDir, version);
|
|
54
84
|
fs.mkdirSync(destDir, { recursive: true });
|
|
55
85
|
|
|
56
86
|
this._copy(files.manifest, path.join(destDir, 'wyvrn.json'));
|
|
57
|
-
this._copy(files.zip,
|
|
87
|
+
this._copy(files.zip, path.join(destDir, 'wyvrn.zip'));
|
|
58
88
|
|
|
59
|
-
// Write latest.json at the package root so consumers can resolve "latest"
|
|
60
89
|
const latestPath = path.join(packageDir, 'latest.json');
|
|
61
90
|
console.log(`[wyvrn] → ${latestPath}`);
|
|
62
91
|
fs.writeFileSync(latestPath, JSON.stringify({ version }), 'utf8');
|
|
63
92
|
}
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
// ── v2 API ────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
_v2Root(source, name) {
|
|
97
|
+
return path.join(this._resolvePath(source), 'v2', name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async v2Exists({ source, name, version, profileHash }) {
|
|
101
|
+
const p = path.join(this._v2Root(source, name), version, profileHash, 'wyvrn.json');
|
|
102
|
+
return fs.existsSync(p);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async v2Publish(files, options) {
|
|
106
|
+
const {
|
|
107
|
+
source, name, version, profileHash, buildSettings,
|
|
108
|
+
contentSha256, gitSha, gitRepo, lockedDependencies,
|
|
109
|
+
} = options;
|
|
110
|
+
|
|
111
|
+
const v2root = this._v2Root(source, name);
|
|
112
|
+
const buildDir = path.join(v2root, version, profileHash);
|
|
113
|
+
fs.mkdirSync(buildDir, { recursive: true });
|
|
114
|
+
|
|
115
|
+
// 1) zip
|
|
116
|
+
this._copy(files.zip, path.join(buildDir, 'wyvrn.zip'));
|
|
117
|
+
|
|
118
|
+
// 2) enriched wyvrn.json (buildSettings stored for debugging only)
|
|
119
|
+
// Dependencies are replaced with locked versions so the server never sees "latest".
|
|
120
|
+
const rawManifest = JSON.parse(fs.readFileSync(files.manifest, 'utf8'));
|
|
121
|
+
const pinnedDeps = pinDependencies(rawManifest.dependencies, lockedDependencies);
|
|
122
|
+
const v2Meta = {
|
|
123
|
+
...rawManifest,
|
|
124
|
+
...(pinnedDeps !== undefined ? { dependencies: pinnedDeps } : {}),
|
|
125
|
+
schemaVersion: 2,
|
|
126
|
+
profileHash,
|
|
127
|
+
buildSettings,
|
|
128
|
+
contentSha256,
|
|
129
|
+
gitSha: gitSha ?? null,
|
|
130
|
+
gitRepo: gitRepo ?? null,
|
|
131
|
+
publishedAt: new Date().toISOString(),
|
|
132
|
+
};
|
|
133
|
+
this._writeJson(path.join(buildDir, 'wyvrn.json'), v2Meta);
|
|
134
|
+
|
|
135
|
+
// 3) source.json
|
|
136
|
+
if (gitSha || gitRepo) {
|
|
137
|
+
const versionDir = path.join(v2root, version);
|
|
138
|
+
fs.mkdirSync(versionDir, { recursive: true });
|
|
139
|
+
this._writeJson(path.join(versionDir, 'source.json'), {
|
|
140
|
+
gitRepo: gitRepo ?? null, gitSha: gitSha ?? null,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 4) versions.json index
|
|
145
|
+
const versionsPath = path.join(v2root, 'versions.json');
|
|
146
|
+
let versionsIdx = this._readJson(versionsPath) ?? { name, latest: version, versions: {} };
|
|
147
|
+
if (!versionsIdx.versions) versionsIdx.versions = {};
|
|
148
|
+
if (!versionsIdx.versions[version]) versionsIdx.versions[version] = { profiles: {} };
|
|
149
|
+
versionsIdx.versions[version].profiles[profileHash] = {
|
|
150
|
+
contentSha256,
|
|
151
|
+
publishedAt: new Date().toISOString(),
|
|
152
|
+
buildSettings,
|
|
153
|
+
};
|
|
154
|
+
if (gitSha || gitRepo) {
|
|
155
|
+
versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
156
|
+
}
|
|
157
|
+
versionsIdx.latest = version;
|
|
158
|
+
this._writeJson(versionsPath, versionsIdx);
|
|
159
|
+
|
|
160
|
+
// 5) v2 latest.json
|
|
161
|
+
this._writeJson(path.join(v2root, 'latest.json'), { version, profileHash, contentSha256 });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async v2GetMeta({ source, name, version, profileHash }) {
|
|
165
|
+
const p = path.join(this._v2Root(source, name), version, profileHash, 'wyvrn.json');
|
|
166
|
+
return this._readJson(p);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async v2GetVersionsIndex({ source, name }) {
|
|
170
|
+
return this._readJson(path.join(this._v2Root(source, name), 'versions.json'));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async v2DownloadZip({ source, name, version, profileHash }, destPath) {
|
|
174
|
+
const src = path.join(this._v2Root(source, name), version, profileHash, 'wyvrn.zip');
|
|
175
|
+
if (!fs.existsSync(src)) return false;
|
|
176
|
+
fs.copyFileSync(src, destPath);
|
|
177
|
+
return true;
|
|
68
178
|
}
|
|
69
179
|
}
|
|
70
180
|
|
package/src/providers/http.js
CHANGED
|
@@ -3,8 +3,26 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const https = require('https');
|
|
6
|
+
const { pipeline } = require('stream/promises');
|
|
7
|
+
const { Readable } = require('stream');
|
|
6
8
|
const BaseProvider = require('./base');
|
|
7
9
|
|
|
10
|
+
function pinDependencies(rawDeps, lockedDeps) {
|
|
11
|
+
if (!rawDeps || !lockedDeps || Object.keys(lockedDeps).length === 0) return rawDeps;
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const [pkgName, value] of Object.entries(rawDeps)) {
|
|
14
|
+
const locked = lockedDeps[pkgName];
|
|
15
|
+
if (locked) {
|
|
16
|
+
result[pkgName] = typeof value === 'object' && value !== null
|
|
17
|
+
? { ...value, version: locked }
|
|
18
|
+
: locked;
|
|
19
|
+
} else {
|
|
20
|
+
result[pkgName] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
class HttpProvider extends BaseProvider {
|
|
9
27
|
static canHandle(source) {
|
|
10
28
|
return /^https?:\/\//.test(source);
|
|
@@ -14,63 +32,26 @@ class HttpProvider extends BaseProvider {
|
|
|
14
32
|
return 'HTTP';
|
|
15
33
|
}
|
|
16
34
|
|
|
17
|
-
|
|
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
|
-
|
|
33
|
-
async publish(files, options) {
|
|
34
|
-
const { source, platform, name, version, token } = options;
|
|
35
|
-
const base = source.replace(/\/$/, '');
|
|
36
|
-
const urlBase = `${base}/${platform}/${name}/${version}`;
|
|
37
|
-
const packageBase = `${base}/${platform}/${name}`;
|
|
38
|
-
|
|
39
|
-
await this._put(`${urlBase}/wyvrn.json`, files.manifest, 'application/json', token);
|
|
40
|
-
await this._put(`${urlBase}/wyvrn.zip`, files.zip, 'application/zip', token);
|
|
35
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
_lib(url) {
|
|
38
|
+
return url.startsWith('https') ? https : http;
|
|
44
39
|
}
|
|
45
40
|
|
|
41
|
+
/** PUT a file from disk to url. */
|
|
46
42
|
_put(url, filePath, contentType, token) {
|
|
47
43
|
return new Promise((resolve, reject) => {
|
|
48
44
|
const body = fs.readFileSync(filePath);
|
|
49
45
|
const parsed = new URL(url);
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const headers = {
|
|
53
|
-
'Content-Type': contentType,
|
|
54
|
-
'Content-Length': body.length,
|
|
55
|
-
};
|
|
46
|
+
const headers = { 'Content-Type': contentType, 'Content-Length': body.length };
|
|
56
47
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
57
|
-
|
|
58
48
|
console.log(`[wyvrn] → PUT ${url}`);
|
|
59
|
-
const req =
|
|
60
|
-
{
|
|
61
|
-
hostname: parsed.hostname,
|
|
62
|
-
port: parsed.port || undefined,
|
|
63
|
-
path: parsed.pathname,
|
|
64
|
-
method: 'PUT',
|
|
65
|
-
headers,
|
|
66
|
-
},
|
|
49
|
+
const req = this._lib(url).request(
|
|
50
|
+
{ hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'PUT', headers },
|
|
67
51
|
(res) => {
|
|
68
|
-
res.resume();
|
|
69
|
-
if (res.statusCode >= 200 && res.statusCode < 300)
|
|
70
|
-
|
|
71
|
-
} else {
|
|
72
|
-
reject(new Error(`HTTP PUT ${url} failed: ${res.statusCode} ${res.statusMessage}`));
|
|
73
|
-
}
|
|
52
|
+
res.resume();
|
|
53
|
+
if (res.statusCode >= 200 && res.statusCode < 300) resolve();
|
|
54
|
+
else reject(new Error(`HTTP PUT ${url} failed: ${res.statusCode}`));
|
|
74
55
|
},
|
|
75
56
|
);
|
|
76
57
|
req.on('error', reject);
|
|
@@ -79,33 +60,19 @@ class HttpProvider extends BaseProvider {
|
|
|
79
60
|
});
|
|
80
61
|
}
|
|
81
62
|
|
|
63
|
+
/** PUT a Buffer to url. */
|
|
82
64
|
_putBuffer(url, buffer, contentType, token) {
|
|
83
65
|
return new Promise((resolve, reject) => {
|
|
84
66
|
const parsed = new URL(url);
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const headers = {
|
|
88
|
-
'Content-Type': contentType,
|
|
89
|
-
'Content-Length': buffer.length,
|
|
90
|
-
};
|
|
67
|
+
const headers = { 'Content-Type': contentType, 'Content-Length': buffer.length };
|
|
91
68
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
92
|
-
|
|
93
69
|
console.log(`[wyvrn] → PUT ${url}`);
|
|
94
|
-
const req =
|
|
95
|
-
{
|
|
96
|
-
hostname: parsed.hostname,
|
|
97
|
-
port: parsed.port || undefined,
|
|
98
|
-
path: parsed.pathname,
|
|
99
|
-
method: 'PUT',
|
|
100
|
-
headers,
|
|
101
|
-
},
|
|
70
|
+
const req = this._lib(url).request(
|
|
71
|
+
{ hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'PUT', headers },
|
|
102
72
|
(res) => {
|
|
103
73
|
res.resume();
|
|
104
|
-
if (res.statusCode >= 200 && res.statusCode < 300)
|
|
105
|
-
|
|
106
|
-
} else {
|
|
107
|
-
reject(new Error(`HTTP PUT ${url} failed: ${res.statusCode} ${res.statusMessage}`));
|
|
108
|
-
}
|
|
74
|
+
if (res.statusCode >= 200 && res.statusCode < 300) resolve();
|
|
75
|
+
else reject(new Error(`HTTP PUT ${url} failed: ${res.statusCode}`));
|
|
109
76
|
},
|
|
110
77
|
);
|
|
111
78
|
req.on('error', reject);
|
|
@@ -113,6 +80,189 @@ class HttpProvider extends BaseProvider {
|
|
|
113
80
|
req.end();
|
|
114
81
|
});
|
|
115
82
|
}
|
|
83
|
+
|
|
84
|
+
/** HEAD check — returns true on 2xx. */
|
|
85
|
+
_head(url, token) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const parsed = new URL(url);
|
|
88
|
+
const headers = {};
|
|
89
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
90
|
+
const req = this._lib(url).request(
|
|
91
|
+
{ hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'HEAD', headers },
|
|
92
|
+
(res) => { res.resume(); resolve(res.statusCode >= 200 && res.statusCode < 300); },
|
|
93
|
+
);
|
|
94
|
+
req.on('error', reject);
|
|
95
|
+
req.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* GET url → parsed JSON, or null on 404 / error.
|
|
101
|
+
* @returns {Promise<object|null>}
|
|
102
|
+
*/
|
|
103
|
+
_getJson(url, token) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const parsed = new URL(url);
|
|
106
|
+
const headers = { Accept: 'application/json' };
|
|
107
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
108
|
+
const req = this._lib(url).request(
|
|
109
|
+
{ hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'GET', headers },
|
|
110
|
+
(res) => {
|
|
111
|
+
if (res.statusCode === 404) { res.resume(); resolve(null); return; }
|
|
112
|
+
if (res.statusCode < 200 || res.statusCode >= 300) { res.resume(); resolve(null); return; }
|
|
113
|
+
const chunks = [];
|
|
114
|
+
res.on('data', (c) => chunks.push(c));
|
|
115
|
+
res.on('end', () => {
|
|
116
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
|
|
117
|
+
catch { resolve(null); }
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
req.on('error', () => resolve(null));
|
|
122
|
+
req.end();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** GET url → stream download to destPath. Returns true on success. */
|
|
127
|
+
async _download(url, destPath, token, timeoutMs) {
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs ?? 300_000);
|
|
130
|
+
try {
|
|
131
|
+
const headers = {};
|
|
132
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
133
|
+
const response = await fetch(url, { signal: controller.signal, headers });
|
|
134
|
+
if (!response.ok) return false;
|
|
135
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
136
|
+
await pipeline(Readable.fromWeb(response.body), fileStream);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
} finally {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── v1 (legacy) API ───────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
async exists({ source, platform, name, version, token }) {
|
|
148
|
+
const base = source.replace(/\/$/, '');
|
|
149
|
+
return this._head(`${base}/${platform}/${name}/${version}/wyvrn.json`, token);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async publish(files, { source, platform, name, version, token }) {
|
|
153
|
+
const base = source.replace(/\/$/, '');
|
|
154
|
+
const urlBase = `${base}/${platform}/${name}/${version}`;
|
|
155
|
+
const packageBase = `${base}/${platform}/${name}`;
|
|
156
|
+
|
|
157
|
+
await this._put(`${urlBase}/wyvrn.json`, files.manifest, 'application/json', token);
|
|
158
|
+
await this._put(`${urlBase}/wyvrn.zip`, files.zip, 'application/zip', token);
|
|
159
|
+
await this._putBuffer(
|
|
160
|
+
`${packageBase}/latest.json`,
|
|
161
|
+
Buffer.from(JSON.stringify({ version })),
|
|
162
|
+
'application/json',
|
|
163
|
+
token,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── v2 API ────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
async v2Exists({ source, name, version, profileHash, token }) {
|
|
170
|
+
const base = source.replace(/\/$/, '');
|
|
171
|
+
return this._head(`${base}/v2/${name}/${version}/${profileHash}/wyvrn.json`, token);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async v2Publish(files, options) {
|
|
175
|
+
const {
|
|
176
|
+
source, name, version, profileHash, buildSettings,
|
|
177
|
+
contentSha256, gitSha, gitRepo, lockedDependencies, token,
|
|
178
|
+
} = options;
|
|
179
|
+
const base = source.replace(/\/$/, '');
|
|
180
|
+
const v2root = `${base}/v2/${name}`;
|
|
181
|
+
const buildRoot = `${v2root}/${version}/${profileHash}`;
|
|
182
|
+
|
|
183
|
+
// 1) Upload the binary zip
|
|
184
|
+
await this._put(`${buildRoot}/wyvrn.zip`, files.zip, 'application/zip', token);
|
|
185
|
+
|
|
186
|
+
// 2) Upload enriched wyvrn.json (original manifest + v2 metadata)
|
|
187
|
+
// buildSettings is stored for debugging purposes only — not for configuration.
|
|
188
|
+
// Dependencies are replaced with locked versions so the server never sees "latest".
|
|
189
|
+
const rawManifest = JSON.parse(fs.readFileSync(files.manifest, 'utf8'));
|
|
190
|
+
const pinnedDeps = pinDependencies(rawManifest.dependencies, lockedDependencies);
|
|
191
|
+
const v2Meta = {
|
|
192
|
+
...rawManifest,
|
|
193
|
+
...(pinnedDeps !== undefined ? { dependencies: pinnedDeps } : {}),
|
|
194
|
+
schemaVersion: 2,
|
|
195
|
+
profileHash,
|
|
196
|
+
buildSettings, // copy of the profile used — for debugging
|
|
197
|
+
contentSha256,
|
|
198
|
+
gitSha: gitSha ?? null,
|
|
199
|
+
gitRepo: gitRepo ?? null,
|
|
200
|
+
publishedAt: new Date().toISOString(),
|
|
201
|
+
};
|
|
202
|
+
await this._putBuffer(
|
|
203
|
+
`${buildRoot}/wyvrn.json`,
|
|
204
|
+
Buffer.from(JSON.stringify(v2Meta, null, 2)),
|
|
205
|
+
'application/json',
|
|
206
|
+
token,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// 3) Upload source.json (git metadata for build-from-source)
|
|
210
|
+
if (gitSha || gitRepo) {
|
|
211
|
+
const srcMeta = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
212
|
+
await this._putBuffer(
|
|
213
|
+
`${v2root}/${version}/source.json`,
|
|
214
|
+
Buffer.from(JSON.stringify(srcMeta, null, 2)),
|
|
215
|
+
'application/json',
|
|
216
|
+
token,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 4) Read-modify-write versions.json index (non-atomic, best-effort)
|
|
221
|
+
let versionsIdx = await this._getJson(`${v2root}/versions.json`, token) ?? {
|
|
222
|
+
name, latest: version, versions: {},
|
|
223
|
+
};
|
|
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
|
+
`${v2root}/versions.json`,
|
|
237
|
+
Buffer.from(JSON.stringify(versionsIdx, null, 2)),
|
|
238
|
+
'application/json',
|
|
239
|
+
token,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// 5) Update v2 latest.json
|
|
243
|
+
await this._putBuffer(
|
|
244
|
+
`${v2root}/latest.json`,
|
|
245
|
+
Buffer.from(JSON.stringify({ version, profileHash, contentSha256 })),
|
|
246
|
+
'application/json',
|
|
247
|
+
token,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async v2GetMeta({ source, name, version, profileHash, token }) {
|
|
252
|
+
const base = source.replace(/\/$/, '');
|
|
253
|
+
return this._getJson(`${base}/v2/${name}/${version}/${profileHash}/wyvrn.json`, token);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async v2GetVersionsIndex({ source, name, token }) {
|
|
257
|
+
const base = source.replace(/\/$/, '');
|
|
258
|
+
return this._getJson(`${base}/v2/${name}/versions.json`, token);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async v2DownloadZip({ source, name, version, profileHash, token }, destPath, timeoutMs) {
|
|
262
|
+
const base = source.replace(/\/$/, '');
|
|
263
|
+
const url = `${base}/v2/${name}/${version}/${profileHash}/wyvrn.zip`;
|
|
264
|
+
return this._download(url, destPath, token, timeoutMs);
|
|
265
|
+
}
|
|
116
266
|
}
|
|
117
267
|
|
|
118
268
|
module.exports = HttpProvider;
|
package/src/providers/index.js
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const S3Provider = require('./s3');
|
|
4
|
-
const HttpProvider = require('./http');
|
|
5
|
-
const FileProvider = require('./file');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Ordered list of providers. Detection runs top-to-bottom; first match wins.
|
|
9
|
-
* Add new providers here (or call registerProvider() at runtime).
|
|
10
|
-
*/
|
|
11
|
-
const PROVIDERS = [
|
|
12
|
-
S3Provider, // must come before HttpProvider — S3 HTTPS URLs also match http://
|
|
13
|
-
HttpProvider,
|
|
14
|
-
FileProvider, // local paths and SMB UNC shares (\\server\share or //server/share)
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Return an instantiated provider that can handle the given source string.
|
|
19
|
-
* @param {string} source
|
|
20
|
-
* @returns {import('./base')}
|
|
21
|
-
*/
|
|
22
|
-
function getProvider(source) {
|
|
23
|
-
for (const Provider of PROVIDERS) {
|
|
24
|
-
if (Provider.canHandle(source)) {
|
|
25
|
-
return new Provider();
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
throw new Error(
|
|
29
|
-
`No provider found for source: "${source}"\n` +
|
|
30
|
-
'Supported schemes: s3://, http://, https://, file://, local path, \\\\server\\share (SMB)',
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Register a custom provider at the front of the detection list
|
|
36
|
-
* (giving it higher priority than built-ins).
|
|
37
|
-
* @param {typeof import('./base')} Provider
|
|
38
|
-
*/
|
|
39
|
-
function registerProvider(Provider) {
|
|
40
|
-
PROVIDERS.unshift(Provider);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
module.exports = { getProvider, registerProvider, PROVIDERS };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const S3Provider = require('./s3');
|
|
4
|
+
const HttpProvider = require('./http');
|
|
5
|
+
const FileProvider = require('./file');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ordered list of providers. Detection runs top-to-bottom; first match wins.
|
|
9
|
+
* Add new providers here (or call registerProvider() at runtime).
|
|
10
|
+
*/
|
|
11
|
+
const PROVIDERS = [
|
|
12
|
+
S3Provider, // must come before HttpProvider — S3 HTTPS URLs also match http://
|
|
13
|
+
HttpProvider,
|
|
14
|
+
FileProvider, // local paths and SMB UNC shares (\\server\share or //server/share)
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Return an instantiated provider that can handle the given source string.
|
|
19
|
+
* @param {string} source
|
|
20
|
+
* @returns {import('./base')}
|
|
21
|
+
*/
|
|
22
|
+
function getProvider(source) {
|
|
23
|
+
for (const Provider of PROVIDERS) {
|
|
24
|
+
if (Provider.canHandle(source)) {
|
|
25
|
+
return new Provider();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
throw new Error(
|
|
29
|
+
`No provider found for source: "${source}"\n` +
|
|
30
|
+
'Supported schemes: s3://, http://, https://, file://, local path, \\\\server\\share (SMB)',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register a custom provider at the front of the detection list
|
|
36
|
+
* (giving it higher priority than built-ins).
|
|
37
|
+
* @param {typeof import('./base')} Provider
|
|
38
|
+
*/
|
|
39
|
+
function registerProvider(Provider) {
|
|
40
|
+
PROVIDERS.unshift(Provider);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { getProvider, registerProvider, PROVIDERS };
|