wyvrnpm 2.10.2 → 2.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1914 -1860
  2. package/bin/{wyvrn.js → wyvrnpm.js} +66 -0
  3. package/cmake/cpp.cmake +9 -9
  4. package/cmake/functions.cmake +224 -224
  5. package/cmake/macros.cmake +284 -284
  6. package/package.json +3 -2
  7. package/src/auth.js +66 -66
  8. package/src/binary-dir.js +95 -0
  9. package/src/bootstrap/cookbook.js +196 -196
  10. package/src/bootstrap/detect.js +150 -150
  11. package/src/bootstrap/index.js +220 -220
  12. package/src/bootstrap/version.js +72 -72
  13. package/src/build/cache.js +344 -344
  14. package/src/build/clone.js +170 -170
  15. package/src/build/cmake.js +342 -342
  16. package/src/build/index.js +299 -297
  17. package/src/build/msvc-env.js +260 -260
  18. package/src/build/recipe.js +188 -188
  19. package/src/commands/add.js +141 -141
  20. package/src/commands/bootstrap.js +96 -96
  21. package/src/commands/build.js +482 -452
  22. package/src/commands/cache.js +189 -189
  23. package/src/commands/clean.js +80 -80
  24. package/src/commands/configure.js +92 -92
  25. package/src/commands/init.js +70 -70
  26. package/src/commands/install-skill.js +115 -115
  27. package/src/commands/install.js +730 -674
  28. package/src/commands/link.js +320 -320
  29. package/src/commands/profile.js +237 -237
  30. package/src/commands/publish.js +584 -555
  31. package/src/commands/show.js +252 -252
  32. package/src/commands/version.js +187 -0
  33. package/src/compat.js +273 -273
  34. package/src/conf/index.js +415 -391
  35. package/src/conf/namespaces.js +94 -94
  36. package/src/context.js +230 -230
  37. package/src/http-fetch.js +53 -53
  38. package/src/ignore.js +118 -118
  39. package/src/logger.js +122 -122
  40. package/src/options.js +303 -303
  41. package/src/settings-overrides.js +152 -152
  42. package/src/toolchain/deps.js +164 -164
  43. package/src/toolchain/index.js +212 -212
  44. package/src/toolchain/presets.js +356 -340
  45. package/src/toolchain/template.cmake +77 -77
  46. package/src/upload-built.js +265 -265
  47. package/src/version-range.js +301 -301
  48. package/src/zip-safe.js +52 -52
  49. package/src/zip-stream.js +126 -0
@@ -1,188 +1,188 @@
1
- 'use strict';
2
-
3
- const { substituteTemplateAll } = require('../options');
4
- const { readRecipeConf } = require('../conf');
5
-
6
- /**
7
- * The `build` section of a source-repo `wyvrn.json` tells wyvrnpm how to
8
- * build the package from source when `--build=missing` fires. Only
9
- * `system: "cmake"` is supported in phase 3.
10
- *
11
- * Example — single generator (strict; CMake errors if Ninja isn't installed):
12
- * "build": {
13
- * "system": "cmake",
14
- * "generator": "Ninja",
15
- * "configure": ["-DZLIB_BUILD_EXAMPLES=OFF"],
16
- * "buildArgs": ["--parallel"],
17
- * "installDir": "install",
18
- * "sourceSubdir": "."
19
- * }
20
- *
21
- * Example — generator fallback chain (tried in order, first available wins;
22
- * falls through to CMake's default if none are present):
23
- * "build": {
24
- * "generator": ["Ninja", "Visual Studio 17 2022", "Visual Studio 16 2019"]
25
- * }
26
- */
27
-
28
- const DEFAULT_RECIPE = Object.freeze({
29
- system: 'cmake',
30
- // Null / empty array → let CMake pick its default
31
- // (Visual Studio on Windows with VS installed, Unix Makefiles on Linux/macOS).
32
- generators: Object.freeze([]),
33
- // All four CMake configurations are built and installed by default so
34
- // downstream consumers can link against whichever they need. Restrict via
35
- // the recipe (e.g. `"configs": ["Release"]`) to cut source-build time.
36
- configs: Object.freeze(['Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel']),
37
- configure: Object.freeze([]),
38
- buildArgs: Object.freeze([]),
39
- installDir: 'install', // relative to binary dir; becomes CMAKE_INSTALL_PREFIX
40
- sourceSubdir: '.', // where CMakeLists.txt lives relative to the clone root
41
- requiredTools: Object.freeze([]),
42
- });
43
-
44
- const VALID_CMAKE_CONFIGS = new Set(['Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel']);
45
-
46
- const SUPPORTED_SYSTEMS = new Set(['cmake']);
47
-
48
- /**
49
- * Normalise the `build` section of a manifest into a concrete, complete
50
- * recipe. Missing fields fall back to `DEFAULT_RECIPE`.
51
- *
52
- * `generator` accepts either a string (strict — single generator) or an
53
- * array of strings (fallback chain — tried in order; first available wins).
54
- * Both forms produce a `generators: string[]` field in the normalised recipe.
55
- *
56
- * When `effectiveOptions` is supplied, `${options.<name>}` tokens inside
57
- * `configure` and `buildArgs` are expanded using those values — bool → ON/OFF,
58
- * strings verbatim. An unknown option in a template is a hard error (fail
59
- * fast so the bad arg never reaches cmake). See src/options.js
60
- * `substituteTemplate`. When `effectiveOptions` is null (recipe declares
61
- * no options), any `${options.*}` token in the recipe also fails fast —
62
- * recipes don't get to reference undeclared options.
63
- *
64
- * When `manifestForConfCollisionCheck` is supplied, PLAN-CONF.md §4.7's
65
- * collision detection runs: any `-DKEY=...` in `build.configure` whose
66
- * KEY is ALSO declared in recipe-level `conf.cmake.cache` is a hard
67
- * error. Recipes pick ONE route, avoiding silent duplication.
68
- *
69
- * @param {object|null|undefined} rawBuild
70
- * @param {Record<string, any>|null} [effectiveOptions]
71
- * @param {object|null} [manifestForConfCollisionCheck]
72
- * @returns {{ system: string, generators: string[], configure: string[],
73
- * buildArgs: string[], installDir: string, sourceSubdir: string,
74
- * requiredTools: string[] }}
75
- * @throws {Error} when `system` is set but not supported, when a
76
- * `${options.<name>}` template references an undeclared option,
77
- * or when `build.configure` collides with `conf.cmake.cache`.
78
- */
79
- function normalizeRecipe(rawBuild, effectiveOptions = null, manifestForConfCollisionCheck = null) {
80
- const b = rawBuild ?? {};
81
- const system = b.system ?? DEFAULT_RECIPE.system;
82
- if (!SUPPORTED_SYSTEMS.has(system)) {
83
- throw new Error(
84
- `build.system "${system}" is not supported. ` +
85
- `Supported: ${[...SUPPORTED_SYSTEMS].join(', ')}.`,
86
- );
87
- }
88
-
89
- // Accept generator as string | string[] | null for user convenience;
90
- // canonicalise to an array.
91
- let generators;
92
- if (typeof b.generator === 'string') {
93
- generators = b.generator.trim() ? [b.generator] : [];
94
- } else if (Array.isArray(b.generator)) {
95
- generators = b.generator.filter((g) => typeof g === 'string' && g.trim());
96
- } else {
97
- generators = [];
98
- }
99
-
100
- // Drop empty-string entries so a stray `""` in the user's JSON doesn't
101
- // become an empty-string argument to CMake / the build tool.
102
- const cleanArray = (arr) =>
103
- Array.isArray(arr)
104
- ? arr.filter((s) => typeof s === 'string' && s.trim().length > 0)
105
- : [];
106
-
107
- // Accept `configs` as string | string[]. Default to all four canonical
108
- // CMake configurations. Unknown entries are silently dropped so a typo
109
- // doesn't blow up the build loop.
110
- let configs;
111
- if (typeof b.configs === 'string') {
112
- configs = [b.configs];
113
- } else if (Array.isArray(b.configs)) {
114
- configs = b.configs.filter((c) => typeof c === 'string' && c.trim());
115
- } else {
116
- configs = DEFAULT_RECIPE.configs.slice();
117
- }
118
- configs = configs.filter((c) => VALID_CMAKE_CONFIGS.has(c));
119
- if (configs.length === 0) configs = DEFAULT_RECIPE.configs.slice();
120
-
121
- // Expand ${options.<name>} tokens in args the recipe passes to cmake.
122
- // Only `configure` and `buildArgs` are user-authored arg lists; the
123
- // other fields are literals.
124
- const configure = substituteTemplateAll(
125
- cleanArray(b.configure),
126
- effectiveOptions,
127
- 'build.configure',
128
- );
129
- const buildArgs = substituteTemplateAll(
130
- cleanArray(b.buildArgs),
131
- effectiveOptions,
132
- 'build.buildArgs',
133
- );
134
-
135
- // PLAN-CONF.md §4.7: recipe-side collision between `build.configure`
136
- // `-DKEY=...` args and declared `conf.cmake.cache.KEY` is a hard
137
- // error. Forces the author to pick one route; avoids silent
138
- // duplication where one route wins opaquely.
139
- if (manifestForConfCollisionCheck) {
140
- const recipeConfFlat = readRecipeConf(manifestForConfCollisionCheck);
141
- const confCacheKeys = new Set(
142
- Object.keys(recipeConfFlat)
143
- .filter((k) => k.startsWith('cmake.cache.'))
144
- .map((k) => k.slice('cmake.cache.'.length)),
145
- );
146
- if (confCacheKeys.size > 0) {
147
- for (const arg of configure) {
148
- const m = arg.match(/^-D([^=]+)=/);
149
- if (m && confCacheKeys.has(m[1])) {
150
- throw new Error(
151
- `recipe collision: "${m[1]}" is set in both ` +
152
- `build.configure (as ${JSON.stringify(arg)}) and ` +
153
- `conf.cmake.cache. Pick one route — see claude/PLAN-CONF.md §4.7.`,
154
- );
155
- }
156
- }
157
- }
158
- }
159
-
160
- return {
161
- system,
162
- generators,
163
- configs,
164
- configure,
165
- buildArgs,
166
- installDir: b.installDir ?? DEFAULT_RECIPE.installDir,
167
- sourceSubdir: b.sourceSubdir ?? DEFAULT_RECIPE.sourceSubdir,
168
- requiredTools: cleanArray(b.requiredTools),
169
- };
170
- }
171
-
172
- /**
173
- * Returns true if a manifest has enough info for source-build — i.e. the
174
- * `build` section is present (even empty — defaults cover the rest).
175
- * @param {object} manifest
176
- * @returns {boolean}
177
- */
178
- function hasBuildRecipe(manifest) {
179
- return !!manifest && typeof manifest.build === 'object' && manifest.build !== null;
180
- }
181
-
182
- module.exports = {
183
- normalizeRecipe,
184
- hasBuildRecipe,
185
- DEFAULT_RECIPE,
186
- SUPPORTED_SYSTEMS,
187
- VALID_CMAKE_CONFIGS,
188
- };
1
+ 'use strict';
2
+
3
+ const { substituteTemplateAll } = require('../options');
4
+ const { readRecipeConf } = require('../conf');
5
+
6
+ /**
7
+ * The `build` section of a source-repo `wyvrn.json` tells wyvrnpm how to
8
+ * build the package from source when `--build=missing` fires. Only
9
+ * `system: "cmake"` is supported in phase 3.
10
+ *
11
+ * Example — single generator (strict; CMake errors if Ninja isn't installed):
12
+ * "build": {
13
+ * "system": "cmake",
14
+ * "generator": "Ninja",
15
+ * "configure": ["-DZLIB_BUILD_EXAMPLES=OFF"],
16
+ * "buildArgs": ["--parallel"],
17
+ * "installDir": "install",
18
+ * "sourceSubdir": "."
19
+ * }
20
+ *
21
+ * Example — generator fallback chain (tried in order, first available wins;
22
+ * falls through to CMake's default if none are present):
23
+ * "build": {
24
+ * "generator": ["Ninja", "Visual Studio 17 2022", "Visual Studio 16 2019"]
25
+ * }
26
+ */
27
+
28
+ const DEFAULT_RECIPE = Object.freeze({
29
+ system: 'cmake',
30
+ // Null / empty array → let CMake pick its default
31
+ // (Visual Studio on Windows with VS installed, Unix Makefiles on Linux/macOS).
32
+ generators: Object.freeze([]),
33
+ // All four CMake configurations are built and installed by default so
34
+ // downstream consumers can link against whichever they need. Restrict via
35
+ // the recipe (e.g. `"configs": ["Release"]`) to cut source-build time.
36
+ configs: Object.freeze(['Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel']),
37
+ configure: Object.freeze([]),
38
+ buildArgs: Object.freeze([]),
39
+ installDir: 'install', // relative to binary dir; becomes CMAKE_INSTALL_PREFIX
40
+ sourceSubdir: '.', // where CMakeLists.txt lives relative to the clone root
41
+ requiredTools: Object.freeze([]),
42
+ });
43
+
44
+ const VALID_CMAKE_CONFIGS = new Set(['Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel']);
45
+
46
+ const SUPPORTED_SYSTEMS = new Set(['cmake']);
47
+
48
+ /**
49
+ * Normalise the `build` section of a manifest into a concrete, complete
50
+ * recipe. Missing fields fall back to `DEFAULT_RECIPE`.
51
+ *
52
+ * `generator` accepts either a string (strict — single generator) or an
53
+ * array of strings (fallback chain — tried in order; first available wins).
54
+ * Both forms produce a `generators: string[]` field in the normalised recipe.
55
+ *
56
+ * When `effectiveOptions` is supplied, `${options.<name>}` tokens inside
57
+ * `configure` and `buildArgs` are expanded using those values — bool → ON/OFF,
58
+ * strings verbatim. An unknown option in a template is a hard error (fail
59
+ * fast so the bad arg never reaches cmake). See src/options.js
60
+ * `substituteTemplate`. When `effectiveOptions` is null (recipe declares
61
+ * no options), any `${options.*}` token in the recipe also fails fast —
62
+ * recipes don't get to reference undeclared options.
63
+ *
64
+ * When `manifestForConfCollisionCheck` is supplied, PLAN-CONF.md §4.7's
65
+ * collision detection runs: any `-DKEY=...` in `build.configure` whose
66
+ * KEY is ALSO declared in recipe-level `conf.cmake.cache` is a hard
67
+ * error. Recipes pick ONE route, avoiding silent duplication.
68
+ *
69
+ * @param {object|null|undefined} rawBuild
70
+ * @param {Record<string, any>|null} [effectiveOptions]
71
+ * @param {object|null} [manifestForConfCollisionCheck]
72
+ * @returns {{ system: string, generators: string[], configure: string[],
73
+ * buildArgs: string[], installDir: string, sourceSubdir: string,
74
+ * requiredTools: string[] }}
75
+ * @throws {Error} when `system` is set but not supported, when a
76
+ * `${options.<name>}` template references an undeclared option,
77
+ * or when `build.configure` collides with `conf.cmake.cache`.
78
+ */
79
+ function normalizeRecipe(rawBuild, effectiveOptions = null, manifestForConfCollisionCheck = null) {
80
+ const b = rawBuild ?? {};
81
+ const system = b.system ?? DEFAULT_RECIPE.system;
82
+ if (!SUPPORTED_SYSTEMS.has(system)) {
83
+ throw new Error(
84
+ `build.system "${system}" is not supported. ` +
85
+ `Supported: ${[...SUPPORTED_SYSTEMS].join(', ')}.`,
86
+ );
87
+ }
88
+
89
+ // Accept generator as string | string[] | null for user convenience;
90
+ // canonicalise to an array.
91
+ let generators;
92
+ if (typeof b.generator === 'string') {
93
+ generators = b.generator.trim() ? [b.generator] : [];
94
+ } else if (Array.isArray(b.generator)) {
95
+ generators = b.generator.filter((g) => typeof g === 'string' && g.trim());
96
+ } else {
97
+ generators = [];
98
+ }
99
+
100
+ // Drop empty-string entries so a stray `""` in the user's JSON doesn't
101
+ // become an empty-string argument to CMake / the build tool.
102
+ const cleanArray = (arr) =>
103
+ Array.isArray(arr)
104
+ ? arr.filter((s) => typeof s === 'string' && s.trim().length > 0)
105
+ : [];
106
+
107
+ // Accept `configs` as string | string[]. Default to all four canonical
108
+ // CMake configurations. Unknown entries are silently dropped so a typo
109
+ // doesn't blow up the build loop.
110
+ let configs;
111
+ if (typeof b.configs === 'string') {
112
+ configs = [b.configs];
113
+ } else if (Array.isArray(b.configs)) {
114
+ configs = b.configs.filter((c) => typeof c === 'string' && c.trim());
115
+ } else {
116
+ configs = DEFAULT_RECIPE.configs.slice();
117
+ }
118
+ configs = configs.filter((c) => VALID_CMAKE_CONFIGS.has(c));
119
+ if (configs.length === 0) configs = DEFAULT_RECIPE.configs.slice();
120
+
121
+ // Expand ${options.<name>} tokens in args the recipe passes to cmake.
122
+ // Only `configure` and `buildArgs` are user-authored arg lists; the
123
+ // other fields are literals.
124
+ const configure = substituteTemplateAll(
125
+ cleanArray(b.configure),
126
+ effectiveOptions,
127
+ 'build.configure',
128
+ );
129
+ const buildArgs = substituteTemplateAll(
130
+ cleanArray(b.buildArgs),
131
+ effectiveOptions,
132
+ 'build.buildArgs',
133
+ );
134
+
135
+ // PLAN-CONF.md §4.7: recipe-side collision between `build.configure`
136
+ // `-DKEY=...` args and declared `conf.cmake.cache.KEY` is a hard
137
+ // error. Forces the author to pick one route; avoids silent
138
+ // duplication where one route wins opaquely.
139
+ if (manifestForConfCollisionCheck) {
140
+ const recipeConfFlat = readRecipeConf(manifestForConfCollisionCheck);
141
+ const confCacheKeys = new Set(
142
+ Object.keys(recipeConfFlat)
143
+ .filter((k) => k.startsWith('cmake.cache.'))
144
+ .map((k) => k.slice('cmake.cache.'.length)),
145
+ );
146
+ if (confCacheKeys.size > 0) {
147
+ for (const arg of configure) {
148
+ const m = arg.match(/^-D([^=]+)=/);
149
+ if (m && confCacheKeys.has(m[1])) {
150
+ throw new Error(
151
+ `recipe collision: "${m[1]}" is set in both ` +
152
+ `build.configure (as ${JSON.stringify(arg)}) and ` +
153
+ `conf.cmake.cache. Pick one route — see claude/PLAN-CONF.md §4.7.`,
154
+ );
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ return {
161
+ system,
162
+ generators,
163
+ configs,
164
+ configure,
165
+ buildArgs,
166
+ installDir: b.installDir ?? DEFAULT_RECIPE.installDir,
167
+ sourceSubdir: b.sourceSubdir ?? DEFAULT_RECIPE.sourceSubdir,
168
+ requiredTools: cleanArray(b.requiredTools),
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Returns true if a manifest has enough info for source-build — i.e. the
174
+ * `build` section is present (even empty — defaults cover the rest).
175
+ * @param {object} manifest
176
+ * @returns {boolean}
177
+ */
178
+ function hasBuildRecipe(manifest) {
179
+ return !!manifest && typeof manifest.build === 'object' && manifest.build !== null;
180
+ }
181
+
182
+ module.exports = {
183
+ normalizeRecipe,
184
+ hasBuildRecipe,
185
+ DEFAULT_RECIPE,
186
+ SUPPORTED_SYSTEMS,
187
+ VALID_CMAKE_CONFIGS,
188
+ };
@@ -1,141 +1,141 @@
1
- 'use strict';
2
-
3
- const path = require('path');
4
-
5
- const { readManifest, writeManifest } = require('../manifest');
6
- const { resolveLatestVersion } = require('../resolve');
7
- const { readConfig } = require('../config');
8
- const { wyvrnFetch } = require('../http-fetch');
9
- const log = require('../logger');
10
-
11
- /**
12
- * Parse a `<name>[@<version>]` positional into its parts.
13
- *
14
- * `@` inside the version (e.g. tags like `latest`) is allowed — only the
15
- * first `@` is treated as the separator.
16
- *
17
- * @param {string} spec
18
- * @returns {{ name: string, version: string | null }}
19
- */
20
- function parseSpec(spec) {
21
- const at = spec.indexOf('@');
22
- if (at === -1) return { name: spec, version: null };
23
- if (at === 0) throw new Error(`invalid package spec "${spec}" — missing name before "@"`);
24
- const name = spec.slice(0, at);
25
- const version = spec.slice(at + 1);
26
- if (!version) throw new Error(`invalid package spec "${spec}" — missing version after "@"`);
27
- return { name, version };
28
- }
29
-
30
- /**
31
- * Add one or more dependencies to wyvrn.json.
32
- *
33
- * wyvrnpm add <name> → resolves latest, writes "^<latest>"
34
- * wyvrnpm add <name>@<version> → writes "<version>" verbatim (exact or range)
35
- *
36
- * Existing entries are replaced. If the prior entry was an object
37
- * (`{ version, settings, options }`), its settings/options are preserved —
38
- * only `version` is updated.
39
- *
40
- * Does not run install. The user must `wyvrnpm install` afterwards.
41
- *
42
- * @param {object} argv
43
- */
44
- async function add(argv) {
45
- const manifestPath = path.resolve(argv.manifest);
46
- const specs = (argv.packages ?? []).map(parseSpec);
47
-
48
- if (specs.length === 0) {
49
- log.error('no packages given. Usage: wyvrnpm add <name>[@<version>] [...]');
50
- process.exit(1);
51
- }
52
-
53
- // Install sources — only needed when at least one spec omits @version and
54
- // we have to hit `latest.json`.
55
- let packageSources;
56
- if (argv.source && argv.source.length > 0) {
57
- packageSources = argv.source;
58
- } else {
59
- const config = readConfig();
60
- packageSources = config.installSources.map((s) => s.url);
61
- }
62
-
63
- const needsResolve = specs.some((s) => s.version === null);
64
- if (needsResolve && packageSources.length === 0) {
65
- log.error(
66
- 'no install sources configured — cannot look up latest versions.\n' +
67
- ' Pass --source <url>, pin versions explicitly (`name@1.2.3.4`), or run:\n' +
68
- ' wyvrnpm configure add-source --kind install --name ... --url ...',
69
- );
70
- process.exit(1);
71
- }
72
-
73
- const manifest = readManifest(manifestPath);
74
-
75
- // Preserve whichever dependencies key the manifest already uses. If neither
76
- // exists, create lowercase `dependencies` (the v2 convention).
77
- const depsKey =
78
- Object.prototype.hasOwnProperty.call(manifest, 'dependencies') ? 'dependencies' :
79
- Object.prototype.hasOwnProperty.call(manifest, 'Dependencies') ? 'Dependencies' :
80
- 'dependencies';
81
-
82
- let deps = manifest[depsKey];
83
- // Legacy array form → object. Editing by name is the whole point of `add`,
84
- // and round-tripping as an array would silently drop per-dep settings.
85
- if (Array.isArray(deps)) {
86
- const obj = {};
87
- for (const d of deps) {
88
- const n = d.Name ?? d.name;
89
- const v = d.Version ?? d.version;
90
- if (n && v) obj[n] = v;
91
- }
92
- deps = obj;
93
- } else if (!deps || typeof deps !== 'object') {
94
- deps = {};
95
- }
96
-
97
- for (const spec of specs) {
98
- let versionToWrite;
99
- if (spec.version) {
100
- versionToWrite = spec.version;
101
- log.info(`${spec.name}: writing "${versionToWrite}" (as given)`);
102
- } else {
103
- const latest = await resolveLatestVersion(spec.name, packageSources, argv.platform, wyvrnFetch);
104
- versionToWrite = `^${latest}`;
105
- log.info(`${spec.name}: latest is ${latest} → writing "${versionToWrite}"`);
106
- }
107
-
108
- const prior = deps[spec.name];
109
- if (prior !== undefined) {
110
- const priorVer =
111
- typeof prior === 'string' ? prior :
112
- (prior && typeof prior === 'object') ? (prior.version ?? prior.Version ?? '(unknown)') :
113
- '(unknown)';
114
- log.info(` replacing existing entry: "${priorVer}"`);
115
- }
116
-
117
- // If the prior entry was an object with settings/options, keep those —
118
- // only bump the version field. Otherwise write the short-form string.
119
- if (prior && typeof prior === 'object' && !Array.isArray(prior)) {
120
- const updated = { ...prior };
121
- if ('Version' in updated && !('version' in updated)) {
122
- updated.Version = versionToWrite;
123
- } else {
124
- updated.version = versionToWrite;
125
- }
126
- deps[spec.name] = updated;
127
- } else {
128
- deps[spec.name] = versionToWrite;
129
- }
130
- }
131
-
132
- manifest[depsKey] = deps;
133
- writeManifest(manifestPath, manifest);
134
-
135
- const n = specs.length;
136
- log.success(`Updated ${manifestPath} — ${n} package${n !== 1 ? 's' : ''}.`);
137
- log.info('Run `wyvrnpm install` to download.');
138
- }
139
-
140
- module.exports = add;
141
- module.exports.parseSpec = parseSpec;
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const { readManifest, writeManifest } = require('../manifest');
6
+ const { resolveLatestVersion } = require('../resolve');
7
+ const { readConfig } = require('../config');
8
+ const { wyvrnFetch } = require('../http-fetch');
9
+ const log = require('../logger');
10
+
11
+ /**
12
+ * Parse a `<name>[@<version>]` positional into its parts.
13
+ *
14
+ * `@` inside the version (e.g. tags like `latest`) is allowed — only the
15
+ * first `@` is treated as the separator.
16
+ *
17
+ * @param {string} spec
18
+ * @returns {{ name: string, version: string | null }}
19
+ */
20
+ function parseSpec(spec) {
21
+ const at = spec.indexOf('@');
22
+ if (at === -1) return { name: spec, version: null };
23
+ if (at === 0) throw new Error(`invalid package spec "${spec}" — missing name before "@"`);
24
+ const name = spec.slice(0, at);
25
+ const version = spec.slice(at + 1);
26
+ if (!version) throw new Error(`invalid package spec "${spec}" — missing version after "@"`);
27
+ return { name, version };
28
+ }
29
+
30
+ /**
31
+ * Add one or more dependencies to wyvrn.json.
32
+ *
33
+ * wyvrnpm add <name> → resolves latest, writes "^<latest>"
34
+ * wyvrnpm add <name>@<version> → writes "<version>" verbatim (exact or range)
35
+ *
36
+ * Existing entries are replaced. If the prior entry was an object
37
+ * (`{ version, settings, options }`), its settings/options are preserved —
38
+ * only `version` is updated.
39
+ *
40
+ * Does not run install. The user must `wyvrnpm install` afterwards.
41
+ *
42
+ * @param {object} argv
43
+ */
44
+ async function add(argv) {
45
+ const manifestPath = path.resolve(argv.manifest);
46
+ const specs = (argv.packages ?? []).map(parseSpec);
47
+
48
+ if (specs.length === 0) {
49
+ log.error('no packages given. Usage: wyvrnpm add <name>[@<version>] [...]');
50
+ process.exit(1);
51
+ }
52
+
53
+ // Install sources — only needed when at least one spec omits @version and
54
+ // we have to hit `latest.json`.
55
+ let packageSources;
56
+ if (argv.source && argv.source.length > 0) {
57
+ packageSources = argv.source;
58
+ } else {
59
+ const config = readConfig();
60
+ packageSources = config.installSources.map((s) => s.url);
61
+ }
62
+
63
+ const needsResolve = specs.some((s) => s.version === null);
64
+ if (needsResolve && packageSources.length === 0) {
65
+ log.error(
66
+ 'no install sources configured — cannot look up latest versions.\n' +
67
+ ' Pass --source <url>, pin versions explicitly (`name@1.2.3.4`), or run:\n' +
68
+ ' wyvrnpm configure add-source --kind install --name ... --url ...',
69
+ );
70
+ process.exit(1);
71
+ }
72
+
73
+ const manifest = readManifest(manifestPath);
74
+
75
+ // Preserve whichever dependencies key the manifest already uses. If neither
76
+ // exists, create lowercase `dependencies` (the v2 convention).
77
+ const depsKey =
78
+ Object.prototype.hasOwnProperty.call(manifest, 'dependencies') ? 'dependencies' :
79
+ Object.prototype.hasOwnProperty.call(manifest, 'Dependencies') ? 'Dependencies' :
80
+ 'dependencies';
81
+
82
+ let deps = manifest[depsKey];
83
+ // Legacy array form → object. Editing by name is the whole point of `add`,
84
+ // and round-tripping as an array would silently drop per-dep settings.
85
+ if (Array.isArray(deps)) {
86
+ const obj = {};
87
+ for (const d of deps) {
88
+ const n = d.Name ?? d.name;
89
+ const v = d.Version ?? d.version;
90
+ if (n && v) obj[n] = v;
91
+ }
92
+ deps = obj;
93
+ } else if (!deps || typeof deps !== 'object') {
94
+ deps = {};
95
+ }
96
+
97
+ for (const spec of specs) {
98
+ let versionToWrite;
99
+ if (spec.version) {
100
+ versionToWrite = spec.version;
101
+ log.info(`${spec.name}: writing "${versionToWrite}" (as given)`);
102
+ } else {
103
+ const latest = await resolveLatestVersion(spec.name, packageSources, argv.platform, wyvrnFetch);
104
+ versionToWrite = `^${latest}`;
105
+ log.info(`${spec.name}: latest is ${latest} → writing "${versionToWrite}"`);
106
+ }
107
+
108
+ const prior = deps[spec.name];
109
+ if (prior !== undefined) {
110
+ const priorVer =
111
+ typeof prior === 'string' ? prior :
112
+ (prior && typeof prior === 'object') ? (prior.version ?? prior.Version ?? '(unknown)') :
113
+ '(unknown)';
114
+ log.info(` replacing existing entry: "${priorVer}"`);
115
+ }
116
+
117
+ // If the prior entry was an object with settings/options, keep those —
118
+ // only bump the version field. Otherwise write the short-form string.
119
+ if (prior && typeof prior === 'object' && !Array.isArray(prior)) {
120
+ const updated = { ...prior };
121
+ if ('Version' in updated && !('version' in updated)) {
122
+ updated.Version = versionToWrite;
123
+ } else {
124
+ updated.version = versionToWrite;
125
+ }
126
+ deps[spec.name] = updated;
127
+ } else {
128
+ deps[spec.name] = versionToWrite;
129
+ }
130
+ }
131
+
132
+ manifest[depsKey] = deps;
133
+ writeManifest(manifestPath, manifest);
134
+
135
+ const n = specs.length;
136
+ log.success(`Updated ${manifestPath} — ${n} package${n !== 1 ? 's' : ''}.`);
137
+ log.info('Run `wyvrnpm install` to download.');
138
+ }
139
+
140
+ module.exports = add;
141
+ module.exports.parseSpec = parseSpec;