wyvrnpm 2.3.2 → 2.8.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 +465 -19
- package/bin/wyvrn.js +175 -9
- package/claude/skills/wyvrnpm.skill +0 -0
- package/cmake/macros.cmake +51 -0
- package/cmake/variables.cmake +13 -3
- package/package.json +1 -1
- package/src/auth.js +66 -0
- package/src/build/cache.js +196 -0
- package/src/build/index.js +23 -2
- package/src/build/msvc-env.js +50 -7
- package/src/build/recipe.js +36 -3
- package/src/commands/add.js +140 -0
- package/src/commands/build.js +111 -7
- package/src/commands/cache.js +189 -0
- package/src/commands/configure.js +15 -4
- package/src/commands/init.js +36 -0
- package/src/commands/install-skill.js +8 -0
- package/src/commands/install.js +271 -5
- package/src/commands/profile.js +70 -7
- package/src/commands/publish.js +41 -4
- package/src/commands/show.js +14 -4
- package/src/compat.js +21 -9
- package/src/conf/index.js +391 -0
- package/src/conf/namespaces.js +94 -0
- package/src/download.js +136 -148
- package/src/logger.js +30 -2
- package/src/manifest.js +34 -7
- package/src/profile.js +148 -12
- package/src/resolve.js +47 -7
- package/src/settings-overrides.js +152 -0
- package/src/toolchain/index.js +71 -0
- package/src/toolchain/presets.js +108 -31
- package/src/toolchain/template.cmake +11 -0
- package/src/upload-built.js +11 -2
- package/src/zip-safe.js +52 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// F12: source-build cache inspection + targeted pruning.
|
|
4
|
+
//
|
|
5
|
+
// `wyvrnpm cache list [pattern]` prints a human-readable table (or JSON
|
|
6
|
+
// with --format json) of everything in the machine-wide source-build
|
|
7
|
+
// cache at `%LOCALAPPDATA%\wyvrnpm\bc\`.
|
|
8
|
+
//
|
|
9
|
+
// `wyvrnpm cache prune` removes cache entries per a policy:
|
|
10
|
+
// --keep-last N retain N most-recently-touched per (name,version)
|
|
11
|
+
// --older-than 30d also remove entries older than the threshold
|
|
12
|
+
// --pattern <glob> restrict to matching package names
|
|
13
|
+
// --dry-run print what would go, remove nothing
|
|
14
|
+
//
|
|
15
|
+
// The existing `wyvrnpm clean --build-cache` nukes everything; this is
|
|
16
|
+
// the targeted alternative for teams whose `--upload-built` cache has
|
|
17
|
+
// ballooned.
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
listCacheEntries,
|
|
21
|
+
computePruneSet,
|
|
22
|
+
removeCacheEntries,
|
|
23
|
+
parseDurationToCutoff,
|
|
24
|
+
getBuildCacheRoot,
|
|
25
|
+
} = require('../build/cache');
|
|
26
|
+
const log = require('../logger');
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
function formatBytes(n) {
|
|
33
|
+
if (n < 1024) return `${n} B`;
|
|
34
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
35
|
+
let size = n / 1024, i = 0;
|
|
36
|
+
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; }
|
|
37
|
+
return `${size.toFixed(size >= 100 ? 0 : 1)} ${units[i]}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatMtime(ms) {
|
|
41
|
+
if (!ms) return '<unknown>';
|
|
42
|
+
return new Date(ms).toISOString().replace('T', ' ').slice(0, 16);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function globToRegExp(glob) {
|
|
46
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
47
|
+
return new RegExp(`^${escaped.replace(/\*/g, '.*').replace(/\?/g, '.')}$`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function patternFor(argPattern, positional) {
|
|
51
|
+
const src = argPattern ?? positional ?? null;
|
|
52
|
+
return src ? globToRegExp(src) : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// list
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* wyvrnpm cache list [pattern] [--format json|text]
|
|
61
|
+
*
|
|
62
|
+
* @param {object} argv
|
|
63
|
+
*/
|
|
64
|
+
function list(argv) {
|
|
65
|
+
const entries = listCacheEntries();
|
|
66
|
+
const regex = patternFor(argv.pattern, argv._?.[1]);
|
|
67
|
+
const filtered = regex ? entries.filter((e) => regex.test(e.name)) : entries;
|
|
68
|
+
|
|
69
|
+
// Newest first — matches what `ls -lt` users expect.
|
|
70
|
+
filtered.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
71
|
+
|
|
72
|
+
if (argv.format === 'json') {
|
|
73
|
+
process.stdout.write(JSON.stringify({
|
|
74
|
+
command: 'cache-list',
|
|
75
|
+
cacheRoot: getBuildCacheRoot(),
|
|
76
|
+
totalEntries: filtered.length,
|
|
77
|
+
totalBytes: filtered.reduce((s, e) => s + e.sizeBytes, 0),
|
|
78
|
+
entries: filtered.map((e) => ({
|
|
79
|
+
name: e.name,
|
|
80
|
+
version: e.version,
|
|
81
|
+
profileHash: e.profileHash,
|
|
82
|
+
sizeBytes: e.sizeBytes,
|
|
83
|
+
mtime: new Date(e.mtimeMs).toISOString(),
|
|
84
|
+
hasArtefact: e.hasArtefact,
|
|
85
|
+
uploadCount: e.uploads.length,
|
|
86
|
+
})),
|
|
87
|
+
}, null, 2) + '\n');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
log.info(`Cache root : ${getBuildCacheRoot()}`);
|
|
92
|
+
if (filtered.length === 0) {
|
|
93
|
+
log.info(' (empty)');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const rows = filtered.map((e) => [
|
|
98
|
+
e.name,
|
|
99
|
+
e.version,
|
|
100
|
+
e.profileHash,
|
|
101
|
+
formatBytes(e.sizeBytes),
|
|
102
|
+
formatMtime(e.mtimeMs),
|
|
103
|
+
e.uploads.length > 0 ? `${e.uploads.length}` : '-',
|
|
104
|
+
]);
|
|
105
|
+
const header = ['NAME', 'VERSION', 'PROFILE HASH', 'SIZE', 'MTIME', 'UPLOADS'];
|
|
106
|
+
const widths = header.map((h, i) =>
|
|
107
|
+
Math.max(h.length, ...rows.map((r) => String(r[i]).length)),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const format = (cells) => cells.map((c, i) => String(c).padEnd(widths[i])).join(' ');
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(' ' + format(header));
|
|
113
|
+
console.log(' ' + widths.map((w) => '-'.repeat(w)).join(' '));
|
|
114
|
+
for (const r of rows) console.log(' ' + format(r));
|
|
115
|
+
console.log('');
|
|
116
|
+
|
|
117
|
+
const totalBytes = filtered.reduce((s, e) => s + e.sizeBytes, 0);
|
|
118
|
+
log.info(`${filtered.length} entries, ${formatBytes(totalBytes)} total`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// prune
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* wyvrnpm cache prune [--keep-last N] [--older-than 30d] [--pattern X] [--dry-run]
|
|
127
|
+
*
|
|
128
|
+
* Requires at least one of --keep-last / --older-than to avoid footgun
|
|
129
|
+
* (use `wyvrnpm clean --build-cache` to wipe everything).
|
|
130
|
+
*
|
|
131
|
+
* @param {object} argv
|
|
132
|
+
*/
|
|
133
|
+
function prune(argv) {
|
|
134
|
+
const hasKeep = argv.keepLast !== undefined && argv.keepLast !== null;
|
|
135
|
+
const hasOlder = typeof argv.olderThan === 'string' && argv.olderThan.length > 0;
|
|
136
|
+
|
|
137
|
+
if (!hasKeep && !hasOlder) {
|
|
138
|
+
log.error(
|
|
139
|
+
'cache prune: specify --keep-last <N> and/or --older-than <duration>.\n' +
|
|
140
|
+
' To wipe the entire build cache, use `wyvrnpm clean --build-cache`.',
|
|
141
|
+
);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const policy = {};
|
|
146
|
+
if (hasKeep) {
|
|
147
|
+
const n = Number(argv.keepLast);
|
|
148
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
149
|
+
log.error(`--keep-last must be a non-negative integer, got ${argv.keepLast}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
policy.keepLast = n;
|
|
153
|
+
}
|
|
154
|
+
if (hasOlder) {
|
|
155
|
+
try { policy.olderThanMs = parseDurationToCutoff(argv.olderThan); }
|
|
156
|
+
catch (err) { log.error(err.message); process.exit(1); }
|
|
157
|
+
}
|
|
158
|
+
if (argv.pattern) {
|
|
159
|
+
policy.patternRegex = globToRegExp(argv.pattern);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const entries = listCacheEntries();
|
|
163
|
+
const pruneSet = computePruneSet(entries, policy);
|
|
164
|
+
|
|
165
|
+
if (pruneSet.length === 0) {
|
|
166
|
+
log.info('Nothing to prune under that policy.');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const totalBytes = pruneSet.reduce((s, e) => s + e.sizeBytes, 0);
|
|
171
|
+
const verb = argv.dryRun ? 'Would remove' : 'Removing';
|
|
172
|
+
log.info(`${verb} ${pruneSet.length} cache entr${pruneSet.length === 1 ? 'y' : 'ies'}, ${formatBytes(totalBytes)}:`);
|
|
173
|
+
for (const e of pruneSet) {
|
|
174
|
+
console.log(` - ${e.name}@${e.version} [${e.profileHash}] ${formatBytes(e.sizeBytes)} ${formatMtime(e.mtimeMs)}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (argv.dryRun) {
|
|
178
|
+
log.info('(dry-run — nothing deleted)');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const removed = removeCacheEntries(pruneSet);
|
|
183
|
+
log.info(`Removed ${removed.length} of ${pruneSet.length} cache entr${pruneSet.length === 1 ? 'y' : 'ies'}`);
|
|
184
|
+
if (removed.length < pruneSet.length) {
|
|
185
|
+
log.warn(`${pruneSet.length - removed.length} entr${pruneSet.length - removed.length === 1 ? 'y' : 'ies'} could not be removed (permissions or concurrent access)`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { list, prune };
|
|
@@ -27,8 +27,9 @@ function printSection(title, sources) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function authLabel(src) {
|
|
30
|
-
if (src.profile)
|
|
31
|
-
if (src.
|
|
30
|
+
if (src.profile) return `[profile: ${src.profile}]`;
|
|
31
|
+
if (src.tokenEnv) return `[token-env: ${src.tokenEnv}]`;
|
|
32
|
+
if (src.token) return '[token: ***]';
|
|
32
33
|
return '';
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -38,12 +39,22 @@ function authLabel(src) {
|
|
|
38
39
|
*/
|
|
39
40
|
function addSource(argv) {
|
|
40
41
|
const { kind, name, url, profile, token } = argv;
|
|
42
|
+
// yargs delivers --token-env as argv['token-env']; accept both shapes
|
|
43
|
+
// so programmatic callers with camelCase also work.
|
|
44
|
+
const tokenEnv = argv.tokenEnv ?? argv['token-env'];
|
|
45
|
+
|
|
46
|
+
if (token && tokenEnv) {
|
|
47
|
+
log.error('Pass --token OR --token-env, not both. --token-env is preferred for CI and shared setups (EVALUATION.md S8).');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
const config = readConfig();
|
|
42
52
|
const key = kind === 'install' ? 'installSources' : 'publishSources';
|
|
43
53
|
|
|
44
54
|
const entry = { name, url };
|
|
45
|
-
if (profile)
|
|
46
|
-
if (token)
|
|
55
|
+
if (profile) entry.profile = profile;
|
|
56
|
+
if (token) entry.token = token;
|
|
57
|
+
if (tokenEnv) entry.tokenEnv = tokenEnv;
|
|
47
58
|
|
|
48
59
|
const idx = config[key].findIndex((s) => s.name === name);
|
|
49
60
|
if (idx !== -1) {
|
package/src/commands/init.js
CHANGED
|
@@ -3,8 +3,35 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { defaultManifest, writeManifest } = require('../manifest');
|
|
6
|
+
const { LOCAL_OVERLAY_FILENAME } = require('../conf');
|
|
6
7
|
const log = require('../logger');
|
|
7
8
|
|
|
9
|
+
// The wyvrn.local.json overlay must never be committed — it is by design
|
|
10
|
+
// per-developer state. init writes a gitignore entry; existing projects
|
|
11
|
+
// get a one-line nudge from install (see install.js) when the file is
|
|
12
|
+
// present but not ignored.
|
|
13
|
+
const GITIGNORE_MARKER_LINE = '# wyvrnpm — per-developer conf overlay (never commit)';
|
|
14
|
+
|
|
15
|
+
function ensureGitignoreEntry(rootDir) {
|
|
16
|
+
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
17
|
+
const desiredLines = [GITIGNORE_MARKER_LINE, LOCAL_OVERLAY_FILENAME];
|
|
18
|
+
|
|
19
|
+
let existing = '';
|
|
20
|
+
if (fs.existsSync(gitignorePath)) {
|
|
21
|
+
existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
22
|
+
const hasEntry = existing.split(/\r?\n/).some((l) => l.trim() === LOCAL_OVERLAY_FILENAME);
|
|
23
|
+
if (hasEntry) return { action: 'already-present', path: gitignorePath };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith('\n');
|
|
27
|
+
const block = (needsLeadingNewline ? '\n' : '') + desiredLines.join('\n') + '\n';
|
|
28
|
+
fs.writeFileSync(gitignorePath, existing + block, 'utf8');
|
|
29
|
+
return {
|
|
30
|
+
action: existing.length > 0 ? 'appended' : 'created',
|
|
31
|
+
path: gitignorePath,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
8
35
|
/**
|
|
9
36
|
* Initialises a new wyvrn.json manifest in the project root.
|
|
10
37
|
* If a manifest already exists the command exits early without overwriting it.
|
|
@@ -28,7 +55,16 @@ async function init(argv) {
|
|
|
28
55
|
|
|
29
56
|
writeManifest(manifestPath, manifest);
|
|
30
57
|
|
|
58
|
+
const gitignore = ensureGitignoreEntry(rootDir);
|
|
59
|
+
if (gitignore.action === 'created') {
|
|
60
|
+
log.info(`Created .gitignore at ${gitignore.path} with ${LOCAL_OVERLAY_FILENAME} exclusion`);
|
|
61
|
+
} else if (gitignore.action === 'appended') {
|
|
62
|
+
log.info(`Added ${LOCAL_OVERLAY_FILENAME} to ${gitignore.path}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
log.success(`Created manifest at ${manifestPath}`);
|
|
32
66
|
}
|
|
33
67
|
|
|
34
68
|
module.exports = init;
|
|
69
|
+
module.exports.ensureGitignoreEntry = ensureGitignoreEntry;
|
|
70
|
+
module.exports.GITIGNORE_MARKER_LINE = GITIGNORE_MARKER_LINE;
|
|
@@ -5,6 +5,7 @@ const os = require('os');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const AdmZip = require('adm-zip');
|
|
7
7
|
|
|
8
|
+
const { assertAllSafeZipEntryNames } = require('../zip-safe');
|
|
8
9
|
const log = require('../logger');
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -72,6 +73,13 @@ function installForClaude({ force, dryRun }) {
|
|
|
72
73
|
rmrf(destZip);
|
|
73
74
|
|
|
74
75
|
const zip = new AdmZip(zipPath);
|
|
76
|
+
// Zip Slip defense-in-depth (EVALUATION.md S2). The shipped .skill zip
|
|
77
|
+
// is trusted-by-provenance, but validating consistently keeps the
|
|
78
|
+
// attack surface flat across all extraction call sites.
|
|
79
|
+
assertAllSafeZipEntryNames(
|
|
80
|
+
zip.getEntries().map((e) => e.entryName),
|
|
81
|
+
`${SKILL_NAME}.skill`,
|
|
82
|
+
);
|
|
75
83
|
zip.extractAllTo(skillsDir, /* overwrite */ true);
|
|
76
84
|
fs.copyFileSync(zipPath, destZip);
|
|
77
85
|
|
package/src/commands/install.js
CHANGED
|
@@ -7,6 +7,8 @@ const { readManifest, normalizeDependencies } = require('../manife
|
|
|
7
7
|
const { resolveDependencies } = require('../resolve');
|
|
8
8
|
const { downloadDependencies } = require('../download');
|
|
9
9
|
const { readConfig } = require('../config');
|
|
10
|
+
const { resolveSourceAuth } = require('../auth');
|
|
11
|
+
const { resolveEffectiveConf, cmakeCacheVariables, unflattenConf } = require('../conf');
|
|
10
12
|
const { loadProfile, hashProfile, formatProfile, mergeProfile } = require('../profile');
|
|
11
13
|
const { generateToolchain, generateCMakePresets } = require('../toolchain');
|
|
12
14
|
const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
|
|
@@ -15,6 +17,10 @@ const {
|
|
|
15
17
|
resolveEffectiveOptions,
|
|
16
18
|
parseCliOptions,
|
|
17
19
|
} = require('../options');
|
|
20
|
+
const {
|
|
21
|
+
normalizeSettingsOverrides,
|
|
22
|
+
resolveOverrideSettings,
|
|
23
|
+
} = require('../settings-overrides');
|
|
18
24
|
const {
|
|
19
25
|
resolveUploadDestination,
|
|
20
26
|
createUploadStats,
|
|
@@ -119,6 +125,45 @@ async function install(argv) {
|
|
|
119
125
|
Object.entries(normalizedDeps).map(([name, d]) => [name, d.version]),
|
|
120
126
|
);
|
|
121
127
|
|
|
128
|
+
// Per-package source pinning (EVALUATION.md S4). Deps declaring
|
|
129
|
+
// `"source": "primary-s3"` restrict to that named source's URL from
|
|
130
|
+
// config — typosquat / mirror-shadow defense. Unknown name → fail
|
|
131
|
+
// fast (silent fallback to fan-out would defeat the point).
|
|
132
|
+
//
|
|
133
|
+
// `argv.source` overrides config entirely; if the user passes
|
|
134
|
+
// `--source <url>` but a dep pins a name, we can't resolve the name
|
|
135
|
+
// → warn and fall back to fan-out across the CLI-supplied URLs.
|
|
136
|
+
//
|
|
137
|
+
// Transitive deps continue to fan out — a pinned top-level dep's
|
|
138
|
+
// recipe can reference whatever it wants.
|
|
139
|
+
const sourcesByName = new Map();
|
|
140
|
+
if (!argv.source || argv.source.length === 0) {
|
|
141
|
+
for (const s of config.installSources ?? []) {
|
|
142
|
+
if (s.name && s.url) sourcesByName.set(s.name, s.url);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const pinnedSources = new Map();
|
|
146
|
+
for (const [name, d] of Object.entries(normalizedDeps)) {
|
|
147
|
+
if (!d.source) continue;
|
|
148
|
+
if (argv.source && argv.source.length > 0) {
|
|
149
|
+
log.warn(
|
|
150
|
+
`"${name}" pins source "${d.source}" but --source was passed on CLI — ` +
|
|
151
|
+
`pin ignored for this run`,
|
|
152
|
+
);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const url = sourcesByName.get(d.source);
|
|
156
|
+
if (!url) {
|
|
157
|
+
log.error(
|
|
158
|
+
`"${name}" pins source "${d.source}" but no install source by that name is configured. ` +
|
|
159
|
+
`Run "wyvrnpm configure list" to see configured sources.`,
|
|
160
|
+
);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
pinnedSources.set(name, url);
|
|
164
|
+
log.info(`${name}: pinned to source "${d.source}"`);
|
|
165
|
+
}
|
|
166
|
+
|
|
122
167
|
log.info(
|
|
123
168
|
`Installing dependencies for "${manifest.name}" (platform: ${argv.platform})`,
|
|
124
169
|
);
|
|
@@ -134,11 +179,31 @@ async function install(argv) {
|
|
|
134
179
|
fetch,
|
|
135
180
|
lockedVersions,
|
|
136
181
|
depManifests,
|
|
182
|
+
// F13: prefer the consumer's own profileHash when picking which build
|
|
183
|
+
// entry to read wyvrn.json from. Today every published build for a
|
|
184
|
+
// given (name, version) ships the same manifest, so this is harmless;
|
|
185
|
+
// it future-proofs against per-profile manifest variance.
|
|
186
|
+
hashProfile(baseProfile),
|
|
187
|
+
pinnedSources,
|
|
137
188
|
);
|
|
138
189
|
|
|
139
190
|
// CLI `-o <pkg>:<name>=<value>` overrides, parsed into { pkg → {name: value} }.
|
|
140
191
|
const cliOptionsByPkg = parseCliOptions(argv.option);
|
|
141
192
|
|
|
193
|
+
// F11: pattern-based settings overrides. `manifest.settingsOverrides`
|
|
194
|
+
// is { "<glob>": { <setting>: <value> }, ... }. Validated + sorted by
|
|
195
|
+
// specificity once here; `resolveOverrideSettings(depName, ...)` then
|
|
196
|
+
// returns the merged-from-patterns settings for each dep. Literal
|
|
197
|
+
// per-dep `settings` still override these (authors pinning a specific
|
|
198
|
+
// dep win over a team-wide pattern).
|
|
199
|
+
let settingsOverrides;
|
|
200
|
+
try {
|
|
201
|
+
settingsOverrides = normalizeSettingsOverrides(manifest.settingsOverrides);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
log.error(err.message);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
142
207
|
// ── Build per-dependency enriched map ─────────────────────────────────────
|
|
143
208
|
// Each dep gets:
|
|
144
209
|
// - an effective profile (base + per-dep settings overrides)
|
|
@@ -146,7 +211,12 @@ async function install(argv) {
|
|
|
146
211
|
// - profileHash computed from both (options only hashed in when declared)
|
|
147
212
|
const enrichedDeps = new Map();
|
|
148
213
|
for (const [name, version] of resolvedDeps) {
|
|
149
|
-
|
|
214
|
+
// F11: layer pattern overrides first (less-specific applied first
|
|
215
|
+
// inside resolveOverrideSettings), then the literal per-dep
|
|
216
|
+
// settings on top. Literal wins over any pattern.
|
|
217
|
+
const patternOverrides = resolveOverrideSettings(name, settingsOverrides);
|
|
218
|
+
const literalOverrides = normalizedDeps[name]?.settings ?? {};
|
|
219
|
+
const depOverrides = { ...patternOverrides, ...literalOverrides };
|
|
150
220
|
const effectiveProfile = mergeProfile(baseProfile, depOverrides);
|
|
151
221
|
|
|
152
222
|
const depManifest = depManifests.get(name);
|
|
@@ -177,16 +247,85 @@ async function install(argv) {
|
|
|
177
247
|
profileHash,
|
|
178
248
|
profile: effectiveProfile,
|
|
179
249
|
options: effectiveOptions,
|
|
250
|
+
// S4: pinned install source (name + URL). Only set for direct
|
|
251
|
+
// deps that declared `"source": "..."`; transitive deps stay
|
|
252
|
+
// undefined so they fan out across configured sources.
|
|
253
|
+
pinnedSource: pinnedSources.has(name)
|
|
254
|
+
? { name: normalizedDeps[name].source, url: pinnedSources.get(name) }
|
|
255
|
+
: undefined,
|
|
180
256
|
});
|
|
181
257
|
}
|
|
182
258
|
|
|
259
|
+
// ── Resolve effective conf (PLAN-CONF.md §4) ──────────────────────────────
|
|
260
|
+
// Four layers (phase 2): CLI > wyvrn.local.json > profile > recipe.
|
|
261
|
+
// Non-ABI; does not fold into profileHash. Feeds into CMakePresets.json
|
|
262
|
+
// cacheVariables below. `baseProfile` may carry its own `conf` block —
|
|
263
|
+
// an auto-detected profile (no on-disk file) does not.
|
|
264
|
+
let effectiveConfResult;
|
|
265
|
+
try {
|
|
266
|
+
effectiveConfResult = resolveEffectiveConf({
|
|
267
|
+
manifest,
|
|
268
|
+
rootDir,
|
|
269
|
+
cliConf: argv.conf, // yargs array: string[]
|
|
270
|
+
profile: baseProfile, // named profile may carry a `conf` block (phase 2)
|
|
271
|
+
});
|
|
272
|
+
} catch (err) {
|
|
273
|
+
log.error(`conf resolution failed: ${err.message}`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
const effectiveConf = effectiveConfResult.flat;
|
|
277
|
+
if (effectiveConfResult.localOverlayPath) {
|
|
278
|
+
log.info(`Local overlay: ${effectiveConfResult.localOverlayPath}`);
|
|
279
|
+
// One-line nudge when wyvrn.local.json exists but isn't gitignored —
|
|
280
|
+
// catching the existing-project case init.js's scaffold can't cover.
|
|
281
|
+
// Non-fatal; we never rewrite the user's .gitignore at install time.
|
|
282
|
+
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
283
|
+
if (fs.existsSync(gitignorePath)) {
|
|
284
|
+
const gi = fs.readFileSync(gitignorePath, 'utf8');
|
|
285
|
+
const ignored = gi.split(/\r?\n/).some((l) => l.trim() === 'wyvrn.local.json');
|
|
286
|
+
if (!ignored) {
|
|
287
|
+
log.warn(
|
|
288
|
+
`wyvrn.local.json is present but not in ${gitignorePath} — ` +
|
|
289
|
+
`add a line "wyvrn.local.json" to keep dev-local overrides out of version control.`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (Object.keys(effectiveConf).length > 0) {
|
|
295
|
+
log.info(`Effective conf: ${JSON.stringify(effectiveConf)}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── --dry-run: print the resolved plan and exit (EVALUATION.md F6) ────────
|
|
299
|
+
// Runs the resolver + profile/options math end-to-end, then reports what
|
|
300
|
+
// WOULD be installed — no download, no wyvrn.lock, no wyvrn_internal/, no
|
|
301
|
+
// toolchain. Preserves --format=json for CI ingestion.
|
|
302
|
+
if (argv.dryRun) {
|
|
303
|
+
emitDryRunPlan({
|
|
304
|
+
enrichedDeps,
|
|
305
|
+
resolvedDeps,
|
|
306
|
+
baseProfile,
|
|
307
|
+
profileName,
|
|
308
|
+
buildMode,
|
|
309
|
+
packageSources,
|
|
310
|
+
effectiveConf,
|
|
311
|
+
jsonOut,
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
183
316
|
const razerDir = path.join(rootDir, 'wyvrn_internal');
|
|
184
317
|
|
|
185
|
-
// Extract auth from the first install source (if any)
|
|
318
|
+
// Extract auth from the first install source (if any). Prefer
|
|
319
|
+
// `tokenEnv` over a literal `token` stored in config — closes
|
|
320
|
+
// EVALUATION.md S8 (no plaintext secret persisted, value is looked up
|
|
321
|
+
// fresh each run).
|
|
186
322
|
const firstInstallSrc = config.installSources?.[0];
|
|
187
323
|
const downloadOptions = {
|
|
188
324
|
awsProfile: firstInstallSrc?.profile ?? undefined,
|
|
189
|
-
token:
|
|
325
|
+
token: resolveSourceAuth(
|
|
326
|
+
{ token: argv.token, tokenEnv: argv.tokenEnv },
|
|
327
|
+
firstInstallSrc,
|
|
328
|
+
).token,
|
|
190
329
|
buildMode,
|
|
191
330
|
profileName,
|
|
192
331
|
};
|
|
@@ -199,7 +338,10 @@ async function install(argv) {
|
|
|
199
338
|
{
|
|
200
339
|
uploadSource: argv.uploadSource,
|
|
201
340
|
awsProfile: argv.awsProfile ?? firstInstallSrc?.profile,
|
|
202
|
-
token:
|
|
341
|
+
token: resolveSourceAuth(
|
|
342
|
+
{ token: argv.token, tokenEnv: argv.tokenEnv },
|
|
343
|
+
firstInstallSrc,
|
|
344
|
+
).token,
|
|
203
345
|
},
|
|
204
346
|
config,
|
|
205
347
|
);
|
|
@@ -249,6 +391,12 @@ async function install(argv) {
|
|
|
249
391
|
profile: baseProfile,
|
|
250
392
|
profileHash: hashProfile(baseProfile),
|
|
251
393
|
packages: sortedPackages,
|
|
394
|
+
// PLAN-CONF.md §4.8.1: informational only, not consulted during
|
|
395
|
+
// resolution. Omitted entirely when empty so pre-conf locks
|
|
396
|
+
// remain byte-identical to today's output.
|
|
397
|
+
...(Object.keys(effectiveConf).length > 0
|
|
398
|
+
? { effectiveConf: unflattenConf(effectiveConf) }
|
|
399
|
+
: {}),
|
|
252
400
|
};
|
|
253
401
|
|
|
254
402
|
fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2) + '\n', 'utf8');
|
|
@@ -278,6 +426,7 @@ async function install(argv) {
|
|
|
278
426
|
let presetGenerator = null;
|
|
279
427
|
let presetCacheVariables = null;
|
|
280
428
|
let presetInstallDir = null;
|
|
429
|
+
let presetConfigs = null;
|
|
281
430
|
if (hasBuildRecipe(manifest)) {
|
|
282
431
|
try {
|
|
283
432
|
// Resolve the project's own options to expand ${options.*} in its
|
|
@@ -291,7 +440,7 @@ async function install(argv) {
|
|
|
291
440
|
const projectEffectiveOptions = resolveEffectiveOptions(
|
|
292
441
|
projectDeclaration, {}, projectCliOverrides, manifest.name,
|
|
293
442
|
);
|
|
294
|
-
const recipe = normalizeRecipe(manifest.build, projectEffectiveOptions);
|
|
443
|
+
const recipe = normalizeRecipe(manifest.build, projectEffectiveOptions, manifest);
|
|
295
444
|
|
|
296
445
|
if (recipe.generators.length > 0) {
|
|
297
446
|
// First entry in the generator fallback chain. Users whose system
|
|
@@ -316,6 +465,14 @@ async function install(argv) {
|
|
|
316
465
|
// (C:/Program Files on Windows, /usr/local on Unix). Same semantics as
|
|
317
466
|
// the source-build path which passes -DCMAKE_INSTALL_PREFIX explicitly.
|
|
318
467
|
presetInstallDir = recipe.installDir;
|
|
468
|
+
|
|
469
|
+
// Propagate `build.configs` so multi-config generators (Ninja
|
|
470
|
+
// Multi-Config, Visual Studio) generate build files for every
|
|
471
|
+
// config the recipe declares. Pre-2.8.1 the preset only carried
|
|
472
|
+
// Debug/Release, so a recipe listing MinSizeRel would fail with
|
|
473
|
+
// `ninja: error: loading 'build-MinSizeRel.ninja'`. CLAUDE.md §8
|
|
474
|
+
// documented that sharp edge; this closes it.
|
|
475
|
+
presetConfigs = recipe.configs;
|
|
319
476
|
} catch (err) {
|
|
320
477
|
log.warn(
|
|
321
478
|
`could not derive CMake preset config from the project's build recipe: ${err.message}\n` +
|
|
@@ -325,6 +482,31 @@ async function install(argv) {
|
|
|
325
482
|
}
|
|
326
483
|
}
|
|
327
484
|
|
|
485
|
+
// PLAN-CONF.md §4.3 / §5 presets sink: fold effectiveConf's
|
|
486
|
+
// cmake.cache namespace into the preset's cacheVariables. Runs
|
|
487
|
+
// regardless of whether the project has its own build recipe —
|
|
488
|
+
// a consumer with no recipe but a wyvrn.local.json conf still gets
|
|
489
|
+
// their overrides into the generated preset.
|
|
490
|
+
//
|
|
491
|
+
// Conf wins on collisions with build.configure-derived vars. The
|
|
492
|
+
// recipe-normaliser blocks author-side collisions (§4.7); a
|
|
493
|
+
// collision here can only come from a consumer's CLI --conf or
|
|
494
|
+
// wyvrn.local.json overriding a value the author also sets via
|
|
495
|
+
// build.configure. Warn so the override is visible.
|
|
496
|
+
const confCacheVars = cmakeCacheVariables(effectiveConf);
|
|
497
|
+
if (Object.keys(confCacheVars).length > 0) {
|
|
498
|
+
presetCacheVariables = presetCacheVariables ?? {};
|
|
499
|
+
for (const [k, v] of Object.entries(confCacheVars)) {
|
|
500
|
+
if (k in presetCacheVariables && presetCacheVariables[k] !== v) {
|
|
501
|
+
log.warn(
|
|
502
|
+
`conf override: cmake.cache.${k} = ${JSON.stringify(v)} ` +
|
|
503
|
+
`supersedes build.configure's ${JSON.stringify(presetCacheVariables[k])}`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
presetCacheVariables[k] = v;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
328
510
|
const presetResult = generateCMakePresets({
|
|
329
511
|
projectRoot: rootDir,
|
|
330
512
|
profileName,
|
|
@@ -333,6 +515,8 @@ async function install(argv) {
|
|
|
333
515
|
generator: presetGenerator,
|
|
334
516
|
extraCacheVariables: presetCacheVariables,
|
|
335
517
|
installDir: presetInstallDir,
|
|
518
|
+
profileArch: baseProfile.arch ?? null,
|
|
519
|
+
configs: presetConfigs,
|
|
336
520
|
});
|
|
337
521
|
if (presetResult.action === 'skipped') {
|
|
338
522
|
log.warn(
|
|
@@ -396,4 +580,86 @@ async function install(argv) {
|
|
|
396
580
|
}
|
|
397
581
|
}
|
|
398
582
|
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
// --dry-run plan emitter (EVALUATION.md F6)
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Render the resolved install plan WITHOUT touching disk. Called from
|
|
589
|
+
* `install()` when `--dry-run` is passed; exits the install flow before
|
|
590
|
+
* download, lockfile write, or toolchain generation.
|
|
591
|
+
*
|
|
592
|
+
* Text mode: human-readable per-dep summary.
|
|
593
|
+
* JSON mode (`--format=json`): structured payload on stdout with
|
|
594
|
+
* `command: "install-dry-run"` so log consumers can disambiguate it
|
|
595
|
+
* from a real install.
|
|
596
|
+
*/
|
|
597
|
+
function emitDryRunPlan({
|
|
598
|
+
enrichedDeps, resolvedDeps, baseProfile, profileName, buildMode,
|
|
599
|
+
packageSources, effectiveConf = {}, jsonOut,
|
|
600
|
+
}) {
|
|
601
|
+
const baseHash = hashProfile(baseProfile);
|
|
602
|
+
const packages = [...resolvedDeps.keys()].sort().map((name) => {
|
|
603
|
+
const entry = enrichedDeps.get(name) ?? {};
|
|
604
|
+
return {
|
|
605
|
+
name,
|
|
606
|
+
version: resolvedDeps.get(name),
|
|
607
|
+
profileHash: entry.profileHash ?? null,
|
|
608
|
+
options: entry.options ?? null,
|
|
609
|
+
// Whether the dep's effective profile differs from the consumer's
|
|
610
|
+
// base (i.e. per-dep settings override produced a distinct hash).
|
|
611
|
+
overridesBaseProfile: entry.profileHash && entry.profileHash !== baseHash,
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
if (jsonOut) {
|
|
616
|
+
const payload = {
|
|
617
|
+
command: 'install-dry-run',
|
|
618
|
+
wyvrnpmVersion: require('../../package.json').version,
|
|
619
|
+
profile: baseProfile,
|
|
620
|
+
profileName,
|
|
621
|
+
profileHash: baseHash,
|
|
622
|
+
buildMode,
|
|
623
|
+
sources: packageSources,
|
|
624
|
+
effectiveConf: Object.keys(effectiveConf).length > 0
|
|
625
|
+
? unflattenConf(effectiveConf)
|
|
626
|
+
: {},
|
|
627
|
+
packages,
|
|
628
|
+
};
|
|
629
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
log.info('── install plan (--dry-run) ─────────────────────────────────────');
|
|
634
|
+
log.info(`Profile : "${profileName}" [${baseHash}]`);
|
|
635
|
+
log.info(`Build : ${buildMode}`);
|
|
636
|
+
log.info(`Sources : ${packageSources.length > 0 ? packageSources.join(', ') : '(none)'}`);
|
|
637
|
+
const confKeys = Object.keys(effectiveConf).sort();
|
|
638
|
+
if (confKeys.length > 0) {
|
|
639
|
+
log.info(`Conf : ${confKeys.map((k) => `${k}=${effectiveConf[k]}`).join(', ')}`);
|
|
640
|
+
}
|
|
641
|
+
if (packages.length === 0) {
|
|
642
|
+
log.info('(no dependencies)');
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const nameW = Math.max(4, ...packages.map((p) => p.name.length));
|
|
647
|
+
const versionW = Math.max(7, ...packages.map((p) => p.version.length));
|
|
648
|
+
const header = ` ${'name'.padEnd(nameW)} ${'version'.padEnd(versionW)} profileHash options`;
|
|
649
|
+
log.info(header);
|
|
650
|
+
log.info(` ${'-'.repeat(nameW)} ${'-'.repeat(versionW)} ---------------- --------------------`);
|
|
651
|
+
for (const p of packages) {
|
|
652
|
+
const override = p.overridesBaseProfile ? ' *' : ' ';
|
|
653
|
+
const optStr = p.options ? JSON.stringify(p.options) : '-';
|
|
654
|
+
log.info(
|
|
655
|
+
` ${p.name.padEnd(nameW)} ${p.version.padEnd(versionW)} ${p.profileHash ?? '(none) '}${override} ${optStr}`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
log.info('');
|
|
659
|
+
log.info('(nothing was downloaded; wyvrn.lock / wyvrn_internal / toolchain untouched)');
|
|
660
|
+
if (packages.some((p) => p.overridesBaseProfile)) {
|
|
661
|
+
log.info(' * = per-dep settings override produces a profileHash distinct from the base.');
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
399
665
|
module.exports = install;
|