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.
- package/README.md +1914 -1860
- package/bin/{wyvrn.js → wyvrnpm.js} +66 -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 -0
- 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/http-fetch.js
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Shared `fetch` wrapper used by every resolver / downloader call site.
|
|
4
|
-
//
|
|
5
|
-
// 2.8.3 shipped with a split between the HTTP provider (which uses
|
|
6
|
-
// node:https directly and sends `User-Agent: wyvrnpm/<ver>` + no
|
|
7
|
-
// `Accept-Encoding`) and the resolver (which used the bare global
|
|
8
|
-
// `fetch` — undici — defaulting to `Accept-Encoding: gzip, deflate,
|
|
9
|
-
// br`). CloudFront caches a **separate entry per response encoding**,
|
|
10
|
-
// so a `versions.json` freshly updated at origin could be stale in the
|
|
11
|
-
// Brotli cache bucket for up to the full TTL while the identity bucket
|
|
12
|
-
// served fresh bytes. The provider calls (show) happened to land in
|
|
13
|
-
// the identity bucket (fresh); the resolver calls (install, add)
|
|
14
|
-
// landed in the Brotli bucket (stale).
|
|
15
|
-
//
|
|
16
|
-
// Mitigation: force `Accept-Encoding: identity`. Manifest JSONs are
|
|
17
|
-
// small (KB to tens of KB) so the lost compression is negligible, and
|
|
18
|
-
// one encoding means one cache bucket — no split-brain. User-Agent +
|
|
19
|
-
// `Accept: application/json` are included for good measure (CloudFront
|
|
20
|
-
// + some WAFs 403 UA-less requests).
|
|
21
|
-
//
|
|
22
|
-
// Defence-in-depth: src/providers/s3.js caps every manifest-JSON PUT
|
|
23
|
-
// at `CacheControl: public, max-age=300` (2.8.4), so the worst-case
|
|
24
|
-
// staleness for any future publish is 5 min regardless of which
|
|
25
|
-
// cache bucket a client lands in.
|
|
26
|
-
//
|
|
27
|
-
// See claude/PLAN-RESOLVER-HTTP-UNIFY.md for the follow-up plan to
|
|
28
|
-
// route all resolver HTTP through the provider API and retire this
|
|
29
|
-
// wrapper.
|
|
30
|
-
|
|
31
|
-
const USER_AGENT = `wyvrnpm/${require('../package.json').version}`;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Drop-in replacement for the global `fetch`. Injects wyvrnpm's
|
|
35
|
-
* `User-Agent`, an `Accept: application/json` hint, and forces
|
|
36
|
-
* `Accept-Encoding: identity` to avoid CloudFront's per-encoding
|
|
37
|
-
* cache-key split. Caller-supplied headers win on collision.
|
|
38
|
-
*
|
|
39
|
-
* @param {string | URL} url
|
|
40
|
-
* @param {RequestInit} [init]
|
|
41
|
-
* @returns {Promise<Response>}
|
|
42
|
-
*/
|
|
43
|
-
function wyvrnFetch(url, init = {}) {
|
|
44
|
-
const headers = {
|
|
45
|
-
'User-Agent': USER_AGENT,
|
|
46
|
-
Accept: 'application/json',
|
|
47
|
-
'Accept-Encoding': 'identity',
|
|
48
|
-
...(init.headers || {}),
|
|
49
|
-
};
|
|
50
|
-
return fetch(url, { ...init, headers });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
module.exports = { wyvrnFetch, USER_AGENT };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared `fetch` wrapper used by every resolver / downloader call site.
|
|
4
|
+
//
|
|
5
|
+
// 2.8.3 shipped with a split between the HTTP provider (which uses
|
|
6
|
+
// node:https directly and sends `User-Agent: wyvrnpm/<ver>` + no
|
|
7
|
+
// `Accept-Encoding`) and the resolver (which used the bare global
|
|
8
|
+
// `fetch` — undici — defaulting to `Accept-Encoding: gzip, deflate,
|
|
9
|
+
// br`). CloudFront caches a **separate entry per response encoding**,
|
|
10
|
+
// so a `versions.json` freshly updated at origin could be stale in the
|
|
11
|
+
// Brotli cache bucket for up to the full TTL while the identity bucket
|
|
12
|
+
// served fresh bytes. The provider calls (show) happened to land in
|
|
13
|
+
// the identity bucket (fresh); the resolver calls (install, add)
|
|
14
|
+
// landed in the Brotli bucket (stale).
|
|
15
|
+
//
|
|
16
|
+
// Mitigation: force `Accept-Encoding: identity`. Manifest JSONs are
|
|
17
|
+
// small (KB to tens of KB) so the lost compression is negligible, and
|
|
18
|
+
// one encoding means one cache bucket — no split-brain. User-Agent +
|
|
19
|
+
// `Accept: application/json` are included for good measure (CloudFront
|
|
20
|
+
// + some WAFs 403 UA-less requests).
|
|
21
|
+
//
|
|
22
|
+
// Defence-in-depth: src/providers/s3.js caps every manifest-JSON PUT
|
|
23
|
+
// at `CacheControl: public, max-age=300` (2.8.4), so the worst-case
|
|
24
|
+
// staleness for any future publish is 5 min regardless of which
|
|
25
|
+
// cache bucket a client lands in.
|
|
26
|
+
//
|
|
27
|
+
// See claude/PLAN-RESOLVER-HTTP-UNIFY.md for the follow-up plan to
|
|
28
|
+
// route all resolver HTTP through the provider API and retire this
|
|
29
|
+
// wrapper.
|
|
30
|
+
|
|
31
|
+
const USER_AGENT = `wyvrnpm/${require('../package.json').version}`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Drop-in replacement for the global `fetch`. Injects wyvrnpm's
|
|
35
|
+
* `User-Agent`, an `Accept: application/json` hint, and forces
|
|
36
|
+
* `Accept-Encoding: identity` to avoid CloudFront's per-encoding
|
|
37
|
+
* cache-key split. Caller-supplied headers win on collision.
|
|
38
|
+
*
|
|
39
|
+
* @param {string | URL} url
|
|
40
|
+
* @param {RequestInit} [init]
|
|
41
|
+
* @returns {Promise<Response>}
|
|
42
|
+
*/
|
|
43
|
+
function wyvrnFetch(url, init = {}) {
|
|
44
|
+
const headers = {
|
|
45
|
+
'User-Agent': USER_AGENT,
|
|
46
|
+
Accept: 'application/json',
|
|
47
|
+
'Accept-Encoding': 'identity',
|
|
48
|
+
...(init.headers || {}),
|
|
49
|
+
};
|
|
50
|
+
return fetch(url, { ...init, headers });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { wyvrnFetch, USER_AGENT };
|
package/src/ignore.js
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// .wyvrnignore matcher — follows .gitignore semantics as closely as is
|
|
7
|
-
// practical for a static-archive filter. Supported:
|
|
8
|
-
//
|
|
9
|
-
// - Blank lines and lines starting with `#` are ignored.
|
|
10
|
-
// - A leading `!` negates the match — re-includes a path a prior pattern
|
|
11
|
-
// excluded. Last matching pattern wins.
|
|
12
|
-
// - A trailing `/` makes the pattern directory-only: it matches directory
|
|
13
|
-
// entries only, never files. This is the single most common gitignore
|
|
14
|
-
// idiom (`build/`, `.git/`) so we honour it rather than treating the
|
|
15
|
-
// slash as a literal character.
|
|
16
|
-
// - A leading `/` anchors the pattern to the project root.
|
|
17
|
-
// - A `/` anywhere else in the pattern (excluding the trailing one) also
|
|
18
|
-
// anchors it to the root — same rule as gitignore.
|
|
19
|
-
// - Otherwise the pattern floats: it matches at any directory depth.
|
|
20
|
-
// - Globs: `*` is single-segment, `**` crosses segments, `?` is one char.
|
|
21
|
-
// - Literal leading `!` or `#` can be escaped with `\!` / `\#`.
|
|
22
|
-
//
|
|
23
|
-
// Not supported (unlike gitignore): pattern re-evaluation after a parent
|
|
24
|
-
// directory has been excluded (gitignore can't re-include a file whose
|
|
25
|
-
// parent directory was excluded — we don't recurse into excluded dirs
|
|
26
|
-
// either, so the behaviour lines up in practice).
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
function parsePattern(raw) {
|
|
30
|
-
let p = raw;
|
|
31
|
-
if (p == null) return null;
|
|
32
|
-
p = p.replace(/\s+$/, ''); // trim trailing whitespace (gitignore rule)
|
|
33
|
-
if (p.length === 0) return null;
|
|
34
|
-
if (p.startsWith('#')) return null;
|
|
35
|
-
|
|
36
|
-
let negate = false;
|
|
37
|
-
if (p.startsWith('!')) {
|
|
38
|
-
negate = true;
|
|
39
|
-
p = p.slice(1);
|
|
40
|
-
} else if (p.startsWith('\\!') || p.startsWith('\\#')) {
|
|
41
|
-
// escaped literal leading ! or #
|
|
42
|
-
p = p.slice(1);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let dirOnly = false;
|
|
46
|
-
if (p.length > 1 && p.endsWith('/')) {
|
|
47
|
-
dirOnly = true;
|
|
48
|
-
p = p.slice(0, -1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const leadingSlash = p.startsWith('/');
|
|
52
|
-
if (leadingSlash) p = p.slice(1);
|
|
53
|
-
|
|
54
|
-
// A `/` anywhere in the (remaining) pattern anchors it to the root.
|
|
55
|
-
// Bare names like `build` or `*.log` float — they match at any depth.
|
|
56
|
-
const anchored = leadingSlash || p.includes('/');
|
|
57
|
-
|
|
58
|
-
const re = p
|
|
59
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
60
|
-
.replace(/\*\*/g, '\x00')
|
|
61
|
-
.replace(/\*/g, '[^/]*')
|
|
62
|
-
.replace(/\?/g, '[^/]')
|
|
63
|
-
.replace(/\x00/g, '.*');
|
|
64
|
-
|
|
65
|
-
const regex = anchored
|
|
66
|
-
? new RegExp(`^${re}(/|$)`)
|
|
67
|
-
: new RegExp(`(^|/)${re}(/|$)`);
|
|
68
|
-
|
|
69
|
-
return { raw, regex, negate, dirOnly };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function loadIgnorePatterns(ignoreFile) {
|
|
73
|
-
if (!fs.existsSync(ignoreFile)) return [];
|
|
74
|
-
return fs
|
|
75
|
-
.readFileSync(ignoreFile, 'utf8')
|
|
76
|
-
.split(/\r?\n/)
|
|
77
|
-
.map(parsePattern)
|
|
78
|
-
.filter(Boolean);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Determine whether a path should be excluded.
|
|
83
|
-
* @param {string} relPath Forward-slash-separated path relative to the root.
|
|
84
|
-
* @param {boolean} isDir True if the path is a directory.
|
|
85
|
-
* @param {Array} patterns Parsed patterns (from loadIgnorePatterns).
|
|
86
|
-
* @returns {boolean}
|
|
87
|
-
*
|
|
88
|
-
* Semantics follow gitignore: a dir-only pattern (`build/`) matches the
|
|
89
|
-
* directory itself AND every descendant. We honour this by testing each
|
|
90
|
-
* ancestor-prefix of the path against dir-only patterns — matters for
|
|
91
|
-
* callers who probe arbitrary paths (tests, library users) rather than
|
|
92
|
-
* walking the tree top-down (where the walker naturally stops recursing
|
|
93
|
-
* once it sees the excluded parent).
|
|
94
|
-
*/
|
|
95
|
-
function isIgnored(relPath, isDir, patterns) {
|
|
96
|
-
let ignored = false;
|
|
97
|
-
for (const { regex, negate, dirOnly } of patterns) {
|
|
98
|
-
let matched;
|
|
99
|
-
if (dirOnly) {
|
|
100
|
-
// Check the path itself (if it's a directory) and every ancestor dir.
|
|
101
|
-
matched = isDir && regex.test(relPath);
|
|
102
|
-
if (!matched) {
|
|
103
|
-
const parts = relPath.split('/');
|
|
104
|
-
let prefix = '';
|
|
105
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
106
|
-
prefix = prefix ? `${prefix}/${parts[i]}` : parts[i];
|
|
107
|
-
if (regex.test(prefix)) { matched = true; break; }
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
matched = regex.test(relPath);
|
|
112
|
-
}
|
|
113
|
-
if (matched) ignored = !negate;
|
|
114
|
-
}
|
|
115
|
-
return ignored;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
module.exports = { parsePattern, loadIgnorePatterns, isIgnored };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// .wyvrnignore matcher — follows .gitignore semantics as closely as is
|
|
7
|
+
// practical for a static-archive filter. Supported:
|
|
8
|
+
//
|
|
9
|
+
// - Blank lines and lines starting with `#` are ignored.
|
|
10
|
+
// - A leading `!` negates the match — re-includes a path a prior pattern
|
|
11
|
+
// excluded. Last matching pattern wins.
|
|
12
|
+
// - A trailing `/` makes the pattern directory-only: it matches directory
|
|
13
|
+
// entries only, never files. This is the single most common gitignore
|
|
14
|
+
// idiom (`build/`, `.git/`) so we honour it rather than treating the
|
|
15
|
+
// slash as a literal character.
|
|
16
|
+
// - A leading `/` anchors the pattern to the project root.
|
|
17
|
+
// - A `/` anywhere else in the pattern (excluding the trailing one) also
|
|
18
|
+
// anchors it to the root — same rule as gitignore.
|
|
19
|
+
// - Otherwise the pattern floats: it matches at any directory depth.
|
|
20
|
+
// - Globs: `*` is single-segment, `**` crosses segments, `?` is one char.
|
|
21
|
+
// - Literal leading `!` or `#` can be escaped with `\!` / `\#`.
|
|
22
|
+
//
|
|
23
|
+
// Not supported (unlike gitignore): pattern re-evaluation after a parent
|
|
24
|
+
// directory has been excluded (gitignore can't re-include a file whose
|
|
25
|
+
// parent directory was excluded — we don't recurse into excluded dirs
|
|
26
|
+
// either, so the behaviour lines up in practice).
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function parsePattern(raw) {
|
|
30
|
+
let p = raw;
|
|
31
|
+
if (p == null) return null;
|
|
32
|
+
p = p.replace(/\s+$/, ''); // trim trailing whitespace (gitignore rule)
|
|
33
|
+
if (p.length === 0) return null;
|
|
34
|
+
if (p.startsWith('#')) return null;
|
|
35
|
+
|
|
36
|
+
let negate = false;
|
|
37
|
+
if (p.startsWith('!')) {
|
|
38
|
+
negate = true;
|
|
39
|
+
p = p.slice(1);
|
|
40
|
+
} else if (p.startsWith('\\!') || p.startsWith('\\#')) {
|
|
41
|
+
// escaped literal leading ! or #
|
|
42
|
+
p = p.slice(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let dirOnly = false;
|
|
46
|
+
if (p.length > 1 && p.endsWith('/')) {
|
|
47
|
+
dirOnly = true;
|
|
48
|
+
p = p.slice(0, -1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const leadingSlash = p.startsWith('/');
|
|
52
|
+
if (leadingSlash) p = p.slice(1);
|
|
53
|
+
|
|
54
|
+
// A `/` anywhere in the (remaining) pattern anchors it to the root.
|
|
55
|
+
// Bare names like `build` or `*.log` float — they match at any depth.
|
|
56
|
+
const anchored = leadingSlash || p.includes('/');
|
|
57
|
+
|
|
58
|
+
const re = p
|
|
59
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
60
|
+
.replace(/\*\*/g, '\x00')
|
|
61
|
+
.replace(/\*/g, '[^/]*')
|
|
62
|
+
.replace(/\?/g, '[^/]')
|
|
63
|
+
.replace(/\x00/g, '.*');
|
|
64
|
+
|
|
65
|
+
const regex = anchored
|
|
66
|
+
? new RegExp(`^${re}(/|$)`)
|
|
67
|
+
: new RegExp(`(^|/)${re}(/|$)`);
|
|
68
|
+
|
|
69
|
+
return { raw, regex, negate, dirOnly };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadIgnorePatterns(ignoreFile) {
|
|
73
|
+
if (!fs.existsSync(ignoreFile)) return [];
|
|
74
|
+
return fs
|
|
75
|
+
.readFileSync(ignoreFile, 'utf8')
|
|
76
|
+
.split(/\r?\n/)
|
|
77
|
+
.map(parsePattern)
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Determine whether a path should be excluded.
|
|
83
|
+
* @param {string} relPath Forward-slash-separated path relative to the root.
|
|
84
|
+
* @param {boolean} isDir True if the path is a directory.
|
|
85
|
+
* @param {Array} patterns Parsed patterns (from loadIgnorePatterns).
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*
|
|
88
|
+
* Semantics follow gitignore: a dir-only pattern (`build/`) matches the
|
|
89
|
+
* directory itself AND every descendant. We honour this by testing each
|
|
90
|
+
* ancestor-prefix of the path against dir-only patterns — matters for
|
|
91
|
+
* callers who probe arbitrary paths (tests, library users) rather than
|
|
92
|
+
* walking the tree top-down (where the walker naturally stops recursing
|
|
93
|
+
* once it sees the excluded parent).
|
|
94
|
+
*/
|
|
95
|
+
function isIgnored(relPath, isDir, patterns) {
|
|
96
|
+
let ignored = false;
|
|
97
|
+
for (const { regex, negate, dirOnly } of patterns) {
|
|
98
|
+
let matched;
|
|
99
|
+
if (dirOnly) {
|
|
100
|
+
// Check the path itself (if it's a directory) and every ancestor dir.
|
|
101
|
+
matched = isDir && regex.test(relPath);
|
|
102
|
+
if (!matched) {
|
|
103
|
+
const parts = relPath.split('/');
|
|
104
|
+
let prefix = '';
|
|
105
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
106
|
+
prefix = prefix ? `${prefix}/${parts[i]}` : parts[i];
|
|
107
|
+
if (regex.test(prefix)) { matched = true; break; }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
matched = regex.test(relPath);
|
|
112
|
+
}
|
|
113
|
+
if (matched) ignored = !negate;
|
|
114
|
+
}
|
|
115
|
+
return ignored;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { parsePattern, loadIgnorePatterns, isIgnored };
|
package/src/logger.js
CHANGED
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// ANSI-coloured, level-tagged logger for wyvrnpm CLI output.
|
|
4
|
-
//
|
|
5
|
-
// All user-facing lines go through this module so the `[wyvrn]` prefix and
|
|
6
|
-
// level markers are applied consistently. Callers pass the message content
|
|
7
|
-
// only — the prefix is never written at call sites.
|
|
8
|
-
//
|
|
9
|
-
// Colour is emitted when the target stream is a TTY, unless overridden by
|
|
10
|
-
// the NO_COLOR or FORCE_COLOR environment variables (see https://no-color.org).
|
|
11
|
-
|
|
12
|
-
const PREFIX = '[wyvrn]';
|
|
13
|
-
|
|
14
|
-
// ── Sensitive-value redaction (EVALUATION.md S8) ──────────────────────────────
|
|
15
|
-
// Defense-in-depth against a token leaking into logs or error traces. Nothing
|
|
16
|
-
// in wyvrnpm deliberately logs tokens today, but callers sometimes paste full
|
|
17
|
-
// commands + error output into bug reports, and some future error path might
|
|
18
|
-
// stringify argv. Two narrow patterns cover every format we handle:
|
|
19
|
-
//
|
|
20
|
-
// `--token <value>` — CLI leak
|
|
21
|
-
// `Authorization: Bearer <value>` — HTTP header leak
|
|
22
|
-
// `Bearer <value>` (standalone) — header fragment leak
|
|
23
|
-
//
|
|
24
|
-
// Redaction is a last-line mitigation — the primary fix is `--token-env` and
|
|
25
|
-
// per-source `tokenEnv` so the literal value never lands in argv to begin with.
|
|
26
|
-
const TOKEN_PATTERNS = [
|
|
27
|
-
[/(--token(?:-env)?[= ]+)\S+/g, '$1***'],
|
|
28
|
-
[/(Authorization\s*:\s*Bearer\s+)\S+/gi, '$1***'],
|
|
29
|
-
[/(\bBearer\s+)[A-Za-z0-9._\-+/=]{16,}/g, '$1***'],
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
function redactSensitive(text) {
|
|
33
|
-
let out = String(text);
|
|
34
|
-
for (const [pattern, replacement] of TOKEN_PATTERNS) {
|
|
35
|
-
out = out.replace(pattern, replacement);
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const ANSI = {
|
|
41
|
-
reset: '\x1b[0m',
|
|
42
|
-
bold: '\x1b[1m',
|
|
43
|
-
dim: '\x1b[2m',
|
|
44
|
-
red: '\x1b[31m',
|
|
45
|
-
green: '\x1b[32m',
|
|
46
|
-
yellow: '\x1b[33m',
|
|
47
|
-
cyan: '\x1b[36m',
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// When a command is emitting a JSON payload on stdout (F7 `--format=json`),
|
|
51
|
-
// stdout must be reserved exclusively for that payload. In that mode we
|
|
52
|
-
// route info/success to stderr too and drop colour so stderr stays
|
|
53
|
-
// log-ingestion-friendly.
|
|
54
|
-
let jsonMode = false;
|
|
55
|
-
|
|
56
|
-
function setJsonMode(enabled) {
|
|
57
|
-
jsonMode = Boolean(enabled);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function isJsonMode() {
|
|
61
|
-
return jsonMode;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function colorEnabled(stream) {
|
|
65
|
-
if (jsonMode) return false;
|
|
66
|
-
if (process.env.NO_COLOR) return false;
|
|
67
|
-
if (process.env.FORCE_COLOR) return true;
|
|
68
|
-
return !!(stream && stream.isTTY);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function paint(text, code, stream) {
|
|
72
|
-
return colorEnabled(stream) ? `${code}${text}${ANSI.reset}` : text;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function emit(stream, consoleFn, coloredPrefix, message) {
|
|
76
|
-
// Every line in a multi-line message gets the prefix — so multi-line errors
|
|
77
|
-
// line up visually without the caller having to repeat `[wyvrn]` per line.
|
|
78
|
-
// Redaction runs before the prefix/colour is applied so patterns like
|
|
79
|
-
// `--token xyz` match regardless of how the caller formatted the line.
|
|
80
|
-
const out = redactSensitive(message)
|
|
81
|
-
.split('\n')
|
|
82
|
-
.map((line) => `${coloredPrefix} ${line}`)
|
|
83
|
-
.join('\n');
|
|
84
|
-
consoleFn(out);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function info(message) {
|
|
88
|
-
// In JSON mode stdout is reserved for the final payload — route info to stderr.
|
|
89
|
-
const stream = jsonMode ? process.stderr : process.stdout;
|
|
90
|
-
const consoleFn = jsonMode ? console.error : console.log;
|
|
91
|
-
emit(stream, consoleFn, paint(PREFIX, ANSI.dim, stream), message);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function warn(message) {
|
|
95
|
-
const p = paint(PREFIX, ANSI.yellow, process.stderr);
|
|
96
|
-
const tag = paint('warn', ANSI.yellow + ANSI.bold, process.stderr);
|
|
97
|
-
emit(process.stderr, console.warn, `${p} ${tag}`, message);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function error(message) {
|
|
101
|
-
const p = paint(PREFIX, ANSI.red, process.stderr);
|
|
102
|
-
const tag = paint('error', ANSI.red + ANSI.bold, process.stderr);
|
|
103
|
-
emit(process.stderr, console.error, `${p} ${tag}`, message);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function success(message) {
|
|
107
|
-
// In JSON mode stdout is reserved for the final payload — route success to stderr.
|
|
108
|
-
const stream = jsonMode ? process.stderr : process.stdout;
|
|
109
|
-
const consoleFn = jsonMode ? console.error : console.log;
|
|
110
|
-
const p = paint(PREFIX, ANSI.green, stream);
|
|
111
|
-
const tag = paint('ok', ANSI.green + ANSI.bold, stream);
|
|
112
|
-
emit(stream, consoleFn, `${p} ${tag}`, message);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function debug(message) {
|
|
116
|
-
if (!process.env.WYVRNPM_DEBUG) return;
|
|
117
|
-
const p = paint(PREFIX, ANSI.dim, process.stderr);
|
|
118
|
-
const tag = paint('debug', ANSI.cyan, process.stderr);
|
|
119
|
-
emit(process.stderr, console.error, `${p} ${tag}`, message);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
module.exports = { info, warn, error, success, debug, setJsonMode, isJsonMode, redactSensitive };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ANSI-coloured, level-tagged logger for wyvrnpm CLI output.
|
|
4
|
+
//
|
|
5
|
+
// All user-facing lines go through this module so the `[wyvrn]` prefix and
|
|
6
|
+
// level markers are applied consistently. Callers pass the message content
|
|
7
|
+
// only — the prefix is never written at call sites.
|
|
8
|
+
//
|
|
9
|
+
// Colour is emitted when the target stream is a TTY, unless overridden by
|
|
10
|
+
// the NO_COLOR or FORCE_COLOR environment variables (see https://no-color.org).
|
|
11
|
+
|
|
12
|
+
const PREFIX = '[wyvrn]';
|
|
13
|
+
|
|
14
|
+
// ── Sensitive-value redaction (EVALUATION.md S8) ──────────────────────────────
|
|
15
|
+
// Defense-in-depth against a token leaking into logs or error traces. Nothing
|
|
16
|
+
// in wyvrnpm deliberately logs tokens today, but callers sometimes paste full
|
|
17
|
+
// commands + error output into bug reports, and some future error path might
|
|
18
|
+
// stringify argv. Two narrow patterns cover every format we handle:
|
|
19
|
+
//
|
|
20
|
+
// `--token <value>` — CLI leak
|
|
21
|
+
// `Authorization: Bearer <value>` — HTTP header leak
|
|
22
|
+
// `Bearer <value>` (standalone) — header fragment leak
|
|
23
|
+
//
|
|
24
|
+
// Redaction is a last-line mitigation — the primary fix is `--token-env` and
|
|
25
|
+
// per-source `tokenEnv` so the literal value never lands in argv to begin with.
|
|
26
|
+
const TOKEN_PATTERNS = [
|
|
27
|
+
[/(--token(?:-env)?[= ]+)\S+/g, '$1***'],
|
|
28
|
+
[/(Authorization\s*:\s*Bearer\s+)\S+/gi, '$1***'],
|
|
29
|
+
[/(\bBearer\s+)[A-Za-z0-9._\-+/=]{16,}/g, '$1***'],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function redactSensitive(text) {
|
|
33
|
+
let out = String(text);
|
|
34
|
+
for (const [pattern, replacement] of TOKEN_PATTERNS) {
|
|
35
|
+
out = out.replace(pattern, replacement);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ANSI = {
|
|
41
|
+
reset: '\x1b[0m',
|
|
42
|
+
bold: '\x1b[1m',
|
|
43
|
+
dim: '\x1b[2m',
|
|
44
|
+
red: '\x1b[31m',
|
|
45
|
+
green: '\x1b[32m',
|
|
46
|
+
yellow: '\x1b[33m',
|
|
47
|
+
cyan: '\x1b[36m',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// When a command is emitting a JSON payload on stdout (F7 `--format=json`),
|
|
51
|
+
// stdout must be reserved exclusively for that payload. In that mode we
|
|
52
|
+
// route info/success to stderr too and drop colour so stderr stays
|
|
53
|
+
// log-ingestion-friendly.
|
|
54
|
+
let jsonMode = false;
|
|
55
|
+
|
|
56
|
+
function setJsonMode(enabled) {
|
|
57
|
+
jsonMode = Boolean(enabled);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isJsonMode() {
|
|
61
|
+
return jsonMode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function colorEnabled(stream) {
|
|
65
|
+
if (jsonMode) return false;
|
|
66
|
+
if (process.env.NO_COLOR) return false;
|
|
67
|
+
if (process.env.FORCE_COLOR) return true;
|
|
68
|
+
return !!(stream && stream.isTTY);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function paint(text, code, stream) {
|
|
72
|
+
return colorEnabled(stream) ? `${code}${text}${ANSI.reset}` : text;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function emit(stream, consoleFn, coloredPrefix, message) {
|
|
76
|
+
// Every line in a multi-line message gets the prefix — so multi-line errors
|
|
77
|
+
// line up visually without the caller having to repeat `[wyvrn]` per line.
|
|
78
|
+
// Redaction runs before the prefix/colour is applied so patterns like
|
|
79
|
+
// `--token xyz` match regardless of how the caller formatted the line.
|
|
80
|
+
const out = redactSensitive(message)
|
|
81
|
+
.split('\n')
|
|
82
|
+
.map((line) => `${coloredPrefix} ${line}`)
|
|
83
|
+
.join('\n');
|
|
84
|
+
consoleFn(out);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function info(message) {
|
|
88
|
+
// In JSON mode stdout is reserved for the final payload — route info to stderr.
|
|
89
|
+
const stream = jsonMode ? process.stderr : process.stdout;
|
|
90
|
+
const consoleFn = jsonMode ? console.error : console.log;
|
|
91
|
+
emit(stream, consoleFn, paint(PREFIX, ANSI.dim, stream), message);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function warn(message) {
|
|
95
|
+
const p = paint(PREFIX, ANSI.yellow, process.stderr);
|
|
96
|
+
const tag = paint('warn', ANSI.yellow + ANSI.bold, process.stderr);
|
|
97
|
+
emit(process.stderr, console.warn, `${p} ${tag}`, message);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function error(message) {
|
|
101
|
+
const p = paint(PREFIX, ANSI.red, process.stderr);
|
|
102
|
+
const tag = paint('error', ANSI.red + ANSI.bold, process.stderr);
|
|
103
|
+
emit(process.stderr, console.error, `${p} ${tag}`, message);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function success(message) {
|
|
107
|
+
// In JSON mode stdout is reserved for the final payload — route success to stderr.
|
|
108
|
+
const stream = jsonMode ? process.stderr : process.stdout;
|
|
109
|
+
const consoleFn = jsonMode ? console.error : console.log;
|
|
110
|
+
const p = paint(PREFIX, ANSI.green, stream);
|
|
111
|
+
const tag = paint('ok', ANSI.green + ANSI.bold, stream);
|
|
112
|
+
emit(stream, consoleFn, `${p} ${tag}`, message);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function debug(message) {
|
|
116
|
+
if (!process.env.WYVRNPM_DEBUG) return;
|
|
117
|
+
const p = paint(PREFIX, ANSI.dim, process.stderr);
|
|
118
|
+
const tag = paint('debug', ANSI.cyan, process.stderr);
|
|
119
|
+
emit(process.stderr, console.error, `${p} ${tag}`, message);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { info, warn, error, success, debug, setJsonMode, isJsonMode, redactSensitive };
|