wyvrnpm 2.1.0 → 2.4.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 +325 -6
- package/bin/wyvrn.js +167 -14
- package/claude/skills/wyvrnpm.skill +0 -0
- package/cmake/macros.cmake +51 -0
- package/package.json +2 -1
- package/src/build/cache.js +47 -0
- package/src/build/index.js +42 -11
- package/src/build/recipe.js +30 -4
- package/src/commands/add.js +140 -0
- package/src/commands/build.js +96 -12
- package/src/commands/clean.js +39 -15
- package/src/commands/install-skill.js +107 -0
- package/src/commands/install.js +202 -5
- package/src/commands/publish.js +150 -35
- package/src/commands/show.js +237 -0
- package/src/compat.js +53 -12
- package/src/download.js +65 -3
- package/src/ignore.js +118 -0
- package/src/logger.js +26 -10
- package/src/manifest.js +12 -7
- package/src/options.js +303 -0
- package/src/profile.js +28 -4
- package/src/providers/base.js +16 -1
- package/src/providers/file.js +8 -3
- package/src/providers/http.js +12 -8
- package/src/providers/s3.js +11 -7
- package/src/resolve.js +171 -13
- package/src/toolchain/index.js +71 -0
- package/src/toolchain/presets.js +125 -16
- package/src/toolchain/template.cmake +11 -0
- package/src/upload-built.js +256 -0
- package/src/version-range.js +301 -0
package/src/toolchain/presets.js
CHANGED
|
@@ -4,6 +4,21 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
const VENDOR_KEY = 'wyvrnpm/generated';
|
|
7
|
+
|
|
8
|
+
// Local copy of the arch→VS-platform mapping. Duplicated here rather than
|
|
9
|
+
// imported from ./index so preset generation can run standalone (keeps
|
|
10
|
+
// the dependency graph simple — presets.js doesn't otherwise need the
|
|
11
|
+
// toolchain generator). Keep in sync with `archToVsPlatform` in index.js.
|
|
12
|
+
const ARCH_VS_PLATFORM = {
|
|
13
|
+
x86_64: 'x64',
|
|
14
|
+
x86: 'Win32',
|
|
15
|
+
armv8: 'ARM64',
|
|
16
|
+
armv7: 'ARM',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function isVsGenerator(name) {
|
|
20
|
+
return typeof name === 'string' && /^Visual Studio \d/.test(name);
|
|
21
|
+
}
|
|
7
22
|
const SCHEMA_VERSION = 3; // requires CMake ≥ 3.21
|
|
8
23
|
|
|
9
24
|
/**
|
|
@@ -33,22 +48,83 @@ function isWyvrnGenerated(presets) {
|
|
|
33
48
|
/**
|
|
34
49
|
* Build the configure preset object for the active profile.
|
|
35
50
|
*
|
|
51
|
+
* When the project has its own `build` recipe (i.e. it's a publishable
|
|
52
|
+
* library with `build.generator` / `build.configure` declared), those are
|
|
53
|
+
* propagated into the generated preset so `wyvrnpm build` locally honours
|
|
54
|
+
* the same knobs the source-build + publish paths use. Without this, a
|
|
55
|
+
* publisher's recipe is effectively advisory at install-time (F15 fix).
|
|
56
|
+
*
|
|
36
57
|
* @param {object} args
|
|
37
58
|
* @param {string} args.profileName
|
|
38
59
|
* @param {string} args.profileHash
|
|
39
|
-
* @param {string} args.toolchainRelPath
|
|
40
|
-
*
|
|
60
|
+
* @param {string} args.toolchainRelPath Relative from project root, CMake-style
|
|
61
|
+
* forward slashes.
|
|
62
|
+
* @param {string|null} [args.generator] Single generator name to pin on the
|
|
63
|
+
* preset (e.g. "Ninja Multi-Config").
|
|
64
|
+
* Null / undefined → let CMake pick.
|
|
65
|
+
* @param {Record<string,string>|null} [args.extraCacheVariables]
|
|
66
|
+
* Merged into cacheVariables alongside
|
|
67
|
+
* `CMAKE_TOOLCHAIN_FILE`. Usually
|
|
68
|
+
* derived from the project's own
|
|
69
|
+
* `build.configure` -D args.
|
|
70
|
+
* @param {string|null} [args.profileArch] Active profile arch — used to pin
|
|
71
|
+
* `architecture` for VS generators
|
|
72
|
+
* so `cmake --preset` produces
|
|
73
|
+
* binaries matching the profile
|
|
74
|
+
* (VS otherwise defaults to x64).
|
|
41
75
|
*/
|
|
42
|
-
function buildConfigurePreset({
|
|
43
|
-
|
|
76
|
+
function buildConfigurePreset({
|
|
77
|
+
profileName,
|
|
78
|
+
profileHash,
|
|
79
|
+
toolchainRelPath,
|
|
80
|
+
generator = null,
|
|
81
|
+
extraCacheVariables = null,
|
|
82
|
+
installDir = null,
|
|
83
|
+
profileArch = null,
|
|
84
|
+
}) {
|
|
85
|
+
const binaryDir = `\${sourceDir}/build/wyvrn-${profileName}`;
|
|
86
|
+
// Spread `extraCacheVariables` FIRST, then the wyvrn-owned entries. This
|
|
87
|
+
// means the recipe cannot redirect `CMAKE_TOOLCHAIN_FILE` — our dep
|
|
88
|
+
// injection is always the authoritative toolchain pointer. A malicious
|
|
89
|
+
// or sloppy recipe trying to override it is silently ignored, not
|
|
90
|
+
// quietly obeyed. Same protection applies to `CMAKE_INSTALL_PREFIX`:
|
|
91
|
+
// if a recipe declares `build.installDir`, we resolve it under the
|
|
92
|
+
// binary dir; any cacheVariables override is overwritten below.
|
|
93
|
+
const cacheVariables = {
|
|
94
|
+
...(extraCacheVariables ?? {}),
|
|
95
|
+
CMAKE_TOOLCHAIN_FILE: `\${sourceDir}/${toolchainRelPath}`,
|
|
96
|
+
};
|
|
97
|
+
if (installDir) {
|
|
98
|
+
// `installDir` in wyvrn.json is defined as relative to the binary dir
|
|
99
|
+
// (same semantics as the source-build path, which passes
|
|
100
|
+
// -DCMAKE_INSTALL_PREFIX=<buildDir>/<installDir>). Without this, a
|
|
101
|
+
// `wyvrnpm build --install` falls back to CMake's system default —
|
|
102
|
+
// on Windows that's `C:/Program Files (x86)/<Project>`, which needs
|
|
103
|
+
// admin rights and almost always fails.
|
|
104
|
+
cacheVariables.CMAKE_INSTALL_PREFIX = `${binaryDir}/${installDir}`;
|
|
105
|
+
}
|
|
106
|
+
const preset = {
|
|
44
107
|
name: `wyvrn-${profileName}`,
|
|
45
108
|
displayName: `wyvrnpm (${profileName})`,
|
|
46
109
|
description: `Generated by wyvrnpm install — profile hash ${profileHash}`,
|
|
47
|
-
binaryDir
|
|
48
|
-
cacheVariables
|
|
49
|
-
CMAKE_TOOLCHAIN_FILE: `\${sourceDir}/${toolchainRelPath}`,
|
|
50
|
-
},
|
|
110
|
+
binaryDir,
|
|
111
|
+
cacheVariables,
|
|
51
112
|
};
|
|
113
|
+
if (generator) preset.generator = generator;
|
|
114
|
+
|
|
115
|
+
// Visual Studio generators default to x64 when `architecture` is
|
|
116
|
+
// unspecified — which silently overrides the profile arch and makes
|
|
117
|
+
// find_package() reject 32-bit installed packages with a pointer-size
|
|
118
|
+
// mismatch. Pin `architecture` from the profile so `cmake --preset`
|
|
119
|
+
// produces the right artefacts end-to-end.
|
|
120
|
+
if (isVsGenerator(generator) && profileArch && ARCH_VS_PLATFORM[profileArch]) {
|
|
121
|
+
preset.architecture = {
|
|
122
|
+
value: ARCH_VS_PLATFORM[profileArch],
|
|
123
|
+
strategy: 'set',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return preset;
|
|
52
128
|
}
|
|
53
129
|
|
|
54
130
|
/**
|
|
@@ -78,7 +154,15 @@ function buildBuildPresets(profileName) {
|
|
|
78
154
|
/**
|
|
79
155
|
* Build a fresh presets file from scratch (no prior content to merge).
|
|
80
156
|
*/
|
|
81
|
-
function buildFreshPresetsFile({
|
|
157
|
+
function buildFreshPresetsFile({
|
|
158
|
+
profileName,
|
|
159
|
+
profileHash,
|
|
160
|
+
toolchainRelPath,
|
|
161
|
+
generator = null,
|
|
162
|
+
extraCacheVariables = null,
|
|
163
|
+
installDir = null,
|
|
164
|
+
profileArch = null,
|
|
165
|
+
}) {
|
|
82
166
|
return {
|
|
83
167
|
version: SCHEMA_VERSION,
|
|
84
168
|
vendor: {
|
|
@@ -88,8 +172,10 @@ function buildFreshPresetsFile({ profileName, profileHash, toolchainRelPath }) {
|
|
|
88
172
|
generatedAt: new Date().toISOString(),
|
|
89
173
|
},
|
|
90
174
|
},
|
|
91
|
-
configurePresets: [buildConfigurePreset({
|
|
92
|
-
|
|
175
|
+
configurePresets: [buildConfigurePreset({
|
|
176
|
+
profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch,
|
|
177
|
+
})],
|
|
178
|
+
buildPresets: buildBuildPresets(profileName),
|
|
93
179
|
};
|
|
94
180
|
}
|
|
95
181
|
|
|
@@ -103,8 +189,18 @@ function buildFreshPresetsFile({ profileName, profileHash, toolchainRelPath }) {
|
|
|
103
189
|
* @param {object} args forwarded to the preset builders
|
|
104
190
|
* @returns {object}
|
|
105
191
|
*/
|
|
106
|
-
function mergeIntoExisting(existing, {
|
|
107
|
-
|
|
192
|
+
function mergeIntoExisting(existing, {
|
|
193
|
+
profileName,
|
|
194
|
+
profileHash,
|
|
195
|
+
toolchainRelPath,
|
|
196
|
+
generator = null,
|
|
197
|
+
extraCacheVariables = null,
|
|
198
|
+
installDir = null,
|
|
199
|
+
profileArch = null,
|
|
200
|
+
}) {
|
|
201
|
+
const newConfigure = buildConfigurePreset({
|
|
202
|
+
profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch,
|
|
203
|
+
});
|
|
108
204
|
const newBuilds = buildBuildPresets(profileName);
|
|
109
205
|
const newConfigureNames = new Set([newConfigure.name]);
|
|
110
206
|
const newBuildNames = new Set(newBuilds.map((b) => b.name));
|
|
@@ -150,9 +246,22 @@ function mergeIntoExisting(existing, { profileName, profileHash, toolchainRelPat
|
|
|
150
246
|
* @param {string} args.profileName
|
|
151
247
|
* @param {string} args.profileHash
|
|
152
248
|
* @param {string} args.toolchainRelPath CMake-style forward slashes, relative to projectRoot.
|
|
249
|
+
* @param {string|null} [args.generator] Propagated onto the configure preset's `generator` field.
|
|
250
|
+
* @param {Record<string,string>|null} [args.extraCacheVariables]
|
|
251
|
+
* Merged into the configure preset's `cacheVariables`.
|
|
153
252
|
* @returns {{ path: string|null, action: 'created'|'updated'|'skipped', isUser: boolean }}
|
|
154
253
|
*/
|
|
155
|
-
function generateCMakePresets({
|
|
254
|
+
function generateCMakePresets({
|
|
255
|
+
projectRoot,
|
|
256
|
+
profileName,
|
|
257
|
+
profileHash,
|
|
258
|
+
toolchainRelPath,
|
|
259
|
+
generator = null,
|
|
260
|
+
extraCacheVariables = null,
|
|
261
|
+
installDir = null,
|
|
262
|
+
profileArch = null,
|
|
263
|
+
}) {
|
|
264
|
+
const args = { profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch };
|
|
156
265
|
const candidates = [
|
|
157
266
|
{ filePath: path.join(projectRoot, 'CMakePresets.json'), isUser: false },
|
|
158
267
|
{ filePath: path.join(projectRoot, 'CMakeUserPresets.json'), isUser: true },
|
|
@@ -160,14 +269,14 @@ function generateCMakePresets({ projectRoot, profileName, profileHash, toolchain
|
|
|
160
269
|
|
|
161
270
|
for (const { filePath, isUser } of candidates) {
|
|
162
271
|
if (!fs.existsSync(filePath)) {
|
|
163
|
-
const content = buildFreshPresetsFile(
|
|
272
|
+
const content = buildFreshPresetsFile(args);
|
|
164
273
|
fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n', 'utf8');
|
|
165
274
|
return { path: filePath, action: 'created', isUser };
|
|
166
275
|
}
|
|
167
276
|
|
|
168
277
|
const existing = safeReadJson(filePath);
|
|
169
278
|
if (isWyvrnGenerated(existing)) {
|
|
170
|
-
const merged = mergeIntoExisting(existing,
|
|
279
|
+
const merged = mergeIntoExisting(existing, args);
|
|
171
280
|
fs.writeFileSync(filePath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
172
281
|
return { path: filePath, action: 'updated', isUser };
|
|
173
282
|
}
|
|
@@ -17,6 +17,17 @@ set(WYVRN_PROFILE_NAME "{{PROFILE_NAME}}")
|
|
|
17
17
|
set(WYVRN_PROFILE_HASH "{{PROFILE_HASH}}")
|
|
18
18
|
set(WYVRN_PROFILE_OS "{{PROFILE_OS}}")
|
|
19
19
|
set(WYVRN_PROFILE_ARCH "{{PROFILE_ARCH}}")
|
|
20
|
+
|
|
21
|
+
# "64" for 64-bit arches (x86_64, armv8, …), "32" for 32-bit (x86, armv7, …),
|
|
22
|
+
# empty for arches outside the known mapping. Derived at generation time from
|
|
23
|
+
# the profile so it is valid BEFORE `project()` — unlike CMAKE_SIZEOF_VOID_P,
|
|
24
|
+
# which is only set after the first project() call. Intended for suffixing
|
|
25
|
+
# artefact names (e.g. "String${WYVRN_ARCH_SUFFIX}" → String64 / String32) so
|
|
26
|
+
# x64 and x86 builds can coexist on disk without overwriting each other.
|
|
27
|
+
# Call WYVRN_APPLY_ARCH_SUFFIX(<target>) from macros.cmake for the common
|
|
28
|
+
# per-target shape, or reference the variable directly for other uses.
|
|
29
|
+
set(WYVRN_ARCH_SUFFIX "{{PROFILE_ARCH_SUFFIX}}")
|
|
30
|
+
|
|
20
31
|
set(WYVRN_PROFILE_COMPILER "{{PROFILE_COMPILER}}")
|
|
21
32
|
set(WYVRN_PROFILE_COMPILER_VERSION "{{PROFILE_COMPILER_VERSION}}")
|
|
22
33
|
set(WYVRN_PROFILE_CPPSTD "{{PROFILE_CPPSTD}}")
|
|
@@ -0,0 +1,256 @@
|
|
|
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 log = require('./logger');
|
|
11
|
+
|
|
12
|
+
const WYVRNPM_VERSION = require('../package.json').version;
|
|
13
|
+
|
|
14
|
+
const UPLOAD_SIDECAR = '.uploaded-to.json';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Record a successful upload in a sidecar file alongside the build-cache
|
|
18
|
+
* entry. Append-only so repeated uploads to different destinations all
|
|
19
|
+
* leave a trail. Purely informational — nothing in the install/build flow
|
|
20
|
+
* branches on these files. Wiped by `wyvrnpm clean --uploaded-built`.
|
|
21
|
+
*
|
|
22
|
+
* @param {{ name: string, version: string, profileHash: string, source: string, contentSha256: string }} args
|
|
23
|
+
*/
|
|
24
|
+
function recordUploadSidecar({ name, version, profileHash, source, contentSha256 }) {
|
|
25
|
+
try {
|
|
26
|
+
const paths = getBuildPaths({ name, version, profileHash });
|
|
27
|
+
if (!fs.existsSync(paths.root)) return;
|
|
28
|
+
const sidecarPath = path.join(paths.root, UPLOAD_SIDECAR);
|
|
29
|
+
let doc = { uploads: [] };
|
|
30
|
+
if (fs.existsSync(sidecarPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = JSON.parse(fs.readFileSync(sidecarPath, 'utf8'));
|
|
33
|
+
if (Array.isArray(raw?.uploads)) doc = raw;
|
|
34
|
+
} catch { /* corrupt sidecar — overwrite with fresh doc */ }
|
|
35
|
+
}
|
|
36
|
+
doc.uploads.push({
|
|
37
|
+
uploadedAt: new Date().toISOString(),
|
|
38
|
+
source,
|
|
39
|
+
profileHash,
|
|
40
|
+
contentSha256,
|
|
41
|
+
});
|
|
42
|
+
fs.writeFileSync(sidecarPath, JSON.stringify(doc, null, 2) + '\n', 'utf8');
|
|
43
|
+
} catch {
|
|
44
|
+
// Sidecar bookkeeping is best-effort — never break an install over it.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Upload a freshly source-built artefact back to the v2 registry under the
|
|
50
|
+
* consumer's profileHash, so subsequent consumers with the same profile get
|
|
51
|
+
* a direct download instead of re-cloning and re-compiling.
|
|
52
|
+
*
|
|
53
|
+
* See PLAN-UPLOAD-BUILT.md for the full design. Key properties:
|
|
54
|
+
*
|
|
55
|
+
* - Opt-in. Only called when the user passes `--upload-built` at install.
|
|
56
|
+
* - Skips silently if the registry already has this (name, version,
|
|
57
|
+
* profileHash) — the common case once the first teammate has uploaded.
|
|
58
|
+
* - Never overwrites. A consumer with write credentials cannot clobber an
|
|
59
|
+
* existing artefact; that still requires `publish --force` from the
|
|
60
|
+
* source repo.
|
|
61
|
+
* - Leaves `latest.json` and the `latest` pointer in `versions.json`
|
|
62
|
+
* alone. A consumer upload is a new profileHash of an existing version,
|
|
63
|
+
* not a new version release. Honoured via `updateLatest: false`.
|
|
64
|
+
* - Records provenance in the uploaded manifest's `uploadedBy` block.
|
|
65
|
+
* Absence of this block implies author-publish by convention.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} args
|
|
68
|
+
* @param {string} args.name
|
|
69
|
+
* @param {string} args.version
|
|
70
|
+
* @param {import('./profile').WyvrnProfile} args.profile
|
|
71
|
+
* @param {string} args.profileHash
|
|
72
|
+
* @param {string} args.artefactPath Absolute path to the built zip in %LOCALAPPDATA%\wyvrnpm\bc\.
|
|
73
|
+
* @param {string} args.clonedManifestPath Absolute path to the cloned-source wyvrn.json.
|
|
74
|
+
* @param {string} args.contentSha256 SHA256 of the built zip.
|
|
75
|
+
* @param {string} args.gitRepo Source repo URL recorded at publish.
|
|
76
|
+
* @param {string} args.gitSha Source commit recorded at publish.
|
|
77
|
+
* @param {string} args.uploadSource Destination URL (resolved from --upload-source or config).
|
|
78
|
+
* @param {object} [args.uploadAuth] { awsProfile?, token? }
|
|
79
|
+
* @param {Record<string, any>|null} [args.options] Effective options for this build (fed through to buildSettings.options).
|
|
80
|
+
* @param {string} [args.toolchainHash] Optional SHA of the generated wyvrn_toolchain.cmake.
|
|
81
|
+
* @returns {Promise<{ uploaded: boolean, reason: string }>}
|
|
82
|
+
*/
|
|
83
|
+
async function uploadSourceBuiltArtefact(args) {
|
|
84
|
+
const {
|
|
85
|
+
name, version, profile, profileHash,
|
|
86
|
+
artefactPath, clonedManifestPath,
|
|
87
|
+
contentSha256, gitRepo, gitSha,
|
|
88
|
+
uploadSource, uploadAuth = {},
|
|
89
|
+
options = null,
|
|
90
|
+
toolchainHash,
|
|
91
|
+
} = args;
|
|
92
|
+
|
|
93
|
+
if (!uploadSource) {
|
|
94
|
+
return { uploaded: false, reason: 'no upload source resolved' };
|
|
95
|
+
}
|
|
96
|
+
if (!fs.existsSync(artefactPath)) {
|
|
97
|
+
return { uploaded: false, reason: `artefact zip missing at ${artefactPath}` };
|
|
98
|
+
}
|
|
99
|
+
if (!fs.existsSync(clonedManifestPath)) {
|
|
100
|
+
// Cache-hit path on a previously-trimmed cache, or the source dir was
|
|
101
|
+
// manually deleted. We have the binary but not the manifest we would
|
|
102
|
+
// have uploaded alongside it. Surface it — nothing silent.
|
|
103
|
+
return { uploaded: false, reason: `cloned manifest missing at ${clonedManifestPath}` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let provider;
|
|
107
|
+
try {
|
|
108
|
+
provider = getProvider(uploadSource);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return { uploaded: false, reason: `no provider for ${uploadSource}: ${err.message}` };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { awsProfile, token } = uploadAuth;
|
|
114
|
+
|
|
115
|
+
// ── Skip-if-exists ────────────────────────────────────────────────────────
|
|
116
|
+
const exists = await provider.v2Exists({
|
|
117
|
+
source: uploadSource, name, version, profileHash, awsProfile, token,
|
|
118
|
+
});
|
|
119
|
+
if (exists) {
|
|
120
|
+
log.info(`upload-built: ${name}@${version} [${profileHash}] already on ${uploadSource} — skipping`);
|
|
121
|
+
return { uploaded: false, reason: 'already exists on registry' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Prepare manifest: cloned wyvrn.json + compat block + uploadedBy ──────
|
|
125
|
+
const rawManifest = JSON.parse(fs.readFileSync(clonedManifestPath, 'utf8'));
|
|
126
|
+
const manifestForUpload = {
|
|
127
|
+
...rawManifest,
|
|
128
|
+
compatibility: rawManifest.compatibility ?? defaultCompatBlock(),
|
|
129
|
+
uploadedBy: {
|
|
130
|
+
kind: 'source-build',
|
|
131
|
+
profileHash,
|
|
132
|
+
builtFromGit: gitRepo ?? null,
|
|
133
|
+
builtFromSha: gitSha ?? null,
|
|
134
|
+
...(toolchainHash ? { toolchainHash } : {}),
|
|
135
|
+
uploadedAt: new Date().toISOString(),
|
|
136
|
+
wyvrnpmVersion: WYVRNPM_VERSION,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const tmpManifestPath = path.join(
|
|
141
|
+
os.tmpdir(),
|
|
142
|
+
`wyvrnpm-upload-${name}-${version}-${profileHash}-${Date.now()}.json`,
|
|
143
|
+
);
|
|
144
|
+
fs.writeFileSync(tmpManifestPath, JSON.stringify(manifestForUpload, null, 2), 'utf8');
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
log.info(`upload-built: ${name}@${version} [${profileHash}] → ${uploadSource}`);
|
|
148
|
+
await provider.v2Publish(
|
|
149
|
+
{ manifest: tmpManifestPath, zip: artefactPath },
|
|
150
|
+
{
|
|
151
|
+
source: uploadSource,
|
|
152
|
+
name, version, profileHash,
|
|
153
|
+
buildSettings: options ? { ...profile, options } : profile,
|
|
154
|
+
contentSha256,
|
|
155
|
+
gitSha: gitSha ?? null,
|
|
156
|
+
gitRepo: gitRepo ?? null,
|
|
157
|
+
// Consumer uploads must not reshape the "latest" pointer — see
|
|
158
|
+
// PLAN-UPLOAD-BUILT §3.3.
|
|
159
|
+
updateLatest: false,
|
|
160
|
+
uploadedBy: manifestForUpload.uploadedBy,
|
|
161
|
+
awsProfile,
|
|
162
|
+
token,
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
log.success(`upload-built: ${name}@${version} [${profileHash}] uploaded`);
|
|
166
|
+
recordUploadSidecar({ name, version, profileHash, source: uploadSource, contentSha256 });
|
|
167
|
+
return { uploaded: true, reason: 'ok' };
|
|
168
|
+
} catch (err) {
|
|
169
|
+
// The caller wraps us in try/catch too, but returning {uploaded:false}
|
|
170
|
+
// here keeps the happy path type-stable.
|
|
171
|
+
return { uploaded: false, reason: `upload failed: ${err.message}` };
|
|
172
|
+
} finally {
|
|
173
|
+
try { fs.unlinkSync(tmpManifestPath); } catch { /* ignore */ }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolve `--upload-source` (URL or configured-publish-source name) + auth
|
|
179
|
+
* into an object the orchestrator understands. Mirrors the resolution
|
|
180
|
+
* pattern in src/commands/publish.js so the two flows behave identically.
|
|
181
|
+
*
|
|
182
|
+
* Returns null if no destination can be resolved — caller decides whether
|
|
183
|
+
* that is fatal (it is, if --upload-built was passed) or just skip-worthy.
|
|
184
|
+
*
|
|
185
|
+
* @param {object} argv
|
|
186
|
+
* @param {string|undefined} argv.uploadSource
|
|
187
|
+
* @param {string|undefined} argv.awsProfile
|
|
188
|
+
* @param {string|undefined} argv.token
|
|
189
|
+
* @param {{ publishSources: Array<{name:string, url:string, profile?:string, token?:string}> }} config
|
|
190
|
+
* @returns {{ uploadSource: string, uploadAuth: { awsProfile?: string, token?: string } } | null}
|
|
191
|
+
*/
|
|
192
|
+
function resolveUploadDestination(argv, config) {
|
|
193
|
+
let src = argv.uploadSource;
|
|
194
|
+
let awsProfile = argv.awsProfile;
|
|
195
|
+
let token = argv.token;
|
|
196
|
+
|
|
197
|
+
const publishSources = config.publishSources ?? [];
|
|
198
|
+
|
|
199
|
+
if (src) {
|
|
200
|
+
// Check if it matches a configured publish source by name.
|
|
201
|
+
const named = publishSources.find((s) => s.name === src);
|
|
202
|
+
if (named) {
|
|
203
|
+
src = named.url;
|
|
204
|
+
awsProfile = awsProfile ?? named.profile;
|
|
205
|
+
token = token ?? named.token;
|
|
206
|
+
log.info(`upload-built: using publish source "${named.name}" from config`);
|
|
207
|
+
}
|
|
208
|
+
} else if (publishSources.length > 0) {
|
|
209
|
+
const first = publishSources[0];
|
|
210
|
+
src = first.url;
|
|
211
|
+
awsProfile = awsProfile ?? first.profile;
|
|
212
|
+
token = token ?? first.token;
|
|
213
|
+
log.info(`upload-built: using first configured publish source "${first.name}"`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!src) return null;
|
|
217
|
+
return { uploadSource: src, uploadAuth: { awsProfile, token } };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Create an empty accumulator for aggregated upload outcomes. Pass the
|
|
222
|
+
* returned object as `options.uploadStats` to `downloadDependencies`, then
|
|
223
|
+
* feed it to `formatUploadSummary()` at the end of the install.
|
|
224
|
+
*
|
|
225
|
+
* @returns {{ uploaded: Array<object>, skipped: Array<object>, failed: Array<object> }}
|
|
226
|
+
*/
|
|
227
|
+
function createUploadStats() {
|
|
228
|
+
return { uploaded: [], skipped: [], failed: [] };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Produce the one-line aggregated summary shown at the end of an install
|
|
233
|
+
* when `--upload-built` was passed. Returns null when nothing was attempted
|
|
234
|
+
* (so the caller can skip printing).
|
|
235
|
+
*
|
|
236
|
+
* @param {{ uploaded: Array, skipped: Array, failed: Array }} stats
|
|
237
|
+
* @returns {string|null}
|
|
238
|
+
*/
|
|
239
|
+
function formatUploadSummary(stats) {
|
|
240
|
+
if (!stats) return null;
|
|
241
|
+
const total = stats.uploaded.length + stats.skipped.length + stats.failed.length;
|
|
242
|
+
if (total === 0) return null;
|
|
243
|
+
const n = (arr, singular) => `${arr.length} ${singular}${arr.length === 1 ? '' : 's'}`;
|
|
244
|
+
return (
|
|
245
|
+
`upload-built summary: ${n(stats.uploaded, 'artefact')} uploaded, ` +
|
|
246
|
+
`${stats.skipped.length} skipped (already existed), ` +
|
|
247
|
+
`${stats.failed.length} failed`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
uploadSourceBuiltArtefact,
|
|
253
|
+
resolveUploadDestination,
|
|
254
|
+
createUploadStats,
|
|
255
|
+
formatUploadSummary,
|
|
256
|
+
};
|