wyvrnpm 2.10.2 → 2.12.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 +1914 -1860
- package/bin/{wyvrn.js → wyvrnpm.js} +66 -0
- package/cmake/cpp.cmake +9 -9
- package/cmake/functions.cmake +224 -224
- package/cmake/macros.cmake +284 -284
- package/package.json +3 -2
- package/src/auth.js +66 -66
- package/src/binary-dir.js +95 -0
- package/src/bootstrap/cookbook.js +196 -196
- package/src/bootstrap/detect.js +150 -150
- package/src/bootstrap/index.js +220 -220
- package/src/bootstrap/version.js +72 -72
- package/src/build/cache.js +344 -344
- package/src/build/clone.js +170 -170
- package/src/build/cmake.js +342 -342
- package/src/build/index.js +299 -297
- package/src/build/msvc-env.js +260 -260
- package/src/build/recipe.js +188 -188
- package/src/commands/add.js +141 -141
- package/src/commands/bootstrap.js +96 -96
- package/src/commands/build.js +482 -452
- package/src/commands/cache.js +189 -189
- package/src/commands/clean.js +80 -80
- package/src/commands/configure.js +92 -92
- package/src/commands/init.js +70 -70
- package/src/commands/install-skill.js +115 -115
- package/src/commands/install.js +730 -674
- package/src/commands/link.js +320 -320
- package/src/commands/profile.js +237 -237
- package/src/commands/publish.js +584 -555
- package/src/commands/show.js +252 -252
- package/src/commands/version.js +187 -0
- package/src/compat.js +273 -273
- package/src/conf/index.js +415 -391
- package/src/conf/namespaces.js +94 -94
- package/src/context.js +230 -230
- package/src/http-fetch.js +53 -53
- package/src/ignore.js +118 -118
- package/src/logger.js +122 -122
- package/src/options.js +303 -303
- package/src/settings-overrides.js +152 -152
- package/src/toolchain/deps.js +164 -164
- package/src/toolchain/index.js +212 -212
- package/src/toolchain/presets.js +356 -340
- package/src/toolchain/template.cmake +77 -77
- package/src/upload-built.js +265 -265
- package/src/version-range.js +301 -301
- package/src/zip-safe.js +52 -52
- package/src/zip-stream.js +126 -0
package/src/upload-built.js
CHANGED
|
@@ -1,265 +1,265 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
7
|
-
const { getProvider } = require('./providers');
|
|
8
|
-
const { defaultCompatBlock } = require('./compat');
|
|
9
|
-
const { getBuildPaths } = require('./build/cache');
|
|
10
|
-
const { resolveSourceAuth } = require('./auth');
|
|
11
|
-
const log = require('./logger');
|
|
12
|
-
|
|
13
|
-
const WYVRNPM_VERSION = require('../package.json').version;
|
|
14
|
-
|
|
15
|
-
const UPLOAD_SIDECAR = '.uploaded-to.json';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Record a successful upload in a sidecar file alongside the build-cache
|
|
19
|
-
* entry. Append-only so repeated uploads to different destinations all
|
|
20
|
-
* leave a trail. Purely informational — nothing in the install/build flow
|
|
21
|
-
* branches on these files. Wiped by `wyvrnpm clean --uploaded-built`.
|
|
22
|
-
*
|
|
23
|
-
* @param {{ name: string, version: string, profileHash: string, source: string, contentSha256: string }} args
|
|
24
|
-
*/
|
|
25
|
-
function recordUploadSidecar({ name, version, profileHash, source, contentSha256 }) {
|
|
26
|
-
try {
|
|
27
|
-
const paths = getBuildPaths({ name, version, profileHash });
|
|
28
|
-
if (!fs.existsSync(paths.root)) return;
|
|
29
|
-
const sidecarPath = path.join(paths.root, UPLOAD_SIDECAR);
|
|
30
|
-
let doc = { uploads: [] };
|
|
31
|
-
if (fs.existsSync(sidecarPath)) {
|
|
32
|
-
try {
|
|
33
|
-
const raw = JSON.parse(fs.readFileSync(sidecarPath, 'utf8'));
|
|
34
|
-
if (Array.isArray(raw?.uploads)) doc = raw;
|
|
35
|
-
} catch { /* corrupt sidecar — overwrite with fresh doc */ }
|
|
36
|
-
}
|
|
37
|
-
doc.uploads.push({
|
|
38
|
-
uploadedAt: new Date().toISOString(),
|
|
39
|
-
source,
|
|
40
|
-
profileHash,
|
|
41
|
-
contentSha256,
|
|
42
|
-
});
|
|
43
|
-
fs.writeFileSync(sidecarPath, JSON.stringify(doc, null, 2) + '\n', 'utf8');
|
|
44
|
-
} catch {
|
|
45
|
-
// Sidecar bookkeeping is best-effort — never break an install over it.
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Upload a freshly source-built artefact back to the v2 registry under the
|
|
51
|
-
* consumer's profileHash, so subsequent consumers with the same profile get
|
|
52
|
-
* a direct download instead of re-cloning and re-compiling.
|
|
53
|
-
*
|
|
54
|
-
* See PLAN-UPLOAD-BUILT.md for the full design. Key properties:
|
|
55
|
-
*
|
|
56
|
-
* - Opt-in. Only called when the user passes `--upload-built` at install.
|
|
57
|
-
* - Skips silently if the registry already has this (name, version,
|
|
58
|
-
* profileHash) — the common case once the first teammate has uploaded.
|
|
59
|
-
* - Never overwrites. A consumer with write credentials cannot clobber an
|
|
60
|
-
* existing artefact; that still requires `publish --force` from the
|
|
61
|
-
* source repo.
|
|
62
|
-
* - Leaves `latest.json` and the `latest` pointer in `versions.json`
|
|
63
|
-
* alone. A consumer upload is a new profileHash of an existing version,
|
|
64
|
-
* not a new version release. Honoured via `updateLatest: false`.
|
|
65
|
-
* - Records provenance in the uploaded manifest's `uploadedBy` block.
|
|
66
|
-
* Absence of this block implies author-publish by convention.
|
|
67
|
-
*
|
|
68
|
-
* @param {object} args
|
|
69
|
-
* @param {string} args.name
|
|
70
|
-
* @param {string} args.version
|
|
71
|
-
* @param {import('./profile').WyvrnProfile} args.profile
|
|
72
|
-
* @param {string} args.profileHash
|
|
73
|
-
* @param {string} args.artefactPath Absolute path to the built zip in %LOCALAPPDATA%\wyvrnpm\bc\.
|
|
74
|
-
* @param {string} args.clonedManifestPath Absolute path to the cloned-source wyvrn.json.
|
|
75
|
-
* @param {string} args.contentSha256 SHA256 of the built zip.
|
|
76
|
-
* @param {string} args.gitRepo Source repo URL recorded at publish.
|
|
77
|
-
* @param {string} args.gitSha Source commit recorded at publish.
|
|
78
|
-
* @param {string} args.uploadSource Destination URL (resolved from --upload-source or config).
|
|
79
|
-
* @param {object} [args.uploadAuth] { awsProfile?, token? }
|
|
80
|
-
* @param {Record<string, any>|null} [args.options] Effective options for this build (fed through to buildSettings.options).
|
|
81
|
-
* @param {string} [args.toolchainHash] Optional SHA of the generated wyvrn_toolchain.cmake.
|
|
82
|
-
* @returns {Promise<{ uploaded: boolean, reason: string }>}
|
|
83
|
-
*/
|
|
84
|
-
async function uploadSourceBuiltArtefact(args) {
|
|
85
|
-
const {
|
|
86
|
-
name, version, profile, profileHash,
|
|
87
|
-
artefactPath, clonedManifestPath,
|
|
88
|
-
contentSha256, gitRepo, gitSha,
|
|
89
|
-
uploadSource, uploadAuth = {},
|
|
90
|
-
options = null,
|
|
91
|
-
toolchainHash,
|
|
92
|
-
} = args;
|
|
93
|
-
|
|
94
|
-
if (!uploadSource) {
|
|
95
|
-
return { uploaded: false, reason: 'no upload source resolved' };
|
|
96
|
-
}
|
|
97
|
-
if (!fs.existsSync(artefactPath)) {
|
|
98
|
-
return { uploaded: false, reason: `artefact zip missing at ${artefactPath}` };
|
|
99
|
-
}
|
|
100
|
-
if (!fs.existsSync(clonedManifestPath)) {
|
|
101
|
-
// Cache-hit path on a previously-trimmed cache, or the source dir was
|
|
102
|
-
// manually deleted. We have the binary but not the manifest we would
|
|
103
|
-
// have uploaded alongside it. Surface it — nothing silent.
|
|
104
|
-
return { uploaded: false, reason: `cloned manifest missing at ${clonedManifestPath}` };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
let provider;
|
|
108
|
-
try {
|
|
109
|
-
provider = getProvider(uploadSource);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
return { uploaded: false, reason: `no provider for ${uploadSource}: ${err.message}` };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const { awsProfile, token } = uploadAuth;
|
|
115
|
-
|
|
116
|
-
// ── Skip-if-exists ────────────────────────────────────────────────────────
|
|
117
|
-
const exists = await provider.v2Exists({
|
|
118
|
-
source: uploadSource, name, version, profileHash, awsProfile, token,
|
|
119
|
-
});
|
|
120
|
-
if (exists) {
|
|
121
|
-
log.info(`upload-built: ${name}@${version} [${profileHash}] already on ${uploadSource} — skipping`);
|
|
122
|
-
return { uploaded: false, reason: 'already exists on registry' };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ── Prepare manifest: cloned wyvrn.json + compat block + uploadedBy ──────
|
|
126
|
-
const rawManifest = JSON.parse(fs.readFileSync(clonedManifestPath, 'utf8'));
|
|
127
|
-
const manifestForUpload = {
|
|
128
|
-
...rawManifest,
|
|
129
|
-
compatibility: rawManifest.compatibility ?? defaultCompatBlock(),
|
|
130
|
-
uploadedBy: {
|
|
131
|
-
kind: 'source-build',
|
|
132
|
-
profileHash,
|
|
133
|
-
builtFromGit: gitRepo ?? null,
|
|
134
|
-
builtFromSha: gitSha ?? null,
|
|
135
|
-
...(toolchainHash ? { toolchainHash } : {}),
|
|
136
|
-
uploadedAt: new Date().toISOString(),
|
|
137
|
-
wyvrnpmVersion: WYVRNPM_VERSION,
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const tmpManifestPath = path.join(
|
|
142
|
-
os.tmpdir(),
|
|
143
|
-
`wyvrnpm-upload-${name}-${version}-${profileHash}-${Date.now()}.json`,
|
|
144
|
-
);
|
|
145
|
-
fs.writeFileSync(tmpManifestPath, JSON.stringify(manifestForUpload, null, 2), 'utf8');
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
log.info(`upload-built: ${name}@${version} [${profileHash}] → ${uploadSource}`);
|
|
149
|
-
await provider.v2Publish(
|
|
150
|
-
{ manifest: tmpManifestPath, zip: artefactPath },
|
|
151
|
-
{
|
|
152
|
-
source: uploadSource,
|
|
153
|
-
name, version, profileHash,
|
|
154
|
-
buildSettings: options ? { ...profile, options } : profile,
|
|
155
|
-
contentSha256,
|
|
156
|
-
gitSha: gitSha ?? null,
|
|
157
|
-
gitRepo: gitRepo ?? null,
|
|
158
|
-
// Consumer uploads must not reshape the "latest" pointer — see
|
|
159
|
-
// PLAN-UPLOAD-BUILT §3.3.
|
|
160
|
-
updateLatest: false,
|
|
161
|
-
uploadedBy: manifestForUpload.uploadedBy,
|
|
162
|
-
awsProfile,
|
|
163
|
-
token,
|
|
164
|
-
},
|
|
165
|
-
);
|
|
166
|
-
log.success(`upload-built: ${name}@${version} [${profileHash}] uploaded`);
|
|
167
|
-
recordUploadSidecar({ name, version, profileHash, source: uploadSource, contentSha256 });
|
|
168
|
-
return { uploaded: true, reason: 'ok' };
|
|
169
|
-
} catch (err) {
|
|
170
|
-
// The caller wraps us in try/catch too, but returning {uploaded:false}
|
|
171
|
-
// here keeps the happy path type-stable.
|
|
172
|
-
return { uploaded: false, reason: `upload failed: ${err.message}` };
|
|
173
|
-
} finally {
|
|
174
|
-
try { fs.unlinkSync(tmpManifestPath); } catch { /* ignore */ }
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Resolve `--upload-source` (URL or configured-publish-source name) + auth
|
|
180
|
-
* into an object the orchestrator understands. Mirrors the resolution
|
|
181
|
-
* pattern in src/commands/publish.js so the two flows behave identically.
|
|
182
|
-
*
|
|
183
|
-
* Returns null if no destination can be resolved — caller decides whether
|
|
184
|
-
* that is fatal (it is, if --upload-built was passed) or just skip-worthy.
|
|
185
|
-
*
|
|
186
|
-
* @param {object} argv
|
|
187
|
-
* @param {string|undefined} argv.uploadSource
|
|
188
|
-
* @param {string|undefined} argv.awsProfile
|
|
189
|
-
* @param {string|undefined} argv.token
|
|
190
|
-
* @param {{ publishSources: Array<{name:string, url:string, profile?:string, token?:string}> }} config
|
|
191
|
-
* @returns {{ uploadSource: string, uploadAuth: { awsProfile?: string, token?: string } } | null}
|
|
192
|
-
*/
|
|
193
|
-
function resolveUploadDestination(argv, config) {
|
|
194
|
-
let src = argv.uploadSource;
|
|
195
|
-
let awsProfile = argv.awsProfile;
|
|
196
|
-
let token = argv.token;
|
|
197
|
-
let sourceEntry = null;
|
|
198
|
-
|
|
199
|
-
const publishSources = config.publishSources ?? [];
|
|
200
|
-
|
|
201
|
-
if (src) {
|
|
202
|
-
// Check if it matches a configured publish source by name.
|
|
203
|
-
const named = publishSources.find((s) => s.name === src);
|
|
204
|
-
if (named) {
|
|
205
|
-
src = named.url;
|
|
206
|
-
awsProfile = awsProfile ?? named.profile;
|
|
207
|
-
sourceEntry = named;
|
|
208
|
-
log.info(`upload-built: using publish source "${named.name}" from config`);
|
|
209
|
-
}
|
|
210
|
-
} else if (publishSources.length > 0) {
|
|
211
|
-
const first = publishSources[0];
|
|
212
|
-
src = first.url;
|
|
213
|
-
awsProfile = awsProfile ?? first.profile;
|
|
214
|
-
sourceEntry = first;
|
|
215
|
-
log.info(`upload-built: using first configured publish source "${first.name}"`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Fall back to the matched source entry's own token / tokenEnv only if
|
|
219
|
-
// the caller didn't pass one already (argv.token wins). resolveSourceAuth
|
|
220
|
-
// handles the literal-vs-env-backed precedence per side.
|
|
221
|
-
if (token === undefined || token === null) {
|
|
222
|
-
token = resolveSourceAuth({}, sourceEntry).token;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (!src) return null;
|
|
226
|
-
return { uploadSource: src, uploadAuth: { awsProfile, token } };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Create an empty accumulator for aggregated upload outcomes. Pass the
|
|
231
|
-
* returned object as `options.uploadStats` to `downloadDependencies`, then
|
|
232
|
-
* feed it to `formatUploadSummary()` at the end of the install.
|
|
233
|
-
*
|
|
234
|
-
* @returns {{ uploaded: Array<object>, skipped: Array<object>, failed: Array<object> }}
|
|
235
|
-
*/
|
|
236
|
-
function createUploadStats() {
|
|
237
|
-
return { uploaded: [], skipped: [], failed: [] };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Produce the one-line aggregated summary shown at the end of an install
|
|
242
|
-
* when `--upload-built` was passed. Returns null when nothing was attempted
|
|
243
|
-
* (so the caller can skip printing).
|
|
244
|
-
*
|
|
245
|
-
* @param {{ uploaded: Array, skipped: Array, failed: Array }} stats
|
|
246
|
-
* @returns {string|null}
|
|
247
|
-
*/
|
|
248
|
-
function formatUploadSummary(stats) {
|
|
249
|
-
if (!stats) return null;
|
|
250
|
-
const total = stats.uploaded.length + stats.skipped.length + stats.failed.length;
|
|
251
|
-
if (total === 0) return null;
|
|
252
|
-
const n = (arr, singular) => `${arr.length} ${singular}${arr.length === 1 ? '' : 's'}`;
|
|
253
|
-
return (
|
|
254
|
-
`upload-built summary: ${n(stats.uploaded, 'artefact')} uploaded, ` +
|
|
255
|
-
`${stats.skipped.length} skipped (already existed), ` +
|
|
256
|
-
`${stats.failed.length} failed`
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
module.exports = {
|
|
261
|
-
uploadSourceBuiltArtefact,
|
|
262
|
-
resolveUploadDestination,
|
|
263
|
-
createUploadStats,
|
|
264
|
-
formatUploadSummary,
|
|
265
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { getProvider } = require('./providers');
|
|
8
|
+
const { defaultCompatBlock } = require('./compat');
|
|
9
|
+
const { getBuildPaths } = require('./build/cache');
|
|
10
|
+
const { resolveSourceAuth } = require('./auth');
|
|
11
|
+
const log = require('./logger');
|
|
12
|
+
|
|
13
|
+
const WYVRNPM_VERSION = require('../package.json').version;
|
|
14
|
+
|
|
15
|
+
const UPLOAD_SIDECAR = '.uploaded-to.json';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Record a successful upload in a sidecar file alongside the build-cache
|
|
19
|
+
* entry. Append-only so repeated uploads to different destinations all
|
|
20
|
+
* leave a trail. Purely informational — nothing in the install/build flow
|
|
21
|
+
* branches on these files. Wiped by `wyvrnpm clean --uploaded-built`.
|
|
22
|
+
*
|
|
23
|
+
* @param {{ name: string, version: string, profileHash: string, source: string, contentSha256: string }} args
|
|
24
|
+
*/
|
|
25
|
+
function recordUploadSidecar({ name, version, profileHash, source, contentSha256 }) {
|
|
26
|
+
try {
|
|
27
|
+
const paths = getBuildPaths({ name, version, profileHash });
|
|
28
|
+
if (!fs.existsSync(paths.root)) return;
|
|
29
|
+
const sidecarPath = path.join(paths.root, UPLOAD_SIDECAR);
|
|
30
|
+
let doc = { uploads: [] };
|
|
31
|
+
if (fs.existsSync(sidecarPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = JSON.parse(fs.readFileSync(sidecarPath, 'utf8'));
|
|
34
|
+
if (Array.isArray(raw?.uploads)) doc = raw;
|
|
35
|
+
} catch { /* corrupt sidecar — overwrite with fresh doc */ }
|
|
36
|
+
}
|
|
37
|
+
doc.uploads.push({
|
|
38
|
+
uploadedAt: new Date().toISOString(),
|
|
39
|
+
source,
|
|
40
|
+
profileHash,
|
|
41
|
+
contentSha256,
|
|
42
|
+
});
|
|
43
|
+
fs.writeFileSync(sidecarPath, JSON.stringify(doc, null, 2) + '\n', 'utf8');
|
|
44
|
+
} catch {
|
|
45
|
+
// Sidecar bookkeeping is best-effort — never break an install over it.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Upload a freshly source-built artefact back to the v2 registry under the
|
|
51
|
+
* consumer's profileHash, so subsequent consumers with the same profile get
|
|
52
|
+
* a direct download instead of re-cloning and re-compiling.
|
|
53
|
+
*
|
|
54
|
+
* See PLAN-UPLOAD-BUILT.md for the full design. Key properties:
|
|
55
|
+
*
|
|
56
|
+
* - Opt-in. Only called when the user passes `--upload-built` at install.
|
|
57
|
+
* - Skips silently if the registry already has this (name, version,
|
|
58
|
+
* profileHash) — the common case once the first teammate has uploaded.
|
|
59
|
+
* - Never overwrites. A consumer with write credentials cannot clobber an
|
|
60
|
+
* existing artefact; that still requires `publish --force` from the
|
|
61
|
+
* source repo.
|
|
62
|
+
* - Leaves `latest.json` and the `latest` pointer in `versions.json`
|
|
63
|
+
* alone. A consumer upload is a new profileHash of an existing version,
|
|
64
|
+
* not a new version release. Honoured via `updateLatest: false`.
|
|
65
|
+
* - Records provenance in the uploaded manifest's `uploadedBy` block.
|
|
66
|
+
* Absence of this block implies author-publish by convention.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} args
|
|
69
|
+
* @param {string} args.name
|
|
70
|
+
* @param {string} args.version
|
|
71
|
+
* @param {import('./profile').WyvrnProfile} args.profile
|
|
72
|
+
* @param {string} args.profileHash
|
|
73
|
+
* @param {string} args.artefactPath Absolute path to the built zip in %LOCALAPPDATA%\wyvrnpm\bc\.
|
|
74
|
+
* @param {string} args.clonedManifestPath Absolute path to the cloned-source wyvrn.json.
|
|
75
|
+
* @param {string} args.contentSha256 SHA256 of the built zip.
|
|
76
|
+
* @param {string} args.gitRepo Source repo URL recorded at publish.
|
|
77
|
+
* @param {string} args.gitSha Source commit recorded at publish.
|
|
78
|
+
* @param {string} args.uploadSource Destination URL (resolved from --upload-source or config).
|
|
79
|
+
* @param {object} [args.uploadAuth] { awsProfile?, token? }
|
|
80
|
+
* @param {Record<string, any>|null} [args.options] Effective options for this build (fed through to buildSettings.options).
|
|
81
|
+
* @param {string} [args.toolchainHash] Optional SHA of the generated wyvrn_toolchain.cmake.
|
|
82
|
+
* @returns {Promise<{ uploaded: boolean, reason: string }>}
|
|
83
|
+
*/
|
|
84
|
+
async function uploadSourceBuiltArtefact(args) {
|
|
85
|
+
const {
|
|
86
|
+
name, version, profile, profileHash,
|
|
87
|
+
artefactPath, clonedManifestPath,
|
|
88
|
+
contentSha256, gitRepo, gitSha,
|
|
89
|
+
uploadSource, uploadAuth = {},
|
|
90
|
+
options = null,
|
|
91
|
+
toolchainHash,
|
|
92
|
+
} = args;
|
|
93
|
+
|
|
94
|
+
if (!uploadSource) {
|
|
95
|
+
return { uploaded: false, reason: 'no upload source resolved' };
|
|
96
|
+
}
|
|
97
|
+
if (!fs.existsSync(artefactPath)) {
|
|
98
|
+
return { uploaded: false, reason: `artefact zip missing at ${artefactPath}` };
|
|
99
|
+
}
|
|
100
|
+
if (!fs.existsSync(clonedManifestPath)) {
|
|
101
|
+
// Cache-hit path on a previously-trimmed cache, or the source dir was
|
|
102
|
+
// manually deleted. We have the binary but not the manifest we would
|
|
103
|
+
// have uploaded alongside it. Surface it — nothing silent.
|
|
104
|
+
return { uploaded: false, reason: `cloned manifest missing at ${clonedManifestPath}` };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let provider;
|
|
108
|
+
try {
|
|
109
|
+
provider = getProvider(uploadSource);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return { uploaded: false, reason: `no provider for ${uploadSource}: ${err.message}` };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { awsProfile, token } = uploadAuth;
|
|
115
|
+
|
|
116
|
+
// ── Skip-if-exists ────────────────────────────────────────────────────────
|
|
117
|
+
const exists = await provider.v2Exists({
|
|
118
|
+
source: uploadSource, name, version, profileHash, awsProfile, token,
|
|
119
|
+
});
|
|
120
|
+
if (exists) {
|
|
121
|
+
log.info(`upload-built: ${name}@${version} [${profileHash}] already on ${uploadSource} — skipping`);
|
|
122
|
+
return { uploaded: false, reason: 'already exists on registry' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Prepare manifest: cloned wyvrn.json + compat block + uploadedBy ──────
|
|
126
|
+
const rawManifest = JSON.parse(fs.readFileSync(clonedManifestPath, 'utf8'));
|
|
127
|
+
const manifestForUpload = {
|
|
128
|
+
...rawManifest,
|
|
129
|
+
compatibility: rawManifest.compatibility ?? defaultCompatBlock(),
|
|
130
|
+
uploadedBy: {
|
|
131
|
+
kind: 'source-build',
|
|
132
|
+
profileHash,
|
|
133
|
+
builtFromGit: gitRepo ?? null,
|
|
134
|
+
builtFromSha: gitSha ?? null,
|
|
135
|
+
...(toolchainHash ? { toolchainHash } : {}),
|
|
136
|
+
uploadedAt: new Date().toISOString(),
|
|
137
|
+
wyvrnpmVersion: WYVRNPM_VERSION,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const tmpManifestPath = path.join(
|
|
142
|
+
os.tmpdir(),
|
|
143
|
+
`wyvrnpm-upload-${name}-${version}-${profileHash}-${Date.now()}.json`,
|
|
144
|
+
);
|
|
145
|
+
fs.writeFileSync(tmpManifestPath, JSON.stringify(manifestForUpload, null, 2), 'utf8');
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
log.info(`upload-built: ${name}@${version} [${profileHash}] → ${uploadSource}`);
|
|
149
|
+
await provider.v2Publish(
|
|
150
|
+
{ manifest: tmpManifestPath, zip: artefactPath },
|
|
151
|
+
{
|
|
152
|
+
source: uploadSource,
|
|
153
|
+
name, version, profileHash,
|
|
154
|
+
buildSettings: options ? { ...profile, options } : profile,
|
|
155
|
+
contentSha256,
|
|
156
|
+
gitSha: gitSha ?? null,
|
|
157
|
+
gitRepo: gitRepo ?? null,
|
|
158
|
+
// Consumer uploads must not reshape the "latest" pointer — see
|
|
159
|
+
// PLAN-UPLOAD-BUILT §3.3.
|
|
160
|
+
updateLatest: false,
|
|
161
|
+
uploadedBy: manifestForUpload.uploadedBy,
|
|
162
|
+
awsProfile,
|
|
163
|
+
token,
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
log.success(`upload-built: ${name}@${version} [${profileHash}] uploaded`);
|
|
167
|
+
recordUploadSidecar({ name, version, profileHash, source: uploadSource, contentSha256 });
|
|
168
|
+
return { uploaded: true, reason: 'ok' };
|
|
169
|
+
} catch (err) {
|
|
170
|
+
// The caller wraps us in try/catch too, but returning {uploaded:false}
|
|
171
|
+
// here keeps the happy path type-stable.
|
|
172
|
+
return { uploaded: false, reason: `upload failed: ${err.message}` };
|
|
173
|
+
} finally {
|
|
174
|
+
try { fs.unlinkSync(tmpManifestPath); } catch { /* ignore */ }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Resolve `--upload-source` (URL or configured-publish-source name) + auth
|
|
180
|
+
* into an object the orchestrator understands. Mirrors the resolution
|
|
181
|
+
* pattern in src/commands/publish.js so the two flows behave identically.
|
|
182
|
+
*
|
|
183
|
+
* Returns null if no destination can be resolved — caller decides whether
|
|
184
|
+
* that is fatal (it is, if --upload-built was passed) or just skip-worthy.
|
|
185
|
+
*
|
|
186
|
+
* @param {object} argv
|
|
187
|
+
* @param {string|undefined} argv.uploadSource
|
|
188
|
+
* @param {string|undefined} argv.awsProfile
|
|
189
|
+
* @param {string|undefined} argv.token
|
|
190
|
+
* @param {{ publishSources: Array<{name:string, url:string, profile?:string, token?:string}> }} config
|
|
191
|
+
* @returns {{ uploadSource: string, uploadAuth: { awsProfile?: string, token?: string } } | null}
|
|
192
|
+
*/
|
|
193
|
+
function resolveUploadDestination(argv, config) {
|
|
194
|
+
let src = argv.uploadSource;
|
|
195
|
+
let awsProfile = argv.awsProfile;
|
|
196
|
+
let token = argv.token;
|
|
197
|
+
let sourceEntry = null;
|
|
198
|
+
|
|
199
|
+
const publishSources = config.publishSources ?? [];
|
|
200
|
+
|
|
201
|
+
if (src) {
|
|
202
|
+
// Check if it matches a configured publish source by name.
|
|
203
|
+
const named = publishSources.find((s) => s.name === src);
|
|
204
|
+
if (named) {
|
|
205
|
+
src = named.url;
|
|
206
|
+
awsProfile = awsProfile ?? named.profile;
|
|
207
|
+
sourceEntry = named;
|
|
208
|
+
log.info(`upload-built: using publish source "${named.name}" from config`);
|
|
209
|
+
}
|
|
210
|
+
} else if (publishSources.length > 0) {
|
|
211
|
+
const first = publishSources[0];
|
|
212
|
+
src = first.url;
|
|
213
|
+
awsProfile = awsProfile ?? first.profile;
|
|
214
|
+
sourceEntry = first;
|
|
215
|
+
log.info(`upload-built: using first configured publish source "${first.name}"`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Fall back to the matched source entry's own token / tokenEnv only if
|
|
219
|
+
// the caller didn't pass one already (argv.token wins). resolveSourceAuth
|
|
220
|
+
// handles the literal-vs-env-backed precedence per side.
|
|
221
|
+
if (token === undefined || token === null) {
|
|
222
|
+
token = resolveSourceAuth({}, sourceEntry).token;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!src) return null;
|
|
226
|
+
return { uploadSource: src, uploadAuth: { awsProfile, token } };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create an empty accumulator for aggregated upload outcomes. Pass the
|
|
231
|
+
* returned object as `options.uploadStats` to `downloadDependencies`, then
|
|
232
|
+
* feed it to `formatUploadSummary()` at the end of the install.
|
|
233
|
+
*
|
|
234
|
+
* @returns {{ uploaded: Array<object>, skipped: Array<object>, failed: Array<object> }}
|
|
235
|
+
*/
|
|
236
|
+
function createUploadStats() {
|
|
237
|
+
return { uploaded: [], skipped: [], failed: [] };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Produce the one-line aggregated summary shown at the end of an install
|
|
242
|
+
* when `--upload-built` was passed. Returns null when nothing was attempted
|
|
243
|
+
* (so the caller can skip printing).
|
|
244
|
+
*
|
|
245
|
+
* @param {{ uploaded: Array, skipped: Array, failed: Array }} stats
|
|
246
|
+
* @returns {string|null}
|
|
247
|
+
*/
|
|
248
|
+
function formatUploadSummary(stats) {
|
|
249
|
+
if (!stats) return null;
|
|
250
|
+
const total = stats.uploaded.length + stats.skipped.length + stats.failed.length;
|
|
251
|
+
if (total === 0) return null;
|
|
252
|
+
const n = (arr, singular) => `${arr.length} ${singular}${arr.length === 1 ? '' : 's'}`;
|
|
253
|
+
return (
|
|
254
|
+
`upload-built summary: ${n(stats.uploaded, 'artefact')} uploaded, ` +
|
|
255
|
+
`${stats.skipped.length} skipped (already existed), ` +
|
|
256
|
+
`${stats.failed.length} failed`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
uploadSourceBuiltArtefact,
|
|
262
|
+
resolveUploadDestination,
|
|
263
|
+
createUploadStats,
|
|
264
|
+
formatUploadSummary,
|
|
265
|
+
};
|