wyvrnpm 2.11.0 → 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 -1905
- package/bin/{wyvrn.js → wyvrnpm.js} +31 -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 -187
- 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/conf/namespaces.js
CHANGED
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Allow-list of recognised `conf` namespaces (PLAN-CONF.md §4.1).
|
|
4
|
-
//
|
|
5
|
-
// Adding a new namespace is an intentional act — the plan commits to
|
|
6
|
-
// expanding this file only when a concrete use case lands, not
|
|
7
|
-
// speculatively. Phase 1 ships `cmake.cache.*` only; every other
|
|
8
|
-
// namespace (env.*, build.*, tool.*) is reserved but not yet valid.
|
|
9
|
-
//
|
|
10
|
-
// Each entry describes how leaf names under that namespace are
|
|
11
|
-
// validated. A namespace with `leaf: true` accepts any non-empty leaf
|
|
12
|
-
// name; a namespace with `leaf: /regex/` enforces the regex at load
|
|
13
|
-
// time. The regex only validates the leaf name — leaf VALUES are
|
|
14
|
-
// normalised separately in ../conf/index.js.
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @typedef {Object} NamespaceSpec
|
|
18
|
-
* @property {RegExp} leaf
|
|
19
|
-
* Pattern that every leaf name under this namespace must satisfy.
|
|
20
|
-
* @property {string} description
|
|
21
|
-
* Human-readable one-liner shown in error messages.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/** @type {Record<string, NamespaceSpec>} */
|
|
25
|
-
const NAMESPACES = {
|
|
26
|
-
'cmake.cache': {
|
|
27
|
-
// CMake variable names: C/C++ identifier rules, no hyphens.
|
|
28
|
-
leaf: /^[A-Za-z_][A-Za-z0-9_]*$/,
|
|
29
|
-
description: 'CMake cache variables — baked into CMakePresets.json cacheVariables.',
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Parse a full dotted conf key (e.g. `cmake.cache.CHROMA_ENABLE_TEST`)
|
|
35
|
-
* into its namespace prefix and leaf name. The namespace is the
|
|
36
|
-
* longest matching prefix registered in NAMESPACES; anything after is
|
|
37
|
-
* the leaf.
|
|
38
|
-
*
|
|
39
|
-
* Returns `{ namespace, leaf }` on success, `null` when the key does
|
|
40
|
-
* not belong to any registered namespace or has no leaf component.
|
|
41
|
-
*
|
|
42
|
-
* @param {string} key
|
|
43
|
-
* @returns {{ namespace: string, leaf: string } | null}
|
|
44
|
-
*/
|
|
45
|
-
function splitKey(key) {
|
|
46
|
-
// Try longer namespaces first so "a.b.c" prefers namespace "a.b"
|
|
47
|
-
// over a hypothetical namespace "a".
|
|
48
|
-
const known = Object.keys(NAMESPACES).sort((a, b) => b.length - a.length);
|
|
49
|
-
for (const ns of known) {
|
|
50
|
-
const prefix = `${ns}.`;
|
|
51
|
-
if (key.startsWith(prefix)) {
|
|
52
|
-
const leaf = key.slice(prefix.length);
|
|
53
|
-
if (leaf.length === 0) return null;
|
|
54
|
-
return { namespace: ns, leaf };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Validate a flat `{key: value}` map against the namespace allow-list.
|
|
62
|
-
* Throws on the first offender (fail-fast, matches the Zip Slip
|
|
63
|
-
* validator's style). Leaf VALUES are not type-checked here — values
|
|
64
|
-
* arrive as strings from the CLI, normalised from JSON elsewhere; type
|
|
65
|
-
* enforcement happens in ../conf/index.js at merge time.
|
|
66
|
-
*
|
|
67
|
-
* @param {Record<string, string>} flatConf
|
|
68
|
-
* @throws {Error} if any key's namespace or leaf is rejected.
|
|
69
|
-
*/
|
|
70
|
-
function validateFlatConfKeys(flatConf) {
|
|
71
|
-
const knownList = Object.keys(NAMESPACES).sort().join(', ');
|
|
72
|
-
for (const key of Object.keys(flatConf)) {
|
|
73
|
-
const parts = splitKey(key);
|
|
74
|
-
if (!parts) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
`unknown conf namespace in ${JSON.stringify(key)} — ` +
|
|
77
|
-
`phase-1 allow-list: [${knownList}]`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
const spec = NAMESPACES[parts.namespace];
|
|
81
|
-
if (!spec.leaf.test(parts.leaf)) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
`invalid leaf name ${JSON.stringify(parts.leaf)} for conf namespace ` +
|
|
84
|
-
`"${parts.namespace}" — ${spec.description}`,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
module.exports = {
|
|
91
|
-
NAMESPACES,
|
|
92
|
-
splitKey,
|
|
93
|
-
validateFlatConfKeys,
|
|
94
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Allow-list of recognised `conf` namespaces (PLAN-CONF.md §4.1).
|
|
4
|
+
//
|
|
5
|
+
// Adding a new namespace is an intentional act — the plan commits to
|
|
6
|
+
// expanding this file only when a concrete use case lands, not
|
|
7
|
+
// speculatively. Phase 1 ships `cmake.cache.*` only; every other
|
|
8
|
+
// namespace (env.*, build.*, tool.*) is reserved but not yet valid.
|
|
9
|
+
//
|
|
10
|
+
// Each entry describes how leaf names under that namespace are
|
|
11
|
+
// validated. A namespace with `leaf: true` accepts any non-empty leaf
|
|
12
|
+
// name; a namespace with `leaf: /regex/` enforces the regex at load
|
|
13
|
+
// time. The regex only validates the leaf name — leaf VALUES are
|
|
14
|
+
// normalised separately in ../conf/index.js.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} NamespaceSpec
|
|
18
|
+
* @property {RegExp} leaf
|
|
19
|
+
* Pattern that every leaf name under this namespace must satisfy.
|
|
20
|
+
* @property {string} description
|
|
21
|
+
* Human-readable one-liner shown in error messages.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** @type {Record<string, NamespaceSpec>} */
|
|
25
|
+
const NAMESPACES = {
|
|
26
|
+
'cmake.cache': {
|
|
27
|
+
// CMake variable names: C/C++ identifier rules, no hyphens.
|
|
28
|
+
leaf: /^[A-Za-z_][A-Za-z0-9_]*$/,
|
|
29
|
+
description: 'CMake cache variables — baked into CMakePresets.json cacheVariables.',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a full dotted conf key (e.g. `cmake.cache.CHROMA_ENABLE_TEST`)
|
|
35
|
+
* into its namespace prefix and leaf name. The namespace is the
|
|
36
|
+
* longest matching prefix registered in NAMESPACES; anything after is
|
|
37
|
+
* the leaf.
|
|
38
|
+
*
|
|
39
|
+
* Returns `{ namespace, leaf }` on success, `null` when the key does
|
|
40
|
+
* not belong to any registered namespace or has no leaf component.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} key
|
|
43
|
+
* @returns {{ namespace: string, leaf: string } | null}
|
|
44
|
+
*/
|
|
45
|
+
function splitKey(key) {
|
|
46
|
+
// Try longer namespaces first so "a.b.c" prefers namespace "a.b"
|
|
47
|
+
// over a hypothetical namespace "a".
|
|
48
|
+
const known = Object.keys(NAMESPACES).sort((a, b) => b.length - a.length);
|
|
49
|
+
for (const ns of known) {
|
|
50
|
+
const prefix = `${ns}.`;
|
|
51
|
+
if (key.startsWith(prefix)) {
|
|
52
|
+
const leaf = key.slice(prefix.length);
|
|
53
|
+
if (leaf.length === 0) return null;
|
|
54
|
+
return { namespace: ns, leaf };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate a flat `{key: value}` map against the namespace allow-list.
|
|
62
|
+
* Throws on the first offender (fail-fast, matches the Zip Slip
|
|
63
|
+
* validator's style). Leaf VALUES are not type-checked here — values
|
|
64
|
+
* arrive as strings from the CLI, normalised from JSON elsewhere; type
|
|
65
|
+
* enforcement happens in ../conf/index.js at merge time.
|
|
66
|
+
*
|
|
67
|
+
* @param {Record<string, string>} flatConf
|
|
68
|
+
* @throws {Error} if any key's namespace or leaf is rejected.
|
|
69
|
+
*/
|
|
70
|
+
function validateFlatConfKeys(flatConf) {
|
|
71
|
+
const knownList = Object.keys(NAMESPACES).sort().join(', ');
|
|
72
|
+
for (const key of Object.keys(flatConf)) {
|
|
73
|
+
const parts = splitKey(key);
|
|
74
|
+
if (!parts) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`unknown conf namespace in ${JSON.stringify(key)} — ` +
|
|
77
|
+
`phase-1 allow-list: [${knownList}]`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const spec = NAMESPACES[parts.namespace];
|
|
81
|
+
if (!spec.leaf.test(parts.leaf)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`invalid leaf name ${JSON.stringify(parts.leaf)} for conf namespace ` +
|
|
84
|
+
`"${parts.namespace}" — ${spec.description}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
NAMESPACES,
|
|
92
|
+
splitKey,
|
|
93
|
+
validateFlatConfKeys,
|
|
94
|
+
};
|
package/src/context.js
CHANGED
|
@@ -1,230 +1,230 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* CommandContext — the shared per-invocation state every command consumes.
|
|
5
|
-
*
|
|
6
|
-
* Commands used to each re-read the manifest, re-resolve the profile name,
|
|
7
|
-
* re-read config, re-resolve auth, and each toggle JSON mode on the logger.
|
|
8
|
-
* The divergence was cheap at 11 commands; it starts costing real bugs
|
|
9
|
-
* once F3 (`tool_requires`) and F4 (build/host profile split) double the
|
|
10
|
-
* flag surface. See [claude/PLAN-COMMAND-CONTEXT.md](../claude/PLAN-COMMAND-CONTEXT.md).
|
|
11
|
-
*
|
|
12
|
-
* `buildContext(argv)` runs once at the top of each command. It:
|
|
13
|
-
* - resolves filesystem anchors (rootDir, manifestPath)
|
|
14
|
-
* - reads config once
|
|
15
|
-
* - resolves the profile-name fallback chain + loads the profile
|
|
16
|
-
* - parses CLI options + conf (format-level only; namespace allow-
|
|
17
|
-
* listing happens later in the sinks that need it)
|
|
18
|
-
* - sets JSON mode on the logger if --format=json
|
|
19
|
-
* - exposes an auth helper pre-curried with CLI --token / --token-env
|
|
20
|
-
* - exposes a lazy manifest getter (show never reads the manifest)
|
|
21
|
-
*
|
|
22
|
-
* `buildContext` does NOT do source filtering — each command has subtly
|
|
23
|
-
* different CLI shape there (publish takes name-or-URL, install/show
|
|
24
|
-
* take URL arrays, pin-resolution only matters for install). That
|
|
25
|
-
* stays per-command until a second feature shows the same duplication.
|
|
26
|
-
*
|
|
27
|
-
* Errors: `buildContext` THROWS on all input-validation failures. Each
|
|
28
|
-
* command wraps the call in one try/catch that does `log.error + exit`.
|
|
29
|
-
* This keeps buildContext unit-testable; behaviour from the user's
|
|
30
|
-
* point of view is unchanged.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
const fs = require('fs');
|
|
34
|
-
const path = require('path');
|
|
35
|
-
|
|
36
|
-
const { readConfig } = require('./config');
|
|
37
|
-
const { loadProfile } = require('./profile');
|
|
38
|
-
const { readManifest } = require('./manifest');
|
|
39
|
-
const { resolveSourceAuth } = require('./auth');
|
|
40
|
-
const { parseCliOptions } = require('./options');
|
|
41
|
-
const { parseCliConf } = require('./conf');
|
|
42
|
-
const log = require('./logger');
|
|
43
|
-
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// Internal helpers
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Resolve the active profile name per the long-standing precedence:
|
|
50
|
-
* --profile > config.defaultProfile > 'default'
|
|
51
|
-
*/
|
|
52
|
-
function resolveProfileName(argv, config) {
|
|
53
|
-
if (typeof argv.profile === 'string' && argv.profile.length > 0) {
|
|
54
|
-
return argv.profile;
|
|
55
|
-
}
|
|
56
|
-
if (typeof config.defaultProfile === 'string' && config.defaultProfile.length > 0) {
|
|
57
|
-
return config.defaultProfile;
|
|
58
|
-
}
|
|
59
|
-
return 'default';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Normalise CLI flags into the stable `ctx.flags` shape. Centralising
|
|
64
|
-
* defaults here means individual commands don't each re-apply
|
|
65
|
-
* `?? 'never'` / `?? 'Release'` / etc.
|
|
66
|
-
*
|
|
67
|
-
* Flags that are command-specific (argv.preset, argv.pkg, argv.clean,
|
|
68
|
-
* argv.verbose, argv.generator, argv.install, argv.installPrefix,
|
|
69
|
-
* argv.uploadSource, argv.autoInstall, argv.noGpgSign, ...) stay on
|
|
70
|
-
* argv and commands read them there. `ctx.argv` is the escape hatch.
|
|
71
|
-
*/
|
|
72
|
-
function normaliseFlags(argv) {
|
|
73
|
-
return {
|
|
74
|
-
format: argv.format === 'json' ? 'json' : 'text',
|
|
75
|
-
dryRun: argv.dryRun === true,
|
|
76
|
-
build: argv.build ?? 'never',
|
|
77
|
-
uploadBuilt: argv.uploadBuilt === true,
|
|
78
|
-
force: argv.force === true,
|
|
79
|
-
platform: argv.platform ?? defaultPlatform(),
|
|
80
|
-
timeout: typeof argv.timeout === 'number' ? argv.timeout : 300,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Default platform string when --platform is absent. Mirrors what
|
|
86
|
-
* bin/
|
|
87
|
-
* it here makes context.js testable without exercising the CLI layer.
|
|
88
|
-
*/
|
|
89
|
-
function defaultPlatform() {
|
|
90
|
-
if (process.platform === 'win32') {
|
|
91
|
-
return process.arch === 'ia32' ? 'win_x86' : 'win_x64';
|
|
92
|
-
}
|
|
93
|
-
if (process.platform === 'darwin') return 'macos';
|
|
94
|
-
return 'linux';
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Produce a frozen `{ host, build }` profile pair. Today both point at
|
|
99
|
-
* the same resolved object — when F4 (build/host profile split) lands,
|
|
100
|
-
* only this function changes; every command reading `ctx.profiles.host`
|
|
101
|
-
* or `ctx.profiles.build` continues to work.
|
|
102
|
-
*
|
|
103
|
-
* The two sub-objects share a reference today. A regression test in
|
|
104
|
-
* tests/context.test.js asserts `ctx.profiles.host === ctx.profiles.build`
|
|
105
|
-
* — that test is expected to be removed (not fixed) when F4 makes them
|
|
106
|
-
* legitimately distinct.
|
|
107
|
-
*/
|
|
108
|
-
function resolveProfiles(argv, config) {
|
|
109
|
-
const name = resolveProfileName(argv, config);
|
|
110
|
-
const { profile, fromFile } = loadProfile(name);
|
|
111
|
-
const entry = Object.freeze({ name, profile, fromFile });
|
|
112
|
-
return Object.freeze({ host: entry, build: entry });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
// buildContext
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Build the per-invocation CommandContext.
|
|
121
|
-
*
|
|
122
|
-
* @param {object} argv - yargs-parsed argument map. Fields read:
|
|
123
|
-
* root, manifest, profile, format, dryRun, build, uploadBuilt,
|
|
124
|
-
* force, platform, timeout, token, tokenEnv, option, conf.
|
|
125
|
-
* Command-specific fields pass through on ctx.argv unchanged.
|
|
126
|
-
*
|
|
127
|
-
* @returns {CommandContext}
|
|
128
|
-
*
|
|
129
|
-
* @throws on:
|
|
130
|
-
* - parseCliOptions: malformed `-o pkg:name=value`
|
|
131
|
-
* - parseCliConf: malformed `--conf KEY=VALUE`
|
|
132
|
-
* - loadProfile: malformed profile JSON, cycle in `extends`, etc.
|
|
133
|
-
*
|
|
134
|
-
* Does NOT throw on:
|
|
135
|
-
* - missing manifest (lazy — throws at first ctx.manifest access)
|
|
136
|
-
* - missing config (readConfig returns empty defaults)
|
|
137
|
-
* - missing profile (loadProfile falls back to detectProfile())
|
|
138
|
-
* - missing --token-env value (resolveSourceAuth throws at ctx.auth.for time)
|
|
139
|
-
*/
|
|
140
|
-
function buildContext(argv = {}) {
|
|
141
|
-
// 1. Filesystem anchors — pure path math, no I/O.
|
|
142
|
-
const rootDir = path.resolve(argv.root ?? '.');
|
|
143
|
-
const manifestPath = path.resolve(
|
|
144
|
-
argv.manifest ?? path.join(rootDir, 'wyvrn.json'),
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// 2. Config — sync, small file, every command needs at least .defaultProfile.
|
|
148
|
-
const config = readConfig();
|
|
149
|
-
|
|
150
|
-
// 3. Profile (F4-ready shape).
|
|
151
|
-
const profiles = resolveProfiles(argv, config);
|
|
152
|
-
|
|
153
|
-
// 4. CLI auth, kept as the raw pair. Actual token lookup happens
|
|
154
|
-
// through ctx.auth.for(sourceEntry), which runs resolveSourceAuth
|
|
155
|
-
// each time so --token-env misses surface at use (same as today).
|
|
156
|
-
const authCli = Object.freeze({
|
|
157
|
-
token: typeof argv.token === 'string' ? argv.token : undefined,
|
|
158
|
-
tokenEnv: typeof argv.tokenEnv === 'string' ? argv.tokenEnv : undefined,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// 5. CLI options + conf — format-level parsing only. Namespace
|
|
162
|
-
// allow-listing for conf happens later in resolveEffectiveConf;
|
|
163
|
-
// options validation against the recipe's declared `allowed` set
|
|
164
|
-
// happens at resolveEffectiveOptions time.
|
|
165
|
-
// Both can throw — that's the contract: buildContext surfaces
|
|
166
|
-
// input-level errors up front.
|
|
167
|
-
const cliOptions = parseCliOptions(argv.option);
|
|
168
|
-
const cliConf = parseCliConf(argv.conf);
|
|
169
|
-
|
|
170
|
-
// 6. Flags.
|
|
171
|
-
const flags = normaliseFlags(argv);
|
|
172
|
-
|
|
173
|
-
// 7. JSON mode — side effect that must happen before the command
|
|
174
|
-
// starts logging. Flipping the logger here means every command
|
|
175
|
-
// that constructs a ctx gets JSON mode right automatically.
|
|
176
|
-
if (flags.format === 'json') log.setJsonMode(true);
|
|
177
|
-
|
|
178
|
-
// 8. Assemble.
|
|
179
|
-
const ctx = {
|
|
180
|
-
rootDir,
|
|
181
|
-
manifestPath,
|
|
182
|
-
cwd: process.cwd(),
|
|
183
|
-
profiles,
|
|
184
|
-
config,
|
|
185
|
-
auth: Object.freeze({
|
|
186
|
-
cli: authCli,
|
|
187
|
-
for: (sourceEntry) => resolveSourceAuth(authCli, sourceEntry ?? null),
|
|
188
|
-
}),
|
|
189
|
-
cliOptions,
|
|
190
|
-
cliConf,
|
|
191
|
-
flags,
|
|
192
|
-
argv,
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// 9. Lazy manifest getter. Most commands need it; `show` does not.
|
|
196
|
-
// Defining as a getter means:
|
|
197
|
-
// - show pays nothing.
|
|
198
|
-
// - other commands pay once, cached.
|
|
199
|
-
// - an invalid manifest surfaces at first use, not at ctx
|
|
200
|
-
// construction — so `show` never trips over it.
|
|
201
|
-
let manifestCache = null;
|
|
202
|
-
let manifestLoaded = false;
|
|
203
|
-
Object.defineProperty(ctx, 'manifest', {
|
|
204
|
-
configurable: false,
|
|
205
|
-
enumerable: true,
|
|
206
|
-
get() {
|
|
207
|
-
if (!manifestLoaded) {
|
|
208
|
-
if (!fs.existsSync(manifestPath)) {
|
|
209
|
-
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
210
|
-
}
|
|
211
|
-
manifestCache = readManifest(manifestPath);
|
|
212
|
-
manifestLoaded = true;
|
|
213
|
-
}
|
|
214
|
-
return manifestCache;
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
return ctx;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
module.exports = {
|
|
222
|
-
buildContext,
|
|
223
|
-
// Exported for unit tests — kept out of the default import surface.
|
|
224
|
-
_helpers: {
|
|
225
|
-
resolveProfileName,
|
|
226
|
-
normaliseFlags,
|
|
227
|
-
defaultPlatform,
|
|
228
|
-
resolveProfiles,
|
|
229
|
-
},
|
|
230
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CommandContext — the shared per-invocation state every command consumes.
|
|
5
|
+
*
|
|
6
|
+
* Commands used to each re-read the manifest, re-resolve the profile name,
|
|
7
|
+
* re-read config, re-resolve auth, and each toggle JSON mode on the logger.
|
|
8
|
+
* The divergence was cheap at 11 commands; it starts costing real bugs
|
|
9
|
+
* once F3 (`tool_requires`) and F4 (build/host profile split) double the
|
|
10
|
+
* flag surface. See [claude/PLAN-COMMAND-CONTEXT.md](../claude/PLAN-COMMAND-CONTEXT.md).
|
|
11
|
+
*
|
|
12
|
+
* `buildContext(argv)` runs once at the top of each command. It:
|
|
13
|
+
* - resolves filesystem anchors (rootDir, manifestPath)
|
|
14
|
+
* - reads config once
|
|
15
|
+
* - resolves the profile-name fallback chain + loads the profile
|
|
16
|
+
* - parses CLI options + conf (format-level only; namespace allow-
|
|
17
|
+
* listing happens later in the sinks that need it)
|
|
18
|
+
* - sets JSON mode on the logger if --format=json
|
|
19
|
+
* - exposes an auth helper pre-curried with CLI --token / --token-env
|
|
20
|
+
* - exposes a lazy manifest getter (show never reads the manifest)
|
|
21
|
+
*
|
|
22
|
+
* `buildContext` does NOT do source filtering — each command has subtly
|
|
23
|
+
* different CLI shape there (publish takes name-or-URL, install/show
|
|
24
|
+
* take URL arrays, pin-resolution only matters for install). That
|
|
25
|
+
* stays per-command until a second feature shows the same duplication.
|
|
26
|
+
*
|
|
27
|
+
* Errors: `buildContext` THROWS on all input-validation failures. Each
|
|
28
|
+
* command wraps the call in one try/catch that does `log.error + exit`.
|
|
29
|
+
* This keeps buildContext unit-testable; behaviour from the user's
|
|
30
|
+
* point of view is unchanged.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const fs = require('fs');
|
|
34
|
+
const path = require('path');
|
|
35
|
+
|
|
36
|
+
const { readConfig } = require('./config');
|
|
37
|
+
const { loadProfile } = require('./profile');
|
|
38
|
+
const { readManifest } = require('./manifest');
|
|
39
|
+
const { resolveSourceAuth } = require('./auth');
|
|
40
|
+
const { parseCliOptions } = require('./options');
|
|
41
|
+
const { parseCliConf } = require('./conf');
|
|
42
|
+
const log = require('./logger');
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Internal helpers
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the active profile name per the long-standing precedence:
|
|
50
|
+
* --profile > config.defaultProfile > 'default'
|
|
51
|
+
*/
|
|
52
|
+
function resolveProfileName(argv, config) {
|
|
53
|
+
if (typeof argv.profile === 'string' && argv.profile.length > 0) {
|
|
54
|
+
return argv.profile;
|
|
55
|
+
}
|
|
56
|
+
if (typeof config.defaultProfile === 'string' && config.defaultProfile.length > 0) {
|
|
57
|
+
return config.defaultProfile;
|
|
58
|
+
}
|
|
59
|
+
return 'default';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalise CLI flags into the stable `ctx.flags` shape. Centralising
|
|
64
|
+
* defaults here means individual commands don't each re-apply
|
|
65
|
+
* `?? 'never'` / `?? 'Release'` / etc.
|
|
66
|
+
*
|
|
67
|
+
* Flags that are command-specific (argv.preset, argv.pkg, argv.clean,
|
|
68
|
+
* argv.verbose, argv.generator, argv.install, argv.installPrefix,
|
|
69
|
+
* argv.uploadSource, argv.autoInstall, argv.noGpgSign, ...) stay on
|
|
70
|
+
* argv and commands read them there. `ctx.argv` is the escape hatch.
|
|
71
|
+
*/
|
|
72
|
+
function normaliseFlags(argv) {
|
|
73
|
+
return {
|
|
74
|
+
format: argv.format === 'json' ? 'json' : 'text',
|
|
75
|
+
dryRun: argv.dryRun === true,
|
|
76
|
+
build: argv.build ?? 'never',
|
|
77
|
+
uploadBuilt: argv.uploadBuilt === true,
|
|
78
|
+
force: argv.force === true,
|
|
79
|
+
platform: argv.platform ?? defaultPlatform(),
|
|
80
|
+
timeout: typeof argv.timeout === 'number' ? argv.timeout : 300,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Default platform string when --platform is absent. Mirrors what
|
|
86
|
+
* bin/wyvrnpm.js passes when yargs applies its own default — keeping
|
|
87
|
+
* it here makes context.js testable without exercising the CLI layer.
|
|
88
|
+
*/
|
|
89
|
+
function defaultPlatform() {
|
|
90
|
+
if (process.platform === 'win32') {
|
|
91
|
+
return process.arch === 'ia32' ? 'win_x86' : 'win_x64';
|
|
92
|
+
}
|
|
93
|
+
if (process.platform === 'darwin') return 'macos';
|
|
94
|
+
return 'linux';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Produce a frozen `{ host, build }` profile pair. Today both point at
|
|
99
|
+
* the same resolved object — when F4 (build/host profile split) lands,
|
|
100
|
+
* only this function changes; every command reading `ctx.profiles.host`
|
|
101
|
+
* or `ctx.profiles.build` continues to work.
|
|
102
|
+
*
|
|
103
|
+
* The two sub-objects share a reference today. A regression test in
|
|
104
|
+
* tests/context.test.js asserts `ctx.profiles.host === ctx.profiles.build`
|
|
105
|
+
* — that test is expected to be removed (not fixed) when F4 makes them
|
|
106
|
+
* legitimately distinct.
|
|
107
|
+
*/
|
|
108
|
+
function resolveProfiles(argv, config) {
|
|
109
|
+
const name = resolveProfileName(argv, config);
|
|
110
|
+
const { profile, fromFile } = loadProfile(name);
|
|
111
|
+
const entry = Object.freeze({ name, profile, fromFile });
|
|
112
|
+
return Object.freeze({ host: entry, build: entry });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// buildContext
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build the per-invocation CommandContext.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} argv - yargs-parsed argument map. Fields read:
|
|
123
|
+
* root, manifest, profile, format, dryRun, build, uploadBuilt,
|
|
124
|
+
* force, platform, timeout, token, tokenEnv, option, conf.
|
|
125
|
+
* Command-specific fields pass through on ctx.argv unchanged.
|
|
126
|
+
*
|
|
127
|
+
* @returns {CommandContext}
|
|
128
|
+
*
|
|
129
|
+
* @throws on:
|
|
130
|
+
* - parseCliOptions: malformed `-o pkg:name=value`
|
|
131
|
+
* - parseCliConf: malformed `--conf KEY=VALUE`
|
|
132
|
+
* - loadProfile: malformed profile JSON, cycle in `extends`, etc.
|
|
133
|
+
*
|
|
134
|
+
* Does NOT throw on:
|
|
135
|
+
* - missing manifest (lazy — throws at first ctx.manifest access)
|
|
136
|
+
* - missing config (readConfig returns empty defaults)
|
|
137
|
+
* - missing profile (loadProfile falls back to detectProfile())
|
|
138
|
+
* - missing --token-env value (resolveSourceAuth throws at ctx.auth.for time)
|
|
139
|
+
*/
|
|
140
|
+
function buildContext(argv = {}) {
|
|
141
|
+
// 1. Filesystem anchors — pure path math, no I/O.
|
|
142
|
+
const rootDir = path.resolve(argv.root ?? '.');
|
|
143
|
+
const manifestPath = path.resolve(
|
|
144
|
+
argv.manifest ?? path.join(rootDir, 'wyvrn.json'),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// 2. Config — sync, small file, every command needs at least .defaultProfile.
|
|
148
|
+
const config = readConfig();
|
|
149
|
+
|
|
150
|
+
// 3. Profile (F4-ready shape).
|
|
151
|
+
const profiles = resolveProfiles(argv, config);
|
|
152
|
+
|
|
153
|
+
// 4. CLI auth, kept as the raw pair. Actual token lookup happens
|
|
154
|
+
// through ctx.auth.for(sourceEntry), which runs resolveSourceAuth
|
|
155
|
+
// each time so --token-env misses surface at use (same as today).
|
|
156
|
+
const authCli = Object.freeze({
|
|
157
|
+
token: typeof argv.token === 'string' ? argv.token : undefined,
|
|
158
|
+
tokenEnv: typeof argv.tokenEnv === 'string' ? argv.tokenEnv : undefined,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 5. CLI options + conf — format-level parsing only. Namespace
|
|
162
|
+
// allow-listing for conf happens later in resolveEffectiveConf;
|
|
163
|
+
// options validation against the recipe's declared `allowed` set
|
|
164
|
+
// happens at resolveEffectiveOptions time.
|
|
165
|
+
// Both can throw — that's the contract: buildContext surfaces
|
|
166
|
+
// input-level errors up front.
|
|
167
|
+
const cliOptions = parseCliOptions(argv.option);
|
|
168
|
+
const cliConf = parseCliConf(argv.conf);
|
|
169
|
+
|
|
170
|
+
// 6. Flags.
|
|
171
|
+
const flags = normaliseFlags(argv);
|
|
172
|
+
|
|
173
|
+
// 7. JSON mode — side effect that must happen before the command
|
|
174
|
+
// starts logging. Flipping the logger here means every command
|
|
175
|
+
// that constructs a ctx gets JSON mode right automatically.
|
|
176
|
+
if (flags.format === 'json') log.setJsonMode(true);
|
|
177
|
+
|
|
178
|
+
// 8. Assemble.
|
|
179
|
+
const ctx = {
|
|
180
|
+
rootDir,
|
|
181
|
+
manifestPath,
|
|
182
|
+
cwd: process.cwd(),
|
|
183
|
+
profiles,
|
|
184
|
+
config,
|
|
185
|
+
auth: Object.freeze({
|
|
186
|
+
cli: authCli,
|
|
187
|
+
for: (sourceEntry) => resolveSourceAuth(authCli, sourceEntry ?? null),
|
|
188
|
+
}),
|
|
189
|
+
cliOptions,
|
|
190
|
+
cliConf,
|
|
191
|
+
flags,
|
|
192
|
+
argv,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// 9. Lazy manifest getter. Most commands need it; `show` does not.
|
|
196
|
+
// Defining as a getter means:
|
|
197
|
+
// - show pays nothing.
|
|
198
|
+
// - other commands pay once, cached.
|
|
199
|
+
// - an invalid manifest surfaces at first use, not at ctx
|
|
200
|
+
// construction — so `show` never trips over it.
|
|
201
|
+
let manifestCache = null;
|
|
202
|
+
let manifestLoaded = false;
|
|
203
|
+
Object.defineProperty(ctx, 'manifest', {
|
|
204
|
+
configurable: false,
|
|
205
|
+
enumerable: true,
|
|
206
|
+
get() {
|
|
207
|
+
if (!manifestLoaded) {
|
|
208
|
+
if (!fs.existsSync(manifestPath)) {
|
|
209
|
+
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
210
|
+
}
|
|
211
|
+
manifestCache = readManifest(manifestPath);
|
|
212
|
+
manifestLoaded = true;
|
|
213
|
+
}
|
|
214
|
+
return manifestCache;
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return ctx;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
buildContext,
|
|
223
|
+
// Exported for unit tests — kept out of the default import surface.
|
|
224
|
+
_helpers: {
|
|
225
|
+
resolveProfileName,
|
|
226
|
+
normaliseFlags,
|
|
227
|
+
defaultPlatform,
|
|
228
|
+
resolveProfiles,
|
|
229
|
+
},
|
|
230
|
+
};
|