wyvrnpm 2.0.4 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +639 -5
- package/bin/wyvrn.js +220 -10
- package/claude/skills/wyvrnpm.skill +0 -0
- package/cmake/cpp.cmake +9 -0
- package/cmake/functions.cmake +224 -0
- package/cmake/macros.cmake +233 -0
- package/cmake/options.cmake +23 -0
- package/cmake/variables.cmake +171 -0
- package/package.json +3 -1
- package/src/build/cache.js +148 -0
- package/src/build/clone.js +170 -0
- package/src/build/cmake.js +342 -0
- package/src/build/index.js +275 -0
- package/src/build/msvc-env.js +217 -0
- package/src/build/recipe.js +155 -0
- package/src/commands/build.js +283 -0
- package/src/commands/clean.js +56 -16
- package/src/commands/configure.js +6 -5
- package/src/commands/init.js +3 -2
- package/src/commands/install-skill.js +107 -0
- package/src/commands/install.js +262 -19
- package/src/commands/link.js +18 -15
- package/src/commands/profile.js +15 -12
- package/src/commands/publish.js +216 -65
- package/src/commands/show.js +237 -0
- package/src/compat.js +261 -0
- package/src/config.js +3 -1
- package/src/download.js +431 -87
- package/src/ignore.js +118 -0
- package/src/logger.js +94 -0
- package/src/manifest.js +12 -7
- package/src/options.js +303 -0
- package/src/profile.js +56 -4
- package/src/providers/base.js +16 -1
- package/src/providers/file.js +12 -6
- package/src/providers/http.js +15 -10
- package/src/providers/s3.js +14 -9
- package/src/resolve.js +179 -19
- package/src/toolchain/deps.js +164 -0
- package/src/toolchain/index.js +141 -0
- package/src/toolchain/presets.js +263 -0
- package/src/toolchain/template.cmake +66 -0
- package/src/upload-built.js +256 -0
- package/src/version-range.js +301 -0
package/src/commands/publish.js
CHANGED
|
@@ -8,44 +8,39 @@ const AdmZip = require('adm-zip');
|
|
|
8
8
|
const { readManifest } = require('../manifest');
|
|
9
9
|
const { getProvider } = require('../providers');
|
|
10
10
|
const { readConfig } = require('../config');
|
|
11
|
-
const { loadProfile, hashProfile, sha256Of, getGitSha, getGitRepo } = require('../profile');
|
|
11
|
+
const { loadProfile, hashProfile, sha256Of, getGitSha, getGitRepo, isShaOnRemote } = require('../profile');
|
|
12
|
+
const { defaultCompatBlock } = require('../compat');
|
|
13
|
+
const {
|
|
14
|
+
normalizeOptionsDeclaration,
|
|
15
|
+
resolveEffectiveOptions,
|
|
16
|
+
parseCliOptions,
|
|
17
|
+
} = require('../options');
|
|
18
|
+
const { loadIgnorePatterns, isIgnored } = require('../ignore');
|
|
19
|
+
const { hasBuildRecipe, normalizeRecipe } = require('../build/recipe');
|
|
20
|
+
const log = require('../logger');
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.split(/\r?\n/)
|
|
22
|
-
.map((l) => l.trim())
|
|
23
|
-
.filter((l) => l.length > 0 && !l.startsWith('#'));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function patternToRegex(pattern) {
|
|
27
|
-
const anchored = pattern.startsWith('/');
|
|
28
|
-
const p = anchored ? pattern.slice(1) : pattern;
|
|
29
|
-
let re = p
|
|
30
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
31
|
-
.replace(/\*\*/g, '\x00')
|
|
32
|
-
.replace(/\*/g, '[^/]*')
|
|
33
|
-
.replace(/\?/g, '[^/]')
|
|
34
|
-
.replace(/\x00/g, '.*');
|
|
35
|
-
return anchored ? new RegExp(`^${re}(/|$)`) : new RegExp(`(^|/)${re}(/|$)`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function addFolderFiltered(zip, dir, base, ignoreRe) {
|
|
22
|
+
/**
|
|
23
|
+
* Walk `dir` recursively and add every file to `zip`, skipping anything that
|
|
24
|
+
* matches `.wyvrnignore` patterns OR whose zip-relative path is already in
|
|
25
|
+
* `reservedRelPaths`. The reserved set lets the install-tree overlay win
|
|
26
|
+
* on path collisions (e.g. `include/foo.h` shipped by both source and
|
|
27
|
+
* install trees).
|
|
28
|
+
*/
|
|
29
|
+
function addFolderFiltered(zip, dir, base, patterns, reservedRelPaths = new Set()) {
|
|
39
30
|
for (const entry of fs.readdirSync(dir)) {
|
|
40
31
|
const fullPath = path.join(dir, entry);
|
|
41
32
|
const relPath = path.relative(base, fullPath).replace(/\\/g, '/');
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
const stat = fs.statSync(fullPath);
|
|
34
|
+
const isDir = stat.isDirectory();
|
|
35
|
+
if (isIgnored(relPath, isDir, patterns)) {
|
|
36
|
+
log.info(` Ignoring: ${relPath}${isDir ? '/' : ''}`);
|
|
44
37
|
continue;
|
|
45
38
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
if (isDir) {
|
|
40
|
+
addFolderFiltered(zip, fullPath, base, patterns, reservedRelPaths);
|
|
41
|
+
} else if (reservedRelPaths.has(relPath)) {
|
|
42
|
+
// Install tree has a canonical version of this file; let that win.
|
|
43
|
+
log.info(` Install-tree wins: ${relPath}`);
|
|
49
44
|
} else {
|
|
50
45
|
const zipDir = path.dirname(relPath).replace(/\\/g, '/');
|
|
51
46
|
zip.addLocalFile(fullPath, zipDir === '.' ? '' : zipDir);
|
|
@@ -53,6 +48,42 @@ function addFolderFiltered(zip, dir, base, ignoreRe) {
|
|
|
53
48
|
}
|
|
54
49
|
}
|
|
55
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Collect every file under `dir` as a `{ fullPath, relPath }` pair. `relPath`
|
|
53
|
+
* is forward-slash-separated and relative to `dir` — so the install tree's
|
|
54
|
+
* `install/cmake/FooConfig.cmake` lands as `cmake/FooConfig.cmake` at the
|
|
55
|
+
* zip root, which is where `find_package(Foo CONFIG)` expects it on the
|
|
56
|
+
* consumer side after extraction into `wyvrn_internal/<name>/`.
|
|
57
|
+
*/
|
|
58
|
+
function collectInstallTreeFiles(dir) {
|
|
59
|
+
const files = [];
|
|
60
|
+
function walk(current) {
|
|
61
|
+
for (const entry of fs.readdirSync(current)) {
|
|
62
|
+
const full = path.join(current, entry);
|
|
63
|
+
const stat = fs.statSync(full);
|
|
64
|
+
if (stat.isDirectory()) {
|
|
65
|
+
walk(full);
|
|
66
|
+
} else {
|
|
67
|
+
const rel = path.relative(dir, full).replace(/\\/g, '/');
|
|
68
|
+
files.push({ fullPath: full, relPath: rel });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
walk(dir);
|
|
73
|
+
return files;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function addInstallTree(zip, installDir) {
|
|
77
|
+
const files = collectInstallTreeFiles(installDir);
|
|
78
|
+
const added = new Set();
|
|
79
|
+
for (const { fullPath, relPath } of files) {
|
|
80
|
+
const zipDir = path.dirname(relPath).replace(/\\/g, '/');
|
|
81
|
+
zip.addLocalFile(fullPath, zipDir === '.' ? '' : zipDir);
|
|
82
|
+
added.add(relPath);
|
|
83
|
+
}
|
|
84
|
+
return added;
|
|
85
|
+
}
|
|
86
|
+
|
|
56
87
|
// ---------------------------------------------------------------------------
|
|
57
88
|
// Source resolution
|
|
58
89
|
// ---------------------------------------------------------------------------
|
|
@@ -64,21 +95,21 @@ function resolveSource(argv) {
|
|
|
64
95
|
if (source) {
|
|
65
96
|
const named = config.publishSources.find((s) => s.name === source);
|
|
66
97
|
if (named) {
|
|
67
|
-
|
|
98
|
+
log.info(`Using publish source "${named.name}" from config`);
|
|
68
99
|
source = named.url;
|
|
69
100
|
awsProfile = awsProfile ?? named.profile;
|
|
70
101
|
token = token ?? named.token;
|
|
71
102
|
}
|
|
72
103
|
} else {
|
|
73
104
|
if (config.publishSources.length === 0) {
|
|
74
|
-
|
|
75
|
-
'
|
|
105
|
+
log.error(
|
|
106
|
+
'--source is required (or configure one with: ' +
|
|
76
107
|
'wyvrnpm configure add-source --kind publish ...)',
|
|
77
108
|
);
|
|
78
109
|
process.exit(1);
|
|
79
110
|
}
|
|
80
111
|
const first = config.publishSources[0];
|
|
81
|
-
|
|
112
|
+
log.info(`Using publish source "${first.name}" from config`);
|
|
82
113
|
source = first.url;
|
|
83
114
|
awsProfile = awsProfile ?? first.profile;
|
|
84
115
|
token = token ?? first.token;
|
|
@@ -99,24 +130,26 @@ async function publish(argv) {
|
|
|
99
130
|
force,
|
|
100
131
|
manifest: manifestArg,
|
|
101
132
|
} = argv;
|
|
133
|
+
const jsonOut = argv.format === 'json';
|
|
134
|
+
if (jsonOut) log.setJsonMode(true);
|
|
102
135
|
|
|
103
136
|
// ── Load manifest ─────────────────────────────────────────────────────────
|
|
104
137
|
const manifestPath = path.resolve(manifestArg || './wyvrn.json');
|
|
105
138
|
const manifest = await readManifest(manifestPath);
|
|
106
139
|
if (!manifest) {
|
|
107
|
-
|
|
140
|
+
log.error(`Could not read manifest at ${manifestPath}`);
|
|
108
141
|
process.exit(1);
|
|
109
142
|
}
|
|
110
143
|
|
|
111
144
|
const { name, version } = manifest;
|
|
112
145
|
if (!name || !version) {
|
|
113
|
-
|
|
146
|
+
log.error('Manifest must have "name" and "version" fields');
|
|
114
147
|
process.exit(1);
|
|
115
148
|
}
|
|
116
149
|
|
|
117
150
|
const srcDir = path.resolve(publishPath || '.');
|
|
118
151
|
if (!fs.existsSync(srcDir)) {
|
|
119
|
-
|
|
152
|
+
log.error(`Publish path does not exist: ${srcDir}`);
|
|
120
153
|
process.exit(1);
|
|
121
154
|
}
|
|
122
155
|
|
|
@@ -125,7 +158,7 @@ async function publish(argv) {
|
|
|
125
158
|
try {
|
|
126
159
|
provider = getProvider(source);
|
|
127
160
|
} catch (err) {
|
|
128
|
-
|
|
161
|
+
log.error(err.message);
|
|
129
162
|
process.exit(1);
|
|
130
163
|
}
|
|
131
164
|
|
|
@@ -133,27 +166,44 @@ async function publish(argv) {
|
|
|
133
166
|
const config = readConfig();
|
|
134
167
|
const activeProfileName = profileName ?? config.defaultProfile ?? 'default';
|
|
135
168
|
const { profile: buildProfile, fromFile } = loadProfile(activeProfileName);
|
|
136
|
-
const profileHash = hashProfile(buildProfile);
|
|
137
169
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
170
|
+
// ── Resolve this package's own declared options ─────────────────────────
|
|
171
|
+
// Publishing needs to commit to a specific value for every option the
|
|
172
|
+
// recipe declares. Sources for overrides: CLI `-o <pkg>:...` flags
|
|
173
|
+
// (scoped to the package being published), else the recipe's defaults.
|
|
174
|
+
const ownDeclaration = manifest.options
|
|
175
|
+
? normalizeOptionsDeclaration(manifest.options, `${name}@${version}`)
|
|
176
|
+
: null;
|
|
177
|
+
const cliOptionsByPkg = parseCliOptions(argv.option);
|
|
178
|
+
const effectiveOptions = resolveEffectiveOptions(
|
|
179
|
+
ownDeclaration,
|
|
180
|
+
{}, // consumer-side overrides don't apply to self-publish
|
|
181
|
+
cliOptionsByPkg[name] ?? {},
|
|
182
|
+
`${name}@${version}`,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const profileHash = hashProfile(buildProfile, effectiveOptions);
|
|
186
|
+
|
|
187
|
+
log.info(
|
|
188
|
+
`Publishing ${name}@${version}\n` +
|
|
189
|
+
`Profile : "${activeProfileName}"${fromFile ? '' : ' (auto-detected — save with `wyvrnpm configure profile detect`)'}\n` +
|
|
190
|
+
` ${buildProfile.os}/${buildProfile.arch} ` +
|
|
142
191
|
`${buildProfile.compiler} ${buildProfile['compiler.version']} ` +
|
|
143
192
|
`C++${buildProfile['compiler.cppstd']}` +
|
|
144
193
|
(buildProfile['compiler.runtime'] ? ` [${buildProfile['compiler.runtime']}]` : '') + '\n' +
|
|
145
|
-
|
|
146
|
-
`
|
|
194
|
+
(effectiveOptions ? `Options : ${JSON.stringify(effectiveOptions)}\n` : '') +
|
|
195
|
+
`Profile hash: ${profileHash}\n` +
|
|
196
|
+
`Provider : ${provider.constructor.providerName}`,
|
|
147
197
|
);
|
|
148
198
|
|
|
149
199
|
// ── Check v2 existence ────────────────────────────────────────────────────
|
|
150
200
|
const v2AlreadyExists = await provider.v2Exists({ source, name, version, profileHash, awsProfile, token });
|
|
151
201
|
if (v2AlreadyExists) {
|
|
152
202
|
if (force) {
|
|
153
|
-
|
|
203
|
+
log.warn(`${name}@${version} [${profileHash}] already exists — overwriting (--force)`);
|
|
154
204
|
} else {
|
|
155
|
-
|
|
156
|
-
|
|
205
|
+
log.error(
|
|
206
|
+
`${name}@${version} [${profileHash}] already exists.\n` +
|
|
157
207
|
' Bump the version, use a different profile, or pass --force to overwrite.',
|
|
158
208
|
);
|
|
159
209
|
process.exit(1);
|
|
@@ -170,50 +220,129 @@ async function publish(argv) {
|
|
|
170
220
|
const ver = typeof entry === 'string' ? entry : entry.version;
|
|
171
221
|
if (ver) lockedDependencies[pkgName] = ver;
|
|
172
222
|
}
|
|
173
|
-
|
|
223
|
+
log.info(`Lock file : ${Object.keys(lockedDependencies).length} pinned package(s)`);
|
|
174
224
|
} catch {
|
|
175
|
-
|
|
225
|
+
log.warn('could not read wyvrn.lock — dependency versions will be published as-is from wyvrn.json');
|
|
176
226
|
}
|
|
177
227
|
} else {
|
|
178
|
-
|
|
228
|
+
log.warn('no wyvrn.lock found — run `wyvrnpm install` before publishing to pin dependency versions');
|
|
179
229
|
}
|
|
180
230
|
|
|
181
231
|
// ── Git metadata ──────────────────────────────────────────────────────────
|
|
182
232
|
const gitSha = getGitSha(srcDir);
|
|
183
233
|
const gitRepo = getGitRepo(srcDir);
|
|
184
|
-
if (gitSha)
|
|
185
|
-
if (gitRepo)
|
|
234
|
+
if (gitSha) log.info(`Git commit : ${gitSha}`);
|
|
235
|
+
if (gitRepo) log.info(`Git remote : ${gitRepo}`);
|
|
236
|
+
|
|
237
|
+
// Warn (but don't block) if HEAD isn't reachable from origin. Downstream
|
|
238
|
+
// `--build=missing` consumers will fail to fetch this commit — the fix
|
|
239
|
+
// is almost always `git push` before publishing.
|
|
240
|
+
if (gitSha && gitRepo && !isShaOnRemote(gitSha, srcDir)) {
|
|
241
|
+
log.warn(
|
|
242
|
+
`commit ${gitSha.slice(0, 12)} is not reachable from any remote ` +
|
|
243
|
+
`branch or tag on origin.\n` +
|
|
244
|
+
` Consumers building this package from source will fail to retrieve it.\n` +
|
|
245
|
+
` Fix: push your branch (or tag the commit) before publishing.`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
186
248
|
|
|
187
249
|
// ── .wyvrnignore ──────────────────────────────────────────────────────────
|
|
188
250
|
const ignoreFile = path.join(srcDir, '.wyvrnignore');
|
|
189
251
|
const ignorePatterns = loadIgnorePatterns(ignoreFile);
|
|
190
|
-
const ignoreRe = ignorePatterns.map(patternToRegex);
|
|
191
252
|
if (ignorePatterns.length > 0) {
|
|
192
|
-
|
|
253
|
+
log.info(`Loaded ${ignorePatterns.length} pattern(s) from .wyvrnignore`);
|
|
193
254
|
}
|
|
194
255
|
|
|
195
256
|
// ── Build zip ─────────────────────────────────────────────────────────────
|
|
196
257
|
const tmpZipPath = path.join(os.tmpdir(), `wyvrnpm-${name}-${version}-${Date.now()}.zip`);
|
|
258
|
+
|
|
259
|
+
// ── Inject default `compatibility` block if the source manifest lacks one ──
|
|
260
|
+
// Written to a temp file so the source wyvrn.json is never modified.
|
|
261
|
+
// A published manifest must always carry a compat block so install-time
|
|
262
|
+
// resolution has something to evaluate (default = all fields `exact`,
|
|
263
|
+
// identical semantics to pre-phase-2 strict behaviour).
|
|
264
|
+
const tmpManifestPath = path.join(
|
|
265
|
+
os.tmpdir(),
|
|
266
|
+
`wyvrnpm-manifest-${name}-${version}-${Date.now()}.json`,
|
|
267
|
+
);
|
|
268
|
+
const manifestForPublish = {
|
|
269
|
+
...manifest,
|
|
270
|
+
compatibility: manifest.compatibility ?? defaultCompatBlock(),
|
|
271
|
+
};
|
|
272
|
+
fs.writeFileSync(tmpManifestPath, JSON.stringify(manifestForPublish, null, 2), 'utf8');
|
|
273
|
+
if (!manifest.compatibility) {
|
|
274
|
+
log.info('Injecting default `compatibility` block (all fields `exact`) — override by adding one to wyvrn.json');
|
|
275
|
+
} else {
|
|
276
|
+
log.info(`Using package-declared compatibility: ${JSON.stringify(manifest.compatibility)}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Install-tree overlay ──────────────────────────────────────────────────
|
|
280
|
+
// Publish zips the source tree (filtered by .wyvrnignore), but consumers
|
|
281
|
+
// actually need the install tree — `lib/cmake/<Name>/<Name>Config.cmake`,
|
|
282
|
+
// the installed headers, compiled libs — so find_package() works after
|
|
283
|
+
// extraction. When a build recipe is declared, locate the per-profile
|
|
284
|
+
// install dir (populated by `wyvrnpm build --config <cfg> --install`) and
|
|
285
|
+
// overlay its contents on top of the source zip. Install-tree files win
|
|
286
|
+
// on path collisions (e.g. `include/`).
|
|
287
|
+
let installOverlayDir = null;
|
|
288
|
+
let installOverlayPaths = new Set();
|
|
289
|
+
if (hasBuildRecipe(manifest)) {
|
|
290
|
+
try {
|
|
291
|
+
const recipe = normalizeRecipe(manifest.build, effectiveOptions);
|
|
292
|
+
const candidate = path.join(
|
|
293
|
+
srcDir, 'build', `wyvrn-${activeProfileName}`, recipe.installDir,
|
|
294
|
+
);
|
|
295
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
296
|
+
installOverlayDir = candidate;
|
|
297
|
+
} else {
|
|
298
|
+
log.warn(
|
|
299
|
+
`build recipe is declared but no install tree found at ${candidate}.\n` +
|
|
300
|
+
' Consumers\' find_package() will likely fail. Fix: run\n' +
|
|
301
|
+
' wyvrnpm build --config Debug --install\n' +
|
|
302
|
+
' wyvrnpm build --config Release --install\n' +
|
|
303
|
+
' wyvrnpm build --config RelWithDebInfo --install\n' +
|
|
304
|
+
' wyvrnpm build --config MinSizeRel --install\n' +
|
|
305
|
+
' then re-run publish.',
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
} catch (err) {
|
|
309
|
+
log.warn(`could not locate install tree: ${err.message} — publishing source tree only`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
197
313
|
try {
|
|
198
|
-
|
|
314
|
+
log.info(`Zipping ${srcDir} ...`);
|
|
199
315
|
const zip = new AdmZip();
|
|
200
|
-
|
|
316
|
+
if (installOverlayDir) {
|
|
317
|
+
// Collect install-tree paths first so the source-tree walker can skip
|
|
318
|
+
// anything the install tree will add canonically.
|
|
319
|
+
const peek = collectInstallTreeFiles(installOverlayDir);
|
|
320
|
+
installOverlayPaths = new Set(peek.map((f) => f.relPath));
|
|
321
|
+
}
|
|
322
|
+
addFolderFiltered(zip, srcDir, srcDir, ignorePatterns, installOverlayPaths);
|
|
323
|
+
if (installOverlayDir) {
|
|
324
|
+
log.info(`Overlaying install tree: ${installOverlayDir}`);
|
|
325
|
+
const added = addInstallTree(zip, installOverlayDir);
|
|
326
|
+
log.info(` Added ${added.size} file(s) from install tree`);
|
|
327
|
+
}
|
|
201
328
|
zip.writeZip(tmpZipPath);
|
|
202
329
|
|
|
203
330
|
const contentSha256 = sha256Of(tmpZipPath);
|
|
204
|
-
|
|
331
|
+
log.info(`Content SHA256 : ${contentSha256}`);
|
|
205
332
|
|
|
206
333
|
// ── v2 publish ────────────────────────────────────────────────────────
|
|
207
|
-
|
|
334
|
+
log.info('Publishing (v2) ...');
|
|
208
335
|
await provider.v2Publish(
|
|
209
|
-
{ manifest:
|
|
336
|
+
{ manifest: tmpManifestPath, zip: tmpZipPath },
|
|
210
337
|
{
|
|
211
338
|
source,
|
|
212
339
|
name,
|
|
213
340
|
version,
|
|
214
341
|
profileHash,
|
|
215
342
|
// buildSettings stored in metadata for debugging — NOT wyvrn.json
|
|
216
|
-
buildSettings:
|
|
343
|
+
buildSettings: effectiveOptions
|
|
344
|
+
? { ...buildProfile, options: effectiveOptions }
|
|
345
|
+
: buildProfile,
|
|
217
346
|
contentSha256,
|
|
218
347
|
gitSha,
|
|
219
348
|
gitRepo,
|
|
@@ -223,10 +352,32 @@ async function publish(argv) {
|
|
|
223
352
|
},
|
|
224
353
|
);
|
|
225
354
|
|
|
226
|
-
|
|
355
|
+
log.success(`Successfully published ${name}@${version} [${profileHash}] to ${source}`);
|
|
356
|
+
|
|
357
|
+
if (jsonOut) {
|
|
358
|
+
const payload = {
|
|
359
|
+
command: 'publish',
|
|
360
|
+
wyvrnpmVersion: require('../../package.json').version,
|
|
361
|
+
name,
|
|
362
|
+
version,
|
|
363
|
+
profileHash,
|
|
364
|
+
source,
|
|
365
|
+
contentSha256,
|
|
366
|
+
options: effectiveOptions ?? null,
|
|
367
|
+
gitSha: gitSha ?? null,
|
|
368
|
+
gitRepo: gitRepo ?? null,
|
|
369
|
+
publishedAt: new Date().toISOString(),
|
|
370
|
+
};
|
|
371
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
372
|
+
}
|
|
227
373
|
} finally {
|
|
228
|
-
if (fs.existsSync(tmpZipPath))
|
|
374
|
+
if (fs.existsSync(tmpZipPath)) fs.unlinkSync(tmpZipPath);
|
|
375
|
+
if (fs.existsSync(tmpManifestPath)) fs.unlinkSync(tmpManifestPath);
|
|
229
376
|
}
|
|
230
377
|
}
|
|
231
378
|
|
|
232
379
|
module.exports = publish;
|
|
380
|
+
// Exported for tests only
|
|
381
|
+
module.exports.addFolderFiltered = addFolderFiltered;
|
|
382
|
+
module.exports.addInstallTree = addInstallTree;
|
|
383
|
+
module.exports.collectInstallTreeFiles = collectInstallTreeFiles;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { readConfig } = require('../config');
|
|
4
|
+
const { getProvider } = require('../providers');
|
|
5
|
+
const log = require('../logger');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `wyvrnpm show <pkg>` — surface registry metadata for a package.
|
|
9
|
+
*
|
|
10
|
+
* Accepts:
|
|
11
|
+
* name → list every published version + profile.
|
|
12
|
+
* name@version → list every profile for that version,
|
|
13
|
+
* fetch full metadata for each, highlight
|
|
14
|
+
* consumer (`uploadedBy`) vs author builds.
|
|
15
|
+
* name@version@profileHash → dump full metadata for one build.
|
|
16
|
+
*
|
|
17
|
+
* Sources: CLI `--source` overrides config.installSources. First source
|
|
18
|
+
* that returns data wins (same first-match policy as `install`).
|
|
19
|
+
*
|
|
20
|
+
* Added for PLAN-UPLOAD-BUILT phase 3 so maintainers can spot-check which
|
|
21
|
+
* profileHashes were uploaded by consumers vs published by an author. The
|
|
22
|
+
* `uploadedBy` block is the signal; its absence means "author publish"
|
|
23
|
+
* by convention.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} argv
|
|
26
|
+
* @param {string} argv.pkg e.g. "zlib", "zlib@1.3.0.0", "zlib@1.3.0.0@a1b2..."
|
|
27
|
+
* @param {string[]} [argv.source] source URLs
|
|
28
|
+
* @param {string} [argv.awsProfile]
|
|
29
|
+
* @param {string} [argv.token]
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
async function show(argv) {
|
|
33
|
+
const { name, version, profileHash } = parseSpec(argv.pkg);
|
|
34
|
+
if (!name) {
|
|
35
|
+
log.error('Usage: wyvrnpm show <name>[@<version>[@<profileHash>]]');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const jsonOut = argv.format === 'json';
|
|
40
|
+
if (jsonOut) log.setJsonMode(true);
|
|
41
|
+
|
|
42
|
+
const { sources, awsProfile, token } = resolveSources(argv);
|
|
43
|
+
if (sources.length === 0) {
|
|
44
|
+
log.error(
|
|
45
|
+
'no sources configured. ' +
|
|
46
|
+
'Pass --source <url> or run: wyvrnpm configure add-source --kind install ...',
|
|
47
|
+
);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const source of sources) {
|
|
52
|
+
let provider;
|
|
53
|
+
try { provider = getProvider(source); } catch { continue; }
|
|
54
|
+
|
|
55
|
+
const idx = await provider.v2GetVersionsIndex({ source, name, awsProfile, token });
|
|
56
|
+
if (!idx?.versions) continue;
|
|
57
|
+
|
|
58
|
+
const versionKeys = version
|
|
59
|
+
? (idx.versions[version] ? [version] : [])
|
|
60
|
+
: Object.keys(idx.versions).sort();
|
|
61
|
+
|
|
62
|
+
if (version && versionKeys.length === 0) {
|
|
63
|
+
log.error(`Version ${version} not found on ${source}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Collect a single structured result as we walk. Text rendering happens
|
|
68
|
+
// inline in the legacy path; JSON mode emits the whole object at the
|
|
69
|
+
// end of the first source that answered.
|
|
70
|
+
const result = {
|
|
71
|
+
command: 'show',
|
|
72
|
+
wyvrnpmVersion: require('../../package.json').version,
|
|
73
|
+
source,
|
|
74
|
+
package: name,
|
|
75
|
+
latest: idx.latest ?? null,
|
|
76
|
+
versions: {},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (!jsonOut) {
|
|
80
|
+
console.log(`${name} (source: ${source})`);
|
|
81
|
+
if (idx.latest) console.log(` latest: ${idx.latest}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const v of versionKeys) {
|
|
85
|
+
const ventry = idx.versions[v];
|
|
86
|
+
const profiles = ventry?.profiles ?? {};
|
|
87
|
+
const profileKeys = profileHash
|
|
88
|
+
? (profiles[profileHash] ? [profileHash] : [])
|
|
89
|
+
: Object.keys(profiles).sort();
|
|
90
|
+
|
|
91
|
+
if (profileHash && profileKeys.length === 0) {
|
|
92
|
+
log.error(`ProfileHash ${profileHash} not found for ${name}@${v}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!jsonOut) {
|
|
97
|
+
console.log(`\n ${name}@${v} (${profileKeys.length} profile${profileKeys.length === 1 ? '' : 's'})`);
|
|
98
|
+
if (ventry.source?.gitSha) {
|
|
99
|
+
console.log(` git: ${ventry.source.gitRepo ?? '?'} @ ${ventry.source.gitSha}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// For bare `name` queries we'd fetch O(M×N) manifests (versions ×
|
|
104
|
+
// profiles) — too noisy. Only deep-fetch when the user has narrowed
|
|
105
|
+
// to a specific version.
|
|
106
|
+
const deepFetch = Boolean(version);
|
|
107
|
+
|
|
108
|
+
const profileArr = [];
|
|
109
|
+
for (const hash of profileKeys) {
|
|
110
|
+
const entry = profiles[hash];
|
|
111
|
+
if (!jsonOut) {
|
|
112
|
+
console.log(`\n [${hash}]`);
|
|
113
|
+
if (entry.buildSettings) {
|
|
114
|
+
console.log(` profile : ${formatBuildSettings(entry.buildSettings)}`);
|
|
115
|
+
if (entry.buildSettings.options && Object.keys(entry.buildSettings.options).length > 0) {
|
|
116
|
+
console.log(` options : ${formatOptions(entry.buildSettings.options)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
console.log(` sha256 : ${entry.contentSha256 ?? '?'}`);
|
|
120
|
+
if (entry.publishedAt) console.log(` published: ${entry.publishedAt}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let meta = null;
|
|
124
|
+
if (deepFetch) {
|
|
125
|
+
meta = await provider.v2GetMeta({
|
|
126
|
+
source, name, version: v, profileHash: hash, awsProfile, token,
|
|
127
|
+
});
|
|
128
|
+
if (!jsonOut) {
|
|
129
|
+
if (meta?.uploadedBy) {
|
|
130
|
+
console.log(` origin : consumer upload (${meta.uploadedBy.kind})`);
|
|
131
|
+
console.log(` uploadedAt : ${meta.uploadedBy.uploadedAt}`);
|
|
132
|
+
console.log(` wyvrnpmVersion : ${meta.uploadedBy.wyvrnpmVersion ?? '?'}`);
|
|
133
|
+
if (meta.uploadedBy.builtFromGit) {
|
|
134
|
+
console.log(` builtFromGit : ${meta.uploadedBy.builtFromGit}`);
|
|
135
|
+
}
|
|
136
|
+
if (meta.uploadedBy.builtFromSha) {
|
|
137
|
+
console.log(` builtFromSha : ${meta.uploadedBy.builtFromSha}`);
|
|
138
|
+
}
|
|
139
|
+
} else if (meta) {
|
|
140
|
+
console.log(` origin : author publish`);
|
|
141
|
+
}
|
|
142
|
+
if (meta?.compatibility) {
|
|
143
|
+
console.log(` compat : ${JSON.stringify(meta.compatibility)}`);
|
|
144
|
+
}
|
|
145
|
+
if (meta?.options && Object.keys(meta.options).length > 0) {
|
|
146
|
+
console.log(` declared: ${formatDeclaredOptions(meta.options)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
profileArr.push({
|
|
152
|
+
profileHash: hash,
|
|
153
|
+
contentSha256: entry.contentSha256 ?? null,
|
|
154
|
+
publishedAt: entry.publishedAt ?? null,
|
|
155
|
+
buildSettings: entry.buildSettings ?? null,
|
|
156
|
+
origin: meta?.uploadedBy ? 'consumer-source-build' : (meta ? 'author-publish' : null),
|
|
157
|
+
uploadedBy: meta?.uploadedBy ?? null,
|
|
158
|
+
compatibility: meta?.compatibility ?? null,
|
|
159
|
+
declaredOptions: meta?.options ?? null,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
result.versions[v] = {
|
|
164
|
+
source: ventry.source ?? null,
|
|
165
|
+
profiles: profileArr,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (jsonOut) {
|
|
170
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
log.error(`Package ${name} not found in any configured source`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse a package spec "name[@version[@profileHash]]".
|
|
181
|
+
* @param {string} spec
|
|
182
|
+
* @returns {{ name?: string, version?: string, profileHash?: string }}
|
|
183
|
+
*/
|
|
184
|
+
function parseSpec(spec) {
|
|
185
|
+
if (!spec || typeof spec !== 'string') return {};
|
|
186
|
+
const parts = spec.split('@');
|
|
187
|
+
const [name, version, profileHash] = parts;
|
|
188
|
+
return {
|
|
189
|
+
name: name || undefined,
|
|
190
|
+
version: version || undefined,
|
|
191
|
+
profileHash: profileHash || undefined,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resolveSources(argv) {
|
|
196
|
+
const config = readConfig();
|
|
197
|
+
const sources = (argv.source && argv.source.length > 0)
|
|
198
|
+
? argv.source
|
|
199
|
+
: config.installSources.map((s) => s.url);
|
|
200
|
+
|
|
201
|
+
const firstConfig = config.installSources?.[0];
|
|
202
|
+
return {
|
|
203
|
+
sources,
|
|
204
|
+
awsProfile: argv.awsProfile ?? firstConfig?.profile,
|
|
205
|
+
token: argv.token ?? firstConfig?.token,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatBuildSettings(bs) {
|
|
210
|
+
const parts = [
|
|
211
|
+
`${bs.os}/${bs.arch}`,
|
|
212
|
+
`${bs.compiler} ${bs['compiler.version']}`,
|
|
213
|
+
`C++${bs['compiler.cppstd']}`,
|
|
214
|
+
];
|
|
215
|
+
if (bs['compiler.runtime']) parts.push(`[${bs['compiler.runtime']}]`);
|
|
216
|
+
return parts.join(' ');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatOptions(options) {
|
|
220
|
+
return Object.entries(options)
|
|
221
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
222
|
+
.map(([k, v]) => `${k}=${typeof v === 'string' ? v : String(v)}`)
|
|
223
|
+
.join(', ');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatDeclaredOptions(declared) {
|
|
227
|
+
return Object.entries(declared)
|
|
228
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
229
|
+
.map(([name, spec]) => {
|
|
230
|
+
const allowed = Array.isArray(spec?.allowed) ? spec.allowed : [];
|
|
231
|
+
return `${name} (default=${String(spec?.default)}, allowed=${allowed.map(String).join('|')})`;
|
|
232
|
+
})
|
|
233
|
+
.join('; ');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = show;
|
|
237
|
+
module.exports.parseSpec = parseSpec;
|