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
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 };