wyvrnpm 2.1.0 → 2.3.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 +257 -1
- package/bin/wyvrn.js +131 -14
- package/claude/skills/wyvrnpm.skill +0 -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/build.js +49 -8
- package/src/commands/clean.js +39 -15
- package/src/commands/install-skill.js +107 -0
- package/src/commands/install.js +201 -5
- package/src/commands/publish.js +150 -35
- package/src/commands/show.js +237 -0
- package/src/compat.js +32 -3
- 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/presets.js +88 -16
- package/src/upload-built.js +256 -0
- package/src/version-range.js +301 -0
package/src/build/recipe.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { substituteTemplateAll } = require('../options');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* The `build` section of a source-repo `wyvrn.json` tells wyvrnpm how to
|
|
5
7
|
* build the package from source when `--build=missing` fires. Only
|
|
@@ -50,13 +52,23 @@ const SUPPORTED_SYSTEMS = new Set(['cmake']);
|
|
|
50
52
|
* array of strings (fallback chain — tried in order; first available wins).
|
|
51
53
|
* Both forms produce a `generators: string[]` field in the normalised recipe.
|
|
52
54
|
*
|
|
55
|
+
* When `effectiveOptions` is supplied, `${options.<name>}` tokens inside
|
|
56
|
+
* `configure` and `buildArgs` are expanded using those values — bool → ON/OFF,
|
|
57
|
+
* strings verbatim. An unknown option in a template is a hard error (fail
|
|
58
|
+
* fast so the bad arg never reaches cmake). See src/options.js
|
|
59
|
+
* `substituteTemplate`. When `effectiveOptions` is null (recipe declares
|
|
60
|
+
* no options), any `${options.*}` token in the recipe also fails fast —
|
|
61
|
+
* recipes don't get to reference undeclared options.
|
|
62
|
+
*
|
|
53
63
|
* @param {object|null|undefined} rawBuild
|
|
64
|
+
* @param {Record<string, any>|null} [effectiveOptions]
|
|
54
65
|
* @returns {{ system: string, generators: string[], configure: string[],
|
|
55
66
|
* buildArgs: string[], installDir: string, sourceSubdir: string,
|
|
56
67
|
* requiredTools: string[] }}
|
|
57
|
-
* @throws {Error} when `system` is set but not supported
|
|
68
|
+
* @throws {Error} when `system` is set but not supported, or when a
|
|
69
|
+
* `${options.<name>}` template references an undeclared option
|
|
58
70
|
*/
|
|
59
|
-
function normalizeRecipe(rawBuild) {
|
|
71
|
+
function normalizeRecipe(rawBuild, effectiveOptions = null) {
|
|
60
72
|
const b = rawBuild ?? {};
|
|
61
73
|
const system = b.system ?? DEFAULT_RECIPE.system;
|
|
62
74
|
if (!SUPPORTED_SYSTEMS.has(system)) {
|
|
@@ -98,12 +110,26 @@ function normalizeRecipe(rawBuild) {
|
|
|
98
110
|
configs = configs.filter((c) => VALID_CMAKE_CONFIGS.has(c));
|
|
99
111
|
if (configs.length === 0) configs = DEFAULT_RECIPE.configs.slice();
|
|
100
112
|
|
|
113
|
+
// Expand ${options.<name>} tokens in args the recipe passes to cmake.
|
|
114
|
+
// Only `configure` and `buildArgs` are user-authored arg lists; the
|
|
115
|
+
// other fields are literals.
|
|
116
|
+
const configure = substituteTemplateAll(
|
|
117
|
+
cleanArray(b.configure),
|
|
118
|
+
effectiveOptions,
|
|
119
|
+
'build.configure',
|
|
120
|
+
);
|
|
121
|
+
const buildArgs = substituteTemplateAll(
|
|
122
|
+
cleanArray(b.buildArgs),
|
|
123
|
+
effectiveOptions,
|
|
124
|
+
'build.buildArgs',
|
|
125
|
+
);
|
|
126
|
+
|
|
101
127
|
return {
|
|
102
128
|
system,
|
|
103
129
|
generators,
|
|
104
130
|
configs,
|
|
105
|
-
configure
|
|
106
|
-
buildArgs
|
|
131
|
+
configure,
|
|
132
|
+
buildArgs,
|
|
107
133
|
installDir: b.installDir ?? DEFAULT_RECIPE.installDir,
|
|
108
134
|
sourceSubdir: b.sourceSubdir ?? DEFAULT_RECIPE.sourceSubdir,
|
|
109
135
|
requiredTools: cleanArray(b.requiredTools),
|
package/src/commands/build.js
CHANGED
|
@@ -4,9 +4,13 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawn } = require('child_process');
|
|
6
6
|
|
|
7
|
-
const { readConfig }
|
|
8
|
-
const
|
|
9
|
-
const
|
|
7
|
+
const { readConfig } = require('../config');
|
|
8
|
+
const { loadProfile } = require('../profile');
|
|
9
|
+
const { readManifest } = require('../manifest');
|
|
10
|
+
const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
|
|
11
|
+
const { loadMsvcEnv } = require('../build/msvc-env');
|
|
12
|
+
const install = require('./install');
|
|
13
|
+
const log = require('../logger');
|
|
10
14
|
|
|
11
15
|
// ---------------------------------------------------------------------------
|
|
12
16
|
// Pure helpers (unit-testable — no process spawning, no filesystem writes)
|
|
@@ -117,12 +121,18 @@ function buildInstallArgs({ binaryDir, config, prefix }) {
|
|
|
117
121
|
* Run `cmake` with the given args. Inherits stdio for real-time output.
|
|
118
122
|
* Rejects if the process exits non-zero or fails to spawn.
|
|
119
123
|
*
|
|
124
|
+
* When `env` is provided, it replaces the ambient environment (used to
|
|
125
|
+
* activate MSVC via vcvarsall for non-VS generators on Windows).
|
|
126
|
+
*
|
|
120
127
|
* @param {string[]} args
|
|
128
|
+
* @param {NodeJS.ProcessEnv|null} [env]
|
|
121
129
|
* @returns {Promise<void>}
|
|
122
130
|
*/
|
|
123
|
-
function runCmake(args) {
|
|
131
|
+
function runCmake(args, env = null) {
|
|
124
132
|
return new Promise((resolve, reject) => {
|
|
125
|
-
const
|
|
133
|
+
const opts = { stdio: 'inherit' };
|
|
134
|
+
if (env) opts.env = env;
|
|
135
|
+
const proc = spawn('cmake', args, opts);
|
|
126
136
|
proc.on('error', (err) => {
|
|
127
137
|
reject(new Error(
|
|
128
138
|
`Failed to spawn cmake: ${err.message}\n` +
|
|
@@ -192,10 +202,41 @@ async function build(argv) {
|
|
|
192
202
|
fs.rmSync(binaryDir, { recursive: true, force: true });
|
|
193
203
|
}
|
|
194
204
|
|
|
205
|
+
// ── MSVC env activation (Windows + non-VS generator) ───────────────────
|
|
206
|
+
// When the preset's generator is Ninja / Ninja Multi-Config / Make / etc.
|
|
207
|
+
// and the active profile is MSVC, we need `vcvarsall.bat` to have been
|
|
208
|
+
// sourced — otherwise cmake's compiler detection fails with "CXX compiler
|
|
209
|
+
// identification is unknown". VS generators sidestep this because MSBuild
|
|
210
|
+
// finds MSVC via its own toolset resolution.
|
|
211
|
+
//
|
|
212
|
+
// Same mechanism the source-build path already uses (src/build/msvc-env.js).
|
|
213
|
+
let cmakeEnv = null;
|
|
214
|
+
try {
|
|
215
|
+
const { profile: activeProfile } = loadProfile(profileName);
|
|
216
|
+
// Which generator will the preset actually use? Read it from the
|
|
217
|
+
// project's own recipe if one exists. If not, default to null and let
|
|
218
|
+
// cmake/CMakePresets decide — in which case MSVC env activation is
|
|
219
|
+
// skipped (because VS is usually the default on Windows).
|
|
220
|
+
const manifest = readManifest(manifestPath);
|
|
221
|
+
let presetGenerator = null;
|
|
222
|
+
if (hasBuildRecipe(manifest)) {
|
|
223
|
+
try {
|
|
224
|
+
const recipe = normalizeRecipe(manifest.build, null);
|
|
225
|
+
if (recipe.generators.length > 0) [presetGenerator] = recipe.generators;
|
|
226
|
+
} catch { /* templates need options → skip env activation */ }
|
|
227
|
+
}
|
|
228
|
+
cmakeEnv = await loadMsvcEnv({
|
|
229
|
+
profile: activeProfile,
|
|
230
|
+
skipForGenerator: presetGenerator,
|
|
231
|
+
});
|
|
232
|
+
} catch (err) {
|
|
233
|
+
log.warn(`Could not resolve MSVC env activation: ${err.message}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
195
236
|
// ── Configure ─────────────────────────────────────────────────────────────
|
|
196
237
|
const configureArgs = buildConfigureArgs({ presetName });
|
|
197
238
|
log.info(`cmake ${configureArgs.join(' ')}`);
|
|
198
|
-
await runCmake(configureArgs);
|
|
239
|
+
await runCmake(configureArgs, cmakeEnv);
|
|
199
240
|
|
|
200
241
|
// ── Build ─────────────────────────────────────────────────────────────────
|
|
201
242
|
const buildArgs = buildBuildArgs({
|
|
@@ -206,7 +247,7 @@ async function build(argv) {
|
|
|
206
247
|
passthru: argv['--'] ?? [],
|
|
207
248
|
});
|
|
208
249
|
log.info(`cmake ${buildArgs.join(' ')}`);
|
|
209
|
-
await runCmake(buildArgs);
|
|
250
|
+
await runCmake(buildArgs, cmakeEnv);
|
|
210
251
|
|
|
211
252
|
// ── Install (optional) ────────────────────────────────────────────────────
|
|
212
253
|
// `--install` runs `cmake --install <binaryDir>` after the build. Prefix
|
|
@@ -219,7 +260,7 @@ async function build(argv) {
|
|
|
219
260
|
prefix: argv.installPrefix,
|
|
220
261
|
});
|
|
221
262
|
log.info(`cmake ${installArgs.join(' ')}`);
|
|
222
|
-
await runCmake(installArgs);
|
|
263
|
+
await runCmake(installArgs, cmakeEnv);
|
|
223
264
|
}
|
|
224
265
|
|
|
225
266
|
const suffix = argv.install
|
package/src/commands/clean.js
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
clearBuildCache,
|
|
8
|
+
getBuildCacheRoot,
|
|
9
|
+
clearUploadSidecars,
|
|
10
|
+
} = require('../build/cache');
|
|
7
11
|
const log = require('../logger');
|
|
8
12
|
|
|
9
13
|
/**
|
|
@@ -11,10 +15,17 @@ const log = require('../logger');
|
|
|
11
15
|
* With `--build-cache`, also clears the machine-wide source-build cache
|
|
12
16
|
* under `%LOCALAPPDATA%\wyvrnpm\bc\` (shared across projects).
|
|
13
17
|
*
|
|
18
|
+
* `--uploaded-built` wipes just the consumer's local RECORD of what they
|
|
19
|
+
* have uploaded via `wyvrnpm install --build=missing --upload-built` (the
|
|
20
|
+
* `.uploaded-to.json` sidecars under the build cache). It does NOT touch
|
|
21
|
+
* the registry, the artefact zips, or the source clones. Privacy / local
|
|
22
|
+
* audit cleanup only.
|
|
23
|
+
*
|
|
14
24
|
* @param {object} argv
|
|
15
|
-
* @param {string} argv.root
|
|
16
|
-
* @param {string} argv.manifest
|
|
17
|
-
* @param {boolean} [argv.buildCache]
|
|
25
|
+
* @param {string} argv.root Project root directory.
|
|
26
|
+
* @param {string} argv.manifest Path to the manifest file (used to locate wyvrn.lock).
|
|
27
|
+
* @param {boolean} [argv.buildCache] When true, also nukes the source-build cache.
|
|
28
|
+
* @param {boolean} [argv.uploadedBuilt] When true, wipes upload sidecars only.
|
|
18
29
|
* @returns {Promise<void>}
|
|
19
30
|
*/
|
|
20
31
|
async function clean(argv) {
|
|
@@ -22,20 +33,27 @@ async function clean(argv) {
|
|
|
22
33
|
const razerDir = path.join(rootDir, 'wyvrn_internal');
|
|
23
34
|
const lockPath = path.join(path.dirname(path.resolve(argv.manifest)), 'wyvrn.lock');
|
|
24
35
|
|
|
36
|
+
// --uploaded-built is a targeted cleanup mode. When passed on its own,
|
|
37
|
+
// skip the default wyvrn_internal/ + wyvrn.lock scrub so a user can
|
|
38
|
+
// "forget uploads" without also wiping their project-local install.
|
|
39
|
+
const targetedOnly = argv.uploadedBuilt && !argv.buildCache;
|
|
40
|
+
|
|
25
41
|
let removedAnything = false;
|
|
26
42
|
|
|
27
|
-
if (
|
|
28
|
-
fs.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
if (!targetedOnly) {
|
|
44
|
+
if (fs.existsSync(razerDir)) {
|
|
45
|
+
fs.rmSync(razerDir, { recursive: true, force: true });
|
|
46
|
+
log.info(`Removed ${razerDir}`);
|
|
47
|
+
removedAnything = true;
|
|
48
|
+
} else {
|
|
49
|
+
log.info(`Nothing to clean at ${razerDir}`);
|
|
50
|
+
}
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
if (fs.existsSync(lockPath)) {
|
|
53
|
+
fs.unlinkSync(lockPath);
|
|
54
|
+
log.info(`Removed ${lockPath}`);
|
|
55
|
+
removedAnything = true;
|
|
56
|
+
}
|
|
39
57
|
}
|
|
40
58
|
|
|
41
59
|
if (argv.buildCache) {
|
|
@@ -46,6 +64,12 @@ async function clean(argv) {
|
|
|
46
64
|
} else {
|
|
47
65
|
log.info(`No source-build cache at ${getBuildCacheRoot()}`);
|
|
48
66
|
}
|
|
67
|
+
} else if (argv.uploadedBuilt) {
|
|
68
|
+
// clearBuildCache() would have nuked the sidecars as a side-effect, so
|
|
69
|
+
// only bother with the sidecar sweep when the cache itself is surviving.
|
|
70
|
+
const removed = clearUploadSidecars();
|
|
71
|
+
log.info(`Removed ${removed} upload record${removed === 1 ? '' : 's'} from the build cache`);
|
|
72
|
+
if (removed > 0) removedAnything = true;
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
if (!removedAnything) {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const AdmZip = require('adm-zip');
|
|
7
|
+
|
|
8
|
+
const log = require('../logger');
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// `wyvrnpm install-skill --claude`
|
|
12
|
+
//
|
|
13
|
+
// Copies the bundled wyvrnpm Agent Skill (claude/skills/wyvrnpm.skill) into
|
|
14
|
+
// the user's Claude Code skills directory so Claude picks it up automatically.
|
|
15
|
+
//
|
|
16
|
+
// Works for both install shapes:
|
|
17
|
+
// - `npm i -g wyvrnpm` → skill zip ships under <pkg>/claude/skills/
|
|
18
|
+
// - `git clone` of this repo → same path, just not from node_modules
|
|
19
|
+
//
|
|
20
|
+
// Destination, per target:
|
|
21
|
+
// --claude → %USERPROFILE%\.claude\skills\wyvrnpm\ (Windows)
|
|
22
|
+
// ~/.claude/skills/wyvrnpm/ (macOS / Linux)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const SKILL_NAME = 'wyvrnpm';
|
|
26
|
+
|
|
27
|
+
function bundledSkillPath() {
|
|
28
|
+
// __dirname = <pkg>/src/commands; the .skill zip lives at
|
|
29
|
+
// <pkg>/claude/skills/wyvrnpm.skill.
|
|
30
|
+
return path.resolve(__dirname, '..', '..', 'claude', 'skills', `${SKILL_NAME}.skill`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function claudeSkillsDir() {
|
|
34
|
+
const home = os.homedir();
|
|
35
|
+
if (!home) throw new Error('cannot resolve user home directory');
|
|
36
|
+
return path.join(home, '.claude', 'skills');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rmrf(target) {
|
|
40
|
+
if (!fs.existsSync(target)) return;
|
|
41
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function installForClaude({ force, dryRun }) {
|
|
45
|
+
const zipPath = bundledSkillPath();
|
|
46
|
+
if (!fs.existsSync(zipPath)) {
|
|
47
|
+
log.error(`bundled skill not found at ${zipPath}`);
|
|
48
|
+
log.error('this is a packaging bug — please file an issue');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const skillsDir = claudeSkillsDir();
|
|
53
|
+
const destDir = path.join(skillsDir, SKILL_NAME);
|
|
54
|
+
const destZip = path.join(skillsDir, `${SKILL_NAME}.skill`);
|
|
55
|
+
|
|
56
|
+
log.info(`Source : ${zipPath}`);
|
|
57
|
+
log.info(`Destination : ${destDir}`);
|
|
58
|
+
|
|
59
|
+
if (fs.existsSync(destDir) && !force) {
|
|
60
|
+
log.warn(`${destDir} already exists`);
|
|
61
|
+
log.warn('re-run with --force to overwrite');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (dryRun) {
|
|
66
|
+
log.info('(--dry-run) would remove existing skill dir and zip, then extract fresh copy');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
71
|
+
rmrf(destDir);
|
|
72
|
+
rmrf(destZip);
|
|
73
|
+
|
|
74
|
+
const zip = new AdmZip(zipPath);
|
|
75
|
+
zip.extractAllTo(skillsDir, /* overwrite */ true);
|
|
76
|
+
fs.copyFileSync(zipPath, destZip);
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(path.join(destDir, 'SKILL.md'))) {
|
|
79
|
+
log.error(`extraction produced no SKILL.md under ${destDir}`);
|
|
80
|
+
log.error('the bundled .skill zip may be malformed');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
log.success(`Installed ${SKILL_NAME} skill to ${destDir}`);
|
|
85
|
+
log.info('Claude Code will pick it up on next session start.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function installSkill(argv) {
|
|
89
|
+
const targets = [];
|
|
90
|
+
if (argv.claude) targets.push('claude');
|
|
91
|
+
|
|
92
|
+
if (targets.length === 0) {
|
|
93
|
+
log.error('no target specified — pass --claude');
|
|
94
|
+
log.error('(more targets may be added in future releases)');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const target of targets) {
|
|
99
|
+
if (target === 'claude') {
|
|
100
|
+
installForClaude({ force: !!argv.force, dryRun: !!argv['dry-run'] });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = installSkill;
|
|
106
|
+
module.exports.bundledSkillPath = bundledSkillPath;
|
|
107
|
+
module.exports.claudeSkillsDir = claudeSkillsDir;
|
package/src/commands/install.js
CHANGED
|
@@ -9,6 +9,17 @@ const { downloadDependencies } = require('../downlo
|
|
|
9
9
|
const { readConfig } = require('../config');
|
|
10
10
|
const { loadProfile, hashProfile, formatProfile, mergeProfile } = require('../profile');
|
|
11
11
|
const { generateToolchain, generateCMakePresets } = require('../toolchain');
|
|
12
|
+
const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
|
|
13
|
+
const {
|
|
14
|
+
normalizeOptionsDeclaration,
|
|
15
|
+
resolveEffectiveOptions,
|
|
16
|
+
parseCliOptions,
|
|
17
|
+
} = require('../options');
|
|
18
|
+
const {
|
|
19
|
+
resolveUploadDestination,
|
|
20
|
+
createUploadStats,
|
|
21
|
+
formatUploadSummary,
|
|
22
|
+
} = require('../upload-built');
|
|
12
23
|
const log = require('../logger');
|
|
13
24
|
|
|
14
25
|
/**
|
|
@@ -26,6 +37,23 @@ async function install(argv) {
|
|
|
26
37
|
const manifestPath = path.resolve(argv.manifest);
|
|
27
38
|
const rootDir = path.resolve(argv.root);
|
|
28
39
|
const buildMode = argv.build ?? 'never';
|
|
40
|
+
const uploadBuilt = argv.uploadBuilt === true;
|
|
41
|
+
const jsonOut = argv.format === 'json';
|
|
42
|
+
|
|
43
|
+
// Under --format=json, stdout is reserved for the final JSON payload.
|
|
44
|
+
// All info/success lines route to stderr via the logger.
|
|
45
|
+
if (jsonOut) log.setJsonMode(true);
|
|
46
|
+
|
|
47
|
+
// --upload-built implies a build step happened. `--build=never` +
|
|
48
|
+
// `--upload-built` is a usage error — fail fast rather than silently
|
|
49
|
+
// accept something that cannot upload anything.
|
|
50
|
+
if (uploadBuilt && buildMode === 'never') {
|
|
51
|
+
log.error(
|
|
52
|
+
'--upload-built requires --build=missing (or --build=always).\n' +
|
|
53
|
+
' Nothing will be built, so there is nothing to upload.',
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
29
57
|
|
|
30
58
|
if (buildMode === 'always') {
|
|
31
59
|
log.error(
|
|
@@ -95,22 +123,44 @@ async function install(argv) {
|
|
|
95
123
|
`Installing dependencies for "${manifest.name}" (platform: ${argv.platform})`,
|
|
96
124
|
);
|
|
97
125
|
|
|
126
|
+
// Manifests collected during resolution — reused below to read each dep's
|
|
127
|
+
// recipe-declared `options` block without a second HTTP round trip.
|
|
128
|
+
const depManifests = new Map();
|
|
129
|
+
|
|
98
130
|
const resolvedDeps = await resolveDependencies(
|
|
99
131
|
depsForResolution,
|
|
100
132
|
packageSources,
|
|
101
133
|
argv.platform,
|
|
102
134
|
fetch,
|
|
103
135
|
lockedVersions,
|
|
136
|
+
depManifests,
|
|
104
137
|
);
|
|
105
138
|
|
|
139
|
+
// CLI `-o <pkg>:<name>=<value>` overrides, parsed into { pkg → {name: value} }.
|
|
140
|
+
const cliOptionsByPkg = parseCliOptions(argv.option);
|
|
141
|
+
|
|
106
142
|
// ── Build per-dependency enriched map ─────────────────────────────────────
|
|
107
|
-
// Each dep gets
|
|
108
|
-
//
|
|
143
|
+
// Each dep gets:
|
|
144
|
+
// - an effective profile (base + per-dep settings overrides)
|
|
145
|
+
// - effective options (recipe defaults + consumer overrides + CLI overrides)
|
|
146
|
+
// - profileHash computed from both (options only hashed in when declared)
|
|
109
147
|
const enrichedDeps = new Map();
|
|
110
148
|
for (const [name, version] of resolvedDeps) {
|
|
111
|
-
const depOverrides
|
|
149
|
+
const depOverrides = normalizedDeps[name]?.settings ?? {};
|
|
112
150
|
const effectiveProfile = mergeProfile(baseProfile, depOverrides);
|
|
113
|
-
|
|
151
|
+
|
|
152
|
+
const depManifest = depManifests.get(name);
|
|
153
|
+
const declaration = depManifest?.options
|
|
154
|
+
? normalizeOptionsDeclaration(depManifest.options, `${name}@${version}`)
|
|
155
|
+
: null;
|
|
156
|
+
const effectiveOptions = resolveEffectiveOptions(
|
|
157
|
+
declaration,
|
|
158
|
+
normalizedDeps[name]?.options ?? {},
|
|
159
|
+
cliOptionsByPkg[name] ?? {},
|
|
160
|
+
`${name}@${version}`,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const profileHash = hashProfile(effectiveProfile, effectiveOptions);
|
|
114
164
|
|
|
115
165
|
if (Object.keys(depOverrides).length > 0) {
|
|
116
166
|
log.info(
|
|
@@ -118,8 +168,16 @@ async function install(argv) {
|
|
|
118
168
|
` (${JSON.stringify(depOverrides)})`,
|
|
119
169
|
);
|
|
120
170
|
}
|
|
171
|
+
if (effectiveOptions) {
|
|
172
|
+
log.info(`${name}: options → ${JSON.stringify(effectiveOptions)}`);
|
|
173
|
+
}
|
|
121
174
|
|
|
122
|
-
enrichedDeps.set(name, {
|
|
175
|
+
enrichedDeps.set(name, {
|
|
176
|
+
version,
|
|
177
|
+
profileHash,
|
|
178
|
+
profile: effectiveProfile,
|
|
179
|
+
options: effectiveOptions,
|
|
180
|
+
});
|
|
123
181
|
}
|
|
124
182
|
|
|
125
183
|
const razerDir = path.join(rootDir, 'wyvrn_internal');
|
|
@@ -133,6 +191,40 @@ async function install(argv) {
|
|
|
133
191
|
profileName,
|
|
134
192
|
};
|
|
135
193
|
|
|
194
|
+
// ── Resolve upload destination up-front when --upload-built is set ──────
|
|
195
|
+
// Failing here (no publish source configured) avoids spending minutes on
|
|
196
|
+
// a source-build whose output we can't deliver.
|
|
197
|
+
if (uploadBuilt) {
|
|
198
|
+
const resolved = resolveUploadDestination(
|
|
199
|
+
{
|
|
200
|
+
uploadSource: argv.uploadSource,
|
|
201
|
+
awsProfile: argv.awsProfile ?? firstInstallSrc?.profile,
|
|
202
|
+
token: argv.token ?? firstInstallSrc?.token,
|
|
203
|
+
},
|
|
204
|
+
config,
|
|
205
|
+
);
|
|
206
|
+
if (!resolved) {
|
|
207
|
+
log.error(
|
|
208
|
+
'--upload-built was passed but no upload destination could be resolved.\n' +
|
|
209
|
+
' Pass --upload-source <url-or-name> or configure a publish source:\n' +
|
|
210
|
+
' wyvrnpm configure add-source --kind publish --name ... --url ...',
|
|
211
|
+
);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
downloadOptions.uploadBuilt = true;
|
|
215
|
+
downloadOptions.uploadSource = resolved.uploadSource;
|
|
216
|
+
downloadOptions.uploadAuth = resolved.uploadAuth;
|
|
217
|
+
downloadOptions.uploadStats = createUploadStats();
|
|
218
|
+
|
|
219
|
+
if (resolved.uploadSource !== (packageSources[0] ?? '')) {
|
|
220
|
+
log.info(
|
|
221
|
+
`upload-built: install source(s) and upload source differ —\n` +
|
|
222
|
+
` install: ${packageSources.join(', ') || '(none)'}\n` +
|
|
223
|
+
` upload : ${resolved.uploadSource}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
136
228
|
const lockEntries = await downloadDependencies(
|
|
137
229
|
enrichedDeps,
|
|
138
230
|
packageSources,
|
|
@@ -177,11 +269,70 @@ async function install(argv) {
|
|
|
177
269
|
const toolchainRelPath = path
|
|
178
270
|
.relative(rootDir, toolchain)
|
|
179
271
|
.replace(/\\/g, '/');
|
|
272
|
+
|
|
273
|
+
// Derive generator + cacheVariables from the project's own `build` recipe
|
|
274
|
+
// so `wyvrnpm build` locally honours the same knobs the source-build /
|
|
275
|
+
// publish paths use. Without this, `build.configure` is advisory metadata
|
|
276
|
+
// and CMake falls back to its default generator (VS on Windows) — which
|
|
277
|
+
// breaks publishers whose recipe disables example/test subprojects (F15).
|
|
278
|
+
let presetGenerator = null;
|
|
279
|
+
let presetCacheVariables = null;
|
|
280
|
+
let presetInstallDir = null;
|
|
281
|
+
if (hasBuildRecipe(manifest)) {
|
|
282
|
+
try {
|
|
283
|
+
// Resolve the project's own options to expand ${options.*} in its
|
|
284
|
+
// configure args. When the project declares no options, this is null
|
|
285
|
+
// and the recipe's configure / buildArgs must not contain templates
|
|
286
|
+
// (normalizeRecipe enforces that).
|
|
287
|
+
const projectDeclaration = manifest.options
|
|
288
|
+
? normalizeOptionsDeclaration(manifest.options, manifest.name)
|
|
289
|
+
: null;
|
|
290
|
+
const projectCliOverrides = cliOptionsByPkg[manifest.name] ?? {};
|
|
291
|
+
const projectEffectiveOptions = resolveEffectiveOptions(
|
|
292
|
+
projectDeclaration, {}, projectCliOverrides, manifest.name,
|
|
293
|
+
);
|
|
294
|
+
const recipe = normalizeRecipe(manifest.build, projectEffectiveOptions);
|
|
295
|
+
|
|
296
|
+
if (recipe.generators.length > 0) {
|
|
297
|
+
// First entry in the generator fallback chain. Users whose system
|
|
298
|
+
// lacks the first pick can override via CMakeUserPresets.json
|
|
299
|
+
// (which CMake merges in automatically).
|
|
300
|
+
[presetGenerator] = recipe.generators;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Fold -DKEY=VALUE from recipe.configure into cacheVariables. Other
|
|
304
|
+
// configure args (anything not matching `-DKEY=VALUE`) can't be
|
|
305
|
+
// expressed in a CMake preset and are silently skipped here — the
|
|
306
|
+
// source-build path still sees them verbatim.
|
|
307
|
+
const cacheVars = {};
|
|
308
|
+
for (const arg of recipe.configure) {
|
|
309
|
+
const match = arg.match(/^-D([^=]+)=(.*)$/);
|
|
310
|
+
if (match) cacheVars[match[1]] = match[2];
|
|
311
|
+
}
|
|
312
|
+
if (Object.keys(cacheVars).length > 0) presetCacheVariables = cacheVars;
|
|
313
|
+
|
|
314
|
+
// Propagate `build.installDir` so that `wyvrnpm build --install` lands
|
|
315
|
+
// under the project's build tree instead of CMake's system default
|
|
316
|
+
// (C:/Program Files on Windows, /usr/local on Unix). Same semantics as
|
|
317
|
+
// the source-build path which passes -DCMAKE_INSTALL_PREFIX explicitly.
|
|
318
|
+
presetInstallDir = recipe.installDir;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
log.warn(
|
|
321
|
+
`could not derive CMake preset config from the project's build recipe: ${err.message}\n` +
|
|
322
|
+
` the preset will be emitted without a generator or recipe cacheVariables. ` +
|
|
323
|
+
`Fix the recipe or override via CMakeUserPresets.json.`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
180
328
|
const presetResult = generateCMakePresets({
|
|
181
329
|
projectRoot: rootDir,
|
|
182
330
|
profileName,
|
|
183
331
|
profileHash: hashProfile(baseProfile),
|
|
184
332
|
toolchainRelPath,
|
|
333
|
+
generator: presetGenerator,
|
|
334
|
+
extraCacheVariables: presetCacheVariables,
|
|
335
|
+
installDir: presetInstallDir,
|
|
185
336
|
});
|
|
186
337
|
if (presetResult.action === 'skipped') {
|
|
187
338
|
log.warn(
|
|
@@ -198,6 +349,51 @@ async function install(argv) {
|
|
|
198
349
|
|
|
199
350
|
const count = resolvedDeps.size;
|
|
200
351
|
log.success(`Done — ${count} package${count !== 1 ? 's' : ''} installed.`);
|
|
352
|
+
|
|
353
|
+
// ── upload-built summary ────────────────────────────────────────────────
|
|
354
|
+
const uploadSummary = formatUploadSummary(downloadOptions.uploadStats);
|
|
355
|
+
if (uploadSummary) log.info(uploadSummary);
|
|
356
|
+
|
|
357
|
+
// ── JSON payload (--format=json) ────────────────────────────────────────
|
|
358
|
+
// All log.info/success above have gone to stderr thanks to setJsonMode.
|
|
359
|
+
// stdout gets exactly one JSON object, no trailing text.
|
|
360
|
+
if (jsonOut) {
|
|
361
|
+
const packages = [...resolvedDeps.keys()].sort().map((name) => {
|
|
362
|
+
const entry = lockEntries?.get(name) ?? {};
|
|
363
|
+
const consumerSpec = normalizedDeps[name]?.version ?? null;
|
|
364
|
+
const resolvedVer = entry.version ?? resolvedDeps.get(name);
|
|
365
|
+
const versionRange = (consumerSpec && consumerSpec !== resolvedVer) ? consumerSpec : null;
|
|
366
|
+
return {
|
|
367
|
+
name,
|
|
368
|
+
version: resolvedVer,
|
|
369
|
+
versionRange,
|
|
370
|
+
profileHash: entry.profileHash ?? null,
|
|
371
|
+
options: entry.options ?? null,
|
|
372
|
+
contentSha256: entry.contentSha256 ?? null,
|
|
373
|
+
gitSha: entry.gitSha ?? null,
|
|
374
|
+
resolvedFrom: entry.resolvedFrom ?? null,
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const stats = downloadOptions.uploadStats;
|
|
379
|
+
const uploadSummaryJson = stats ? {
|
|
380
|
+
uploaded: stats.uploaded.length,
|
|
381
|
+
skipped: stats.skipped.length,
|
|
382
|
+
failed: stats.failed.length,
|
|
383
|
+
} : null;
|
|
384
|
+
|
|
385
|
+
const payload = {
|
|
386
|
+
command: 'install',
|
|
387
|
+
wyvrnpmVersion: require('../../package.json').version,
|
|
388
|
+
profile: baseProfile,
|
|
389
|
+
profileName,
|
|
390
|
+
profileHash: hashProfile(baseProfile),
|
|
391
|
+
packages,
|
|
392
|
+
lockFile: lockPath,
|
|
393
|
+
uploadSummary: uploadSummaryJson,
|
|
394
|
+
};
|
|
395
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
396
|
+
}
|
|
201
397
|
}
|
|
202
398
|
|
|
203
399
|
module.exports = install;
|