@getmarrow/mcp 3.9.102 → 3.9.103

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 (109) hide show
  1. package/README.md +49 -23
  2. package/dist/cli.js +41 -10
  3. package/dist/cli.js.map +1 -1
  4. package/dist/codex-plugin-pins.d.ts +19 -0
  5. package/dist/codex-plugin-pins.d.ts.map +1 -0
  6. package/dist/codex-plugin-pins.js +43 -0
  7. package/dist/codex-plugin-pins.js.map +1 -0
  8. package/dist/codex-plugins.d.ts +34 -0
  9. package/dist/codex-plugins.d.ts.map +1 -0
  10. package/dist/codex-plugins.js +240 -0
  11. package/dist/codex-plugins.js.map +1 -0
  12. package/dist/hook-contract.d.ts +2 -2
  13. package/dist/hook-contract.d.ts.map +1 -1
  14. package/dist/hook-contract.js +8 -3
  15. package/dist/hook-contract.js.map +1 -1
  16. package/dist/hook-pre-action.d.ts +48 -2
  17. package/dist/hook-pre-action.d.ts.map +1 -1
  18. package/dist/hook-pre-action.js +195 -22
  19. package/dist/hook-pre-action.js.map +1 -1
  20. package/dist/hook.d.ts +1 -1
  21. package/dist/hook.d.ts.map +1 -1
  22. package/dist/hook.js +12 -6
  23. package/dist/hook.js.map +1 -1
  24. package/dist/host-session.d.ts +11 -0
  25. package/dist/host-session.d.ts.map +1 -1
  26. package/dist/host-session.js +49 -0
  27. package/dist/host-session.js.map +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +15 -10
  30. package/dist/index.js.map +1 -1
  31. package/dist/normalized-action.d.ts +29 -6
  32. package/dist/normalized-action.d.ts.map +1 -1
  33. package/dist/normalized-action.js +492 -102
  34. package/dist/normalized-action.js.map +1 -1
  35. package/dist/own-server-identity.d.ts +123 -0
  36. package/dist/own-server-identity.d.ts.map +1 -0
  37. package/dist/own-server-identity.js +1052 -0
  38. package/dist/own-server-identity.js.map +1 -0
  39. package/dist/own-tool.d.ts +7 -3
  40. package/dist/own-tool.d.ts.map +1 -1
  41. package/dist/own-tool.js +7 -3
  42. package/dist/own-tool.js.map +1 -1
  43. package/dist/redact.d.ts +23 -0
  44. package/dist/redact.d.ts.map +1 -1
  45. package/dist/redact.js +181 -8
  46. package/dist/redact.js.map +1 -1
  47. package/dist/seatbelt/classifier-pin.d.ts +15 -0
  48. package/dist/seatbelt/classifier-pin.d.ts.map +1 -0
  49. package/dist/seatbelt/classifier-pin.js +26 -0
  50. package/dist/seatbelt/classifier-pin.js.map +1 -0
  51. package/dist/seatbelt/guards.d.ts +10 -0
  52. package/dist/seatbelt/guards.d.ts.map +1 -0
  53. package/dist/seatbelt/guards.js +509 -0
  54. package/dist/seatbelt/guards.js.map +1 -0
  55. package/dist/seatbelt/hook.d.ts +72 -0
  56. package/dist/seatbelt/hook.d.ts.map +1 -0
  57. package/dist/seatbelt/hook.js +362 -0
  58. package/dist/seatbelt/hook.js.map +1 -0
  59. package/dist/seatbelt/longopts.d.ts +68 -0
  60. package/dist/seatbelt/longopts.d.ts.map +1 -0
  61. package/dist/seatbelt/longopts.js +377 -0
  62. package/dist/seatbelt/longopts.js.map +1 -0
  63. package/dist/seatbelt/mapper.d.ts +83 -0
  64. package/dist/seatbelt/mapper.d.ts.map +1 -0
  65. package/dist/seatbelt/mapper.js +10248 -0
  66. package/dist/seatbelt/mapper.js.map +1 -0
  67. package/dist/seatbelt/record.d.ts +94 -0
  68. package/dist/seatbelt/record.d.ts.map +1 -0
  69. package/dist/seatbelt/record.js +776 -0
  70. package/dist/seatbelt/record.js.map +1 -0
  71. package/dist/seatbelt/tables.d.ts +82 -0
  72. package/dist/seatbelt/tables.d.ts.map +1 -0
  73. package/dist/seatbelt/tables.js +359 -0
  74. package/dist/seatbelt/tables.js.map +1 -0
  75. package/dist/seatbelt-deploy.d.ts +100 -0
  76. package/dist/seatbelt-deploy.d.ts.map +1 -0
  77. package/dist/seatbelt-deploy.js +393 -0
  78. package/dist/seatbelt-deploy.js.map +1 -0
  79. package/dist/seatbelt-fs.d.ts +30 -0
  80. package/dist/seatbelt-fs.d.ts.map +1 -0
  81. package/dist/seatbelt-fs.js +94 -0
  82. package/dist/seatbelt-fs.js.map +1 -0
  83. package/dist/seatbelt-git.d.ts +212 -0
  84. package/dist/seatbelt-git.d.ts.map +1 -0
  85. package/dist/seatbelt-git.js +844 -0
  86. package/dist/seatbelt-git.js.map +1 -0
  87. package/dist/seatbelt-mode.d.ts +77 -0
  88. package/dist/seatbelt-mode.d.ts.map +1 -0
  89. package/dist/seatbelt-mode.js +496 -0
  90. package/dist/seatbelt-mode.js.map +1 -0
  91. package/dist/seatbelt-path.d.ts +123 -0
  92. package/dist/seatbelt-path.d.ts.map +1 -0
  93. package/dist/seatbelt-path.js +482 -0
  94. package/dist/seatbelt-path.js.map +1 -0
  95. package/dist/seatbelt-scripts.d.ts +109 -0
  96. package/dist/seatbelt-scripts.d.ts.map +1 -0
  97. package/dist/seatbelt-scripts.js +1060 -0
  98. package/dist/seatbelt-scripts.js.map +1 -0
  99. package/dist/tool-catalog.js +2 -2
  100. package/dist/tool-catalog.js.map +1 -1
  101. package/dist/types.d.ts +2 -1
  102. package/dist/types.d.ts.map +1 -1
  103. package/package.json +2 -1
  104. package/server.json +3 -3
  105. package/vendor/classifier/MANIFEST.json +25 -0
  106. package/vendor/classifier/classifier.js +1356 -0
  107. package/vendor/classifier/policy-baseline.js +187 -0
  108. package/vendor/classifier/redact.js +59 -0
  109. package/vendor/classifier/shell-parse.js +455 -0
@@ -0,0 +1,1060 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PUBLISH_LIFECYCLE = exports.MAX_MAKE_EXPANDED_BYTES = exports.MAX_MAKE_COMMANDS = exports.MAX_MAKE_RECIPE_LINES = exports.MAX_MAKE_RULES = exports.MAX_MAKE_PREREQUISITES = exports.MAX_MAKE_TARGETS_PER_RULE = exports.MAX_MAKE_DEPTH = exports.MAX_MANIFEST_BYTES = exports.MAX_SCRIPT_BYTES = void 0;
4
+ exports.resolvePackageScript = resolvePackageScript;
5
+ exports.packageCommands = packageCommands;
6
+ exports.resolveMakeTargets = resolveMakeTargets;
7
+ exports.readScriptFile = readScriptFile;
8
+ const seatbelt_path_1 = require("./seatbelt-path");
9
+ const seatbelt_git_1 = require("./seatbelt-git");
10
+ // Indirect command resolution for S2/S3 (spec §1.2, abuse rows A11/A12):
11
+ // package.json scripts, Makefile recipes and script files are read at hook
12
+ // time so a harmless-looking name cannot hide a deploy or publish. Anything
13
+ // that cannot be read exactly comes back 'unknown'.
14
+ exports.MAX_SCRIPT_BYTES = 64 * 1024;
15
+ exports.MAX_MANIFEST_BYTES = 1024 * 1024;
16
+ exports.MAX_MAKE_DEPTH = 3;
17
+ /** Parse budgets: over any of them the Makefile is 'unknown' (time and memory stay linear in its size). */
18
+ exports.MAX_MAKE_TARGETS_PER_RULE = 256;
19
+ exports.MAX_MAKE_PREREQUISITES = 4096;
20
+ exports.MAX_MAKE_RULES = 4096;
21
+ exports.MAX_MAKE_RECIPE_LINES = 16384;
22
+ exports.MAX_MAKE_COMMANDS = 1024;
23
+ exports.MAX_MAKE_EXPANDED_BYTES = 256 * 1024;
24
+ function readResolved(fs, path, maxBytes) {
25
+ const real = (0, seatbelt_path_1.realPath)(fs, path, { followFinal: true });
26
+ if (real.status !== 'resolved')
27
+ return { status: 'unknown', reason: 'unreadable' };
28
+ if (!real.exists)
29
+ return { status: 'absent' };
30
+ try {
31
+ const entry = fs.lstat(real.path);
32
+ if (!entry)
33
+ return { status: 'absent' };
34
+ if (entry.type !== 'file')
35
+ return { status: 'unknown', reason: 'not_file' };
36
+ if (entry.size > maxBytes)
37
+ return { status: 'unknown', reason: 'too_large' };
38
+ const text = fs.readFile(real.path, maxBytes);
39
+ return text === null ? { status: 'unknown', reason: 'too_large' } : { status: 'ok', text, path: real.path };
40
+ }
41
+ catch {
42
+ return { status: 'unknown', reason: 'unreadable' };
43
+ }
44
+ }
45
+ function parentOf(dir) {
46
+ if (dir === '/')
47
+ return null;
48
+ return dir.slice(0, dir.lastIndexOf('/')) || '/';
49
+ }
50
+ /**
51
+ * The commands `npm|pnpm|yarn|bun run <name>` executes: pre<name>, <name>,
52
+ * post<name>, from the nearest package.json at or above dir (npm's prefix).
53
+ * A missing script is 'absent' (the runner fails); a malformed manifest is unknown.
54
+ */
55
+ function resolvePackageScript(fs, dir, name) {
56
+ return packageCommands(fs, dir, [`pre${name}`, name, `post${name}`]);
57
+ }
58
+ /** Lifecycle scripts `npm publish` runs (S3 "lifecycle prepublish*"). */
59
+ exports.PUBLISH_LIFECYCLE = ['prepublish', 'prepare', 'prepublishOnly', 'prepack', 'postpack', 'publish', 'postpublish'];
60
+ function packageCommands(fs, dir, names) {
61
+ let current = dir;
62
+ while (current) {
63
+ const read = readResolved(fs, current === '/' ? '/package.json' : `${current}/package.json`, exports.MAX_MANIFEST_BYTES);
64
+ if (read.status === 'unknown')
65
+ return { status: 'unknown', reason: read.reason };
66
+ if (read.status === 'ok') {
67
+ let manifest;
68
+ try {
69
+ manifest = JSON.parse(read.text);
70
+ }
71
+ catch {
72
+ return { status: 'unknown', reason: 'malformed' };
73
+ }
74
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest))
75
+ return { status: 'unknown', reason: 'malformed' };
76
+ const scripts = manifest.scripts;
77
+ if (scripts !== undefined && (typeof scripts !== 'object' || scripts === null || Array.isArray(scripts)))
78
+ return { status: 'unknown', reason: 'malformed' };
79
+ const table = (scripts || {});
80
+ const commands = [];
81
+ for (const name of names) {
82
+ if (!Object.prototype.hasOwnProperty.call(table, name))
83
+ continue;
84
+ const command = table[name];
85
+ if (typeof command !== 'string')
86
+ return { status: 'unknown', reason: 'malformed' };
87
+ commands.push({ name, command });
88
+ }
89
+ return { status: 'ok', packageDir: current, commands };
90
+ }
91
+ current = parentOf(current);
92
+ }
93
+ return { status: 'absent' };
94
+ }
95
+ const MAKEFILE_NAMES = ['GNUmakefile', 'makefile', 'Makefile'];
96
+ /**
97
+ * Special targets that only tune make's behaviour; every other dot-target
98
+ * (suffix rules, .DEFAULT, .SECONDEXPANSION, .ONESHELL, .EXPORT_ALL_VARIABLES
99
+ * ...) can change which recipe runs or what it runs with.
100
+ */
101
+ const MAKE_HARMLESS_SPECIAL_TARGETS = new Set(['.PHONY', '.PRECIOUS', '.INTERMEDIATE', '.SECONDARY', '.SILENT', '.IGNORE', '.DELETE_ON_ERROR', '.LOW_RESOLUTION_TIME', '.NOTPARALLEL', '.POSIX']);
102
+ /** Variables that change which program runs the recipes or which files make reads: assigning one is dynamic. */
103
+ const MAKE_EXECUTION_VARIABLES = new Set(['SHELL', 'MAKE', 'MAKEFLAGS', 'MFLAGS', 'GNUMAKEFLAGS', 'MAKEFILES', 'MAKEOVERRIDES', 'MAKELEVEL', 'MAKE_COMMAND', 'MAKECMDGOALS', 'VPATH', 'CURDIR']);
104
+ /** Environment that changes how make itself reads files or resolves variables: any of them present makes the answer dynamic. */
105
+ const MAKE_RELOCATING_ENV = ['MAKEFLAGS', 'GNUMAKEFLAGS', 'MFLAGS', 'MAKEFILES', 'MAKEOVERRIDES'];
106
+ /** A plain system shell path is the one SHELL value that is understood. */
107
+ const MAKE_SAFE_SHELL = /^(?:\/usr)?(?:\/local)?\/bin\/(?:sh|bash|zsh|dash|ksh|ash)$/;
108
+ /**
109
+ * Names that change what a recipe's program is or where it runs: shell start-up
110
+ * files, search paths, loaders, relocated tool state. make hands every variable
111
+ * that came from the environment or the command line to each recipe (and all of
112
+ * them under `.EXPORT_ALL_VARIABLES` or `export`), so an assignment to one of
113
+ * these, in the file or on the command line, is dynamic whether or not this
114
+ * resolver can tell that make exports it. Anything else is only a recipe
115
+ * environment change when the name is in the caller's environment.
116
+ */
117
+ const MAKE_RECIPE_ENV_DENY = new RegExp('^(?:' +
118
+ [
119
+ 'PATH', 'BASH_ENV', 'ENV', 'SHELL', 'SHELLOPTS', 'BASHOPTS', 'IFS', 'CDPATH', 'ZDOTDIR', 'HOME', 'TMPDIR', 'PS4', 'PROMPT_COMMAND',
120
+ 'BASH_.*', 'GIT_.*', 'LD_.*', 'DYLD_.*', 'NODE_.*', 'PYTHON.*', 'PERL.*', 'RUBY.*', 'XDG_.*',
121
+ 'EDITOR', 'VISUAL', 'PAGER', 'BROWSER', 'LESSOPEN', 'LOCPATH', 'NLSPATH', 'GCONV_PATH', 'HOSTALIASES',
122
+ 'JAVA_TOOL_OPTIONS', '_JAVA_OPTIONS', 'JDK_JAVA_OPTIONS', 'CLASSPATH', 'GOFLAGS', 'RUSTC_WRAPPER', 'RUSTFLAGS',
123
+ ].join('|') +
124
+ ')$');
125
+ /** npm reads its configuration from `npm_config_*` in either case. */
126
+ const MAKE_RECIPE_ENV_DENY_ANY_CASE = /^(?:npm_config_.*|path)$/i;
127
+ /**
128
+ * Names that make every recipe (or the git, node or tool it starts) run other
129
+ * code or somewhere else: shell start-up files, the dynamic loader, relocated
130
+ * git. A routine environment has none of them, so one present in the caller's
131
+ * environment (the hook's, or a prefix assignment of the command) is dynamic.
132
+ */
133
+ const MAKE_INJECTION_ENV = new RegExp(`^(?:BASH_ENV|ENV|SHELLOPTS|BASHOPTS|PS4|PROMPT_COMMAND|IFS|CDPATH|ZDOTDIR|BASH_FUNC_.*|LD_.*|DYLD_.*|${seatbelt_git_1.GIT_RELOCATING_ENV.join('|')})$`);
134
+ /**
135
+ * Variables GNU make defines before it reads any file (`make -p -f /dev/null`
136
+ * 4.3, plus the names of later versions). `CC ?= gcc` does not assign (CC is
137
+ * `cc`) and `RM += -r` is `rm -f -r`: with no assignment from the file their
138
+ * value is not known here, so `?=` and `+=` on them are unresolved.
139
+ */
140
+ const MAKE_BUILTIN_VARIABLES = new Set([
141
+ 'AR', 'ARFLAGS', 'AS', 'CC', 'CO', 'COFLAGS', 'CPP', 'CTANGLE', 'CWEAVE', 'CXX', 'F77', 'F77FLAGS', 'FC', 'GET', 'LD', 'LEX', 'LINT', 'M2C', 'MAKEINFO', 'OBJC', 'OUTPUT_OPTION', 'PC', 'RM', 'TANGLE', 'TEX', 'TEXI2DVI', 'WEAVE', 'YACC',
142
+ 'MAKE_HOST', 'MAKE_VERSION', 'MAKE_TERMOUT', 'MAKE_TERMERR', 'MAKEFILE_LIST', 'SUFFIXES', 'GPATH',
143
+ ]);
144
+ const MAKE_BUILTIN_VARIABLE_FAMILIES = /^(?:COMPILE|LINK|PREPROCESS|YACC|LEX|LINT)\./;
145
+ /** Make functions with no side effect (not evaluated here, but a use of their result is unresolved, not an execution at read time). */
146
+ const MAKE_PURE_FUNCTIONS = new Set(['subst', 'patsubst', 'strip', 'findstring', 'filter', 'filter-out', 'sort', 'word', 'wordlist', 'words', 'firstword', 'lastword', 'dir', 'notdir', 'suffix', 'basename', 'addsuffix', 'addprefix', 'join', 'wildcard', 'realpath', 'abspath']);
147
+ const MAKE_SIMPLE_FLAGS = new Set(['-s', '-k', '-w', '--silent', '--quiet', '--keep-going', '--no-print-directory', '--print-directory']);
148
+ /** The very function or object Object.prototype hands every object under this name (`toString`, `constructor`, `__proto__` ...): not an environment variable. */
149
+ function inheritedBuiltin(env, name, value) {
150
+ if (Object.prototype.hasOwnProperty.call(env, name))
151
+ return false;
152
+ if (name === '__proto__')
153
+ return value === Object.getPrototypeOf(env);
154
+ return name in Object.prototype && value === Object.prototype[name];
155
+ }
156
+ /**
157
+ * A simple Makefile recipe parse: explicit rules, recipe lines, prerequisites
158
+ * (depth ≤3) and variable references. Expansion follows GNU make: `=` is
159
+ * deferred, `:=`/`::=` are expanded when read, `?=` and `+=` need the prior
160
+ * value (the environment, when `options.env` is not given, is unknown), target
161
+ * names and prerequisites are expanded when the rule line is read and split
162
+ * into words, recipes after the whole file is read. `$(MAKE) <targets>` in a
163
+ * recipe is followed in the same file. `include`, conditionals, pattern or
164
+ * suffix rules, functions (`$(shell …)`), target-specific variables, variables
165
+ * that change the shell, or an unknown variable make the answer dynamic, which
166
+ * the mapper treats like an unknown script.
167
+ *
168
+ * `options.env` is the environment make would run in (the hook's, plus the
169
+ * command's prefix assignments). A caller MUST pass it: with it omitted every
170
+ * answer is dynamic, because MAKEFLAGS/MAKEFILES, `?=` and the names make
171
+ * exports to recipes all depend on it.
172
+ *
173
+ * Allowlist: an answer is definite only when none of these holds, each of which
174
+ * makes make run something the file does not say, or run it differently:
175
+ * `options.env` omitted, holding a non-string (an own property, enumerable or
176
+ * not), MAKEFLAGS-like names, shell
177
+ * start-up / loader / git-relocating names, or a PATH entry that is relative,
178
+ * non-canonical or inside the makefile's directory; an assignment (file or
179
+ * command line) to a dot-name, an execution variable, a name make hands to
180
+ * recipes (MAKE_RECIPE_ENV_DENY) or a name in the environment; `export`,
181
+ * `unexport`, `.EXPORT_ALL_VARIABLES`; `?=`/`+=` on a variable make defines
182
+ * itself; a command-line value with `$` or a control character, or a
183
+ * command-line name no expansion reaches; an assignment the command line overrides
184
+ * (make still reads it) that is `!=` or has a right side not proven inert; an assignment to a name the file assigned with
185
+ * `override` earlier (make keeps the override and still reads the right side); a recipe-less target that is not
186
+ * .PHONY and could have a source file (built-in rules); a rule for the makefile
187
+ * itself; two recipes for one target. A Makefile with a NUL byte, or a CR that
188
+ * does not end a CRLF, is `unknown` (control_character). The mapper must re-classify `commands`.
189
+ */
190
+ function resolveMakeTargets(fs, dir, targets, options = {}) {
191
+ let read = { status: 'absent' };
192
+ if (options.makefile)
193
+ read = readResolved(fs, options.makefile.startsWith('/') ? options.makefile : `${dir}/${options.makefile}`, exports.MAX_MANIFEST_BYTES);
194
+ else {
195
+ for (const name of MAKEFILE_NAMES) {
196
+ read = readResolved(fs, `${dir}/${name}`, exports.MAX_MANIFEST_BYTES);
197
+ if (read.status !== 'absent')
198
+ break;
199
+ }
200
+ }
201
+ if (read.status !== 'ok')
202
+ return read.status === 'absent' ? read : { status: 'unknown', reason: read.reason };
203
+ // make drops the rest of a line after a NUL, and the newline with it: the next line is joined to this one, so a NUL
204
+ // can hide a rule header or a recipe line from a reader that keeps the text. A CR that does not end a CRLF is not a
205
+ // line end to make, though it is one to other readers. Neither is modelled: the file is not read.
206
+ if (read.text.includes('\0') || /\r(?!\n)/.test(read.text))
207
+ return { status: 'unknown', reason: 'control_character' };
208
+ const variables = new Map();
209
+ const rules = new Map();
210
+ const makefileNames = new Set([...MAKEFILE_NAMES, read.path.slice(read.path.lastIndexOf('/') + 1)]);
211
+ let firstTarget = null;
212
+ const phony = new Set();
213
+ let dynamic = false;
214
+ let current = null;
215
+ let prerequisiteCount = 0;
216
+ let recipeCount = 0;
217
+ const overBudget = { status: 'unknown', reason: 'too_complex' };
218
+ // The environment make would see. Unknown (undefined) means a name the file
219
+ // does not assign may still have a value, ?= / += cannot be resolved, and
220
+ // MAKEFLAGS / MAKEFILES / exported names cannot be ruled out: the answer is
221
+ // dynamic. A caller that states no environment never gets a definite answer.
222
+ const env = options.env;
223
+ /**
224
+ * A string value only. `constructor`, `__proto__` and `toString` (functions and objects Object.prototype hands every
225
+ * object) are not environment variables. Any other value that is neither undefined nor a string is not understood
226
+ * (make is handed it as some text): the answer is dynamic, whichever way the value was reached.
227
+ */
228
+ const envValue = (name) => {
229
+ if (!env)
230
+ return undefined;
231
+ const value = env[name];
232
+ if (typeof value === 'string')
233
+ return value;
234
+ if (value !== undefined && !inheritedBuiltin(env, name, value))
235
+ dynamic = true;
236
+ return undefined;
237
+ };
238
+ if (!env || MAKE_RELOCATING_ENV.some((name) => envValue(name) !== undefined))
239
+ dynamic = true;
240
+ const makeDirectories = [dir, read.path.slice(0, read.path.lastIndexOf('/')) || '/'].map((directory) => directory.replace(/\/+$/, '') || '/');
241
+ const isTrustedPathEntry = (entry) => {
242
+ if (!/^(?:\/|[A-Za-z]:[\\/])/.test(entry) || /(?:^|[\\/])\.{1,2}(?:[\\/]|$)/.test(entry))
243
+ return false;
244
+ const bare = entry.replace(/\/+$/, '') || '/';
245
+ return !makeDirectories.some((directory) => directory === '/' || bare === directory || bare.startsWith(`${directory}/`));
246
+ };
247
+ const envNames = new Set();
248
+ if (env) {
249
+ // for...in: a name an object only inherits is still a name make would see; an own property that is not enumerable
250
+ // is one too (the same value `envValue` reads). A value that is not a string is not understood.
251
+ const names = new Set();
252
+ for (const name in env)
253
+ names.add(name);
254
+ for (const name of Object.getOwnPropertyNames(env))
255
+ names.add(name);
256
+ for (const name of names) {
257
+ const value = env[name];
258
+ if (typeof value !== 'string') {
259
+ if (value !== undefined)
260
+ dynamic = true;
261
+ continue;
262
+ }
263
+ envNames.add(name.toUpperCase());
264
+ if (MAKE_INJECTION_ENV.test(name.toUpperCase()))
265
+ dynamic = true;
266
+ // A PATH entry that is relative, empty, not canonical or inside the directory make runs in makes `git` or
267
+ // `make` in a recipe a file of the project (the workspace root above it is the mapper's to model).
268
+ if (name.toUpperCase() === 'PATH' && value.split(value.includes(';') ? ';' : ':').some((entry) => !isTrustedPathEntry(entry)))
269
+ dynamic = true;
270
+ }
271
+ }
272
+ /**
273
+ * make hands this name to every recipe, or it changes what runs or where:
274
+ * assigning it (in the file or on the command line, any operator) is dynamic.
275
+ * Dot-names are make's own settings; a name in the environment is exported
276
+ * with its new value; the rest is MAKE_RECIPE_ENV_DENY and MAKE_EXECUTION_VARIABLES.
277
+ */
278
+ const changesWhatRuns = (name) => name.startsWith('.') || MAKE_EXECUTION_VARIABLES.has(name) || MAKE_RECIPE_ENV_DENY.test(name) || MAKE_RECIPE_ENV_DENY_ANY_CASE.test(name) || envNames.has(name.toUpperCase());
279
+ const isBuiltin = (name) => MAKE_BUILTIN_VARIABLES.has(name) || MAKE_BUILTIN_VARIABLE_FAMILIES.test(name);
280
+ const commandLine = new Map(Object.entries(options.overrides || {}));
281
+ // Names the file assigned with `override`: make keeps that value, and ignores every later assignment without `override`.
282
+ const overridden = new Set();
283
+ // A command-line variable is exported to every recipe and its value is expanded each time make builds a recipe
284
+ // environment, used or not: only a plain value and a name the Makefile itself uses (and that is not one of the lists) are definite.
285
+ const referenced = new Set();
286
+ for (const [name, value] of commandLine) {
287
+ const shell = name === 'SHELL' && MAKE_SAFE_SHELL.test(value);
288
+ if (!shell && changesWhatRuns(name))
289
+ dynamic = true;
290
+ if (/[$\x00-\x1f\x7f]/.test(value))
291
+ dynamic = true;
292
+ variables.set(name, { flavor: 'recursive', value, unresolved: false });
293
+ }
294
+ // Every expansion is charged to one budget, so nested variables cannot blow up.
295
+ let expandedBytes = 0;
296
+ let exhausted = false;
297
+ // Set while the right side of an assignment make drops is only looked at: no name is marked as used, and a value this
298
+ // resolver could not compute counts as a function with effects (it may hold one).
299
+ let probing = false;
300
+ const MAKE_AUTOMATIC = /^[@<^*?%+|]$/;
301
+ /**
302
+ * `state.bad` is set when a reference could not be resolved (the reference
303
+ * text is kept as written). `state.fn` is set when make would run a function
304
+ * with side effects (`$(shell …)`, `$(eval …)`, `$(file …)`, unparsed syntax):
305
+ * in a context make expands when it reads the line, that executes whether or
306
+ * not the target is wanted.
307
+ */
308
+ const expand = (text, state, depth = 0) => {
309
+ if (!text.includes('$'))
310
+ return text;
311
+ expandedBytes += text.length;
312
+ if (expandedBytes > exports.MAX_MAKE_EXPANDED_BYTES) {
313
+ exhausted = true;
314
+ return '';
315
+ }
316
+ if (depth > 8) {
317
+ state.bad = true;
318
+ state.fn = true;
319
+ return text;
320
+ }
321
+ let out = '';
322
+ let i = 0;
323
+ while (i < text.length) {
324
+ const ch = text[i];
325
+ if (ch !== '$') {
326
+ out += ch;
327
+ i += 1;
328
+ continue;
329
+ }
330
+ const next = text[i + 1];
331
+ if (next === undefined) {
332
+ state.bad = true;
333
+ state.fn = true;
334
+ out += '$';
335
+ i += 1;
336
+ continue;
337
+ }
338
+ if (next === '$') {
339
+ out += '$';
340
+ i += 2;
341
+ continue;
342
+ }
343
+ let reference;
344
+ let name;
345
+ if (next === '(' || next === '{') {
346
+ const close = next === '(' ? ')' : '}';
347
+ let level = 1;
348
+ let j = i + 2;
349
+ while (j < text.length && level > 0) {
350
+ if (text[j] === next)
351
+ level += 1;
352
+ else if (text[j] === close)
353
+ level -= 1;
354
+ j += 1;
355
+ }
356
+ if (level !== 0) {
357
+ // Unbalanced: nothing after this point can be read reliably.
358
+ state.bad = true;
359
+ state.fn = true;
360
+ out += text.slice(i);
361
+ break;
362
+ }
363
+ reference = text.slice(i, j);
364
+ const inner = text.slice(i + 2, j - 1);
365
+ name = inner.includes('$') ? expand(inner, state, depth + 1) : inner;
366
+ i = j;
367
+ }
368
+ else {
369
+ reference = text.slice(i, i + 2);
370
+ name = next;
371
+ i += 2;
372
+ }
373
+ if (exhausted)
374
+ return '';
375
+ if (MAKE_AUTOMATIC.test(name)) {
376
+ out += reference;
377
+ continue;
378
+ }
379
+ // A function call (`$(shell …)`, `$(subst …)`), a substitution reference
380
+ // or an automatic-variable modifier is not evaluated here.
381
+ if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(name)) {
382
+ state.bad = true;
383
+ const functionName = /^([A-Za-z][A-Za-z-]*)\s/.exec(name)?.[1];
384
+ if (functionName !== undefined) {
385
+ if (!MAKE_PURE_FUNCTIONS.has(functionName))
386
+ state.fn = true;
387
+ }
388
+ else if (!/^[A-Za-z_][A-Za-z0-9_.-]*:[^\s=]*=\S*$/.test(name))
389
+ state.fn = true;
390
+ out += reference;
391
+ continue;
392
+ }
393
+ if (!probing)
394
+ referenced.add(name);
395
+ const variable = variables.get(name);
396
+ if (variable === undefined) {
397
+ const fromEnv = envValue(name);
398
+ if (fromEnv !== undefined) {
399
+ const inner = { bad: false, fn: false };
400
+ out += expand(fromEnv, inner, depth + 1);
401
+ if (inner.bad)
402
+ state.bad = true;
403
+ if (inner.fn)
404
+ state.fn = true;
405
+ }
406
+ else if (name === 'MAKE' && !options.overrides?.MAKE)
407
+ out += 'make';
408
+ else {
409
+ state.bad = true;
410
+ out += reference;
411
+ }
412
+ continue;
413
+ }
414
+ if (variable.unresolved) {
415
+ state.bad = true;
416
+ // A deferred value that holds a `$` may hold a call with effects (`CC += $(shell …)` on a variable make defines
417
+ // itself): wherever make expands it when it reads the line, it runs, used or not. A simple value is text make has
418
+ // already expanded (its calls ran when it was assigned and set `fn` then). While a dropped right side is only
419
+ // looked at, any unresolved value counts.
420
+ if (probing || (variable.flavor === 'recursive' && variable.value.includes('$')))
421
+ state.fn = true;
422
+ out += reference;
423
+ continue;
424
+ }
425
+ if (variable.flavor === 'simple')
426
+ out += variable.value;
427
+ else {
428
+ const inner = { bad: false, fn: false };
429
+ out += expand(variable.value, inner, depth + 1);
430
+ if (inner.bad)
431
+ state.bad = true;
432
+ if (inner.fn)
433
+ state.fn = true;
434
+ }
435
+ }
436
+ return out;
437
+ };
438
+ const words = (value) => value.split(/\s+/).filter(Boolean);
439
+ const lines = read.text.replace(/\\\r?\n/g, ' ').split(/\r?\n/);
440
+ for (const rawLine of lines) {
441
+ let line = rawLine;
442
+ if (line.startsWith('\t')) {
443
+ if (current) {
444
+ recipeCount += 1;
445
+ if (recipeCount > exports.MAX_MAKE_RECIPE_LINES)
446
+ return overBudget;
447
+ current.recipe.push(line.slice(1));
448
+ continue;
449
+ }
450
+ // Outside a rule a tab-led line is ordinary makefile syntax (GNU make read.c).
451
+ line = line.replace(/^\t+/, '');
452
+ }
453
+ // `\#` is a literal hash; this resolver does not model it.
454
+ if (line.includes('\\#'))
455
+ dynamic = true;
456
+ const trimmed = line.replace(/#.*$/, '').trim();
457
+ if (!trimmed)
458
+ continue;
459
+ if (/^(-?include|sinclude|ifeq|ifneq|ifdef|ifndef|else|endif|define|endef|export|unexport|undefine|vpath|load|-load)\b/.test(trimmed)) {
460
+ dynamic = true;
461
+ current = null;
462
+ continue;
463
+ }
464
+ const assign = /^(override\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*(\?=|:::=|::=|:=|\+=|!=|=)\s*(.*)$/.exec(trimmed);
465
+ if (assign) {
466
+ const [, overrideKeyword, name, op, value] = assign;
467
+ current = null;
468
+ const safeShell = name === 'SHELL' && (op === '=' || op === ':=' || op === '::=') && MAKE_SAFE_SHELL.test(value.trim());
469
+ if (!safeShell && changesWhatRuns(name))
470
+ dynamic = true;
471
+ // A variable given on the command line wins over the file's assignments (unless `override`). make still reads the
472
+ // dropped assignment: `!=` runs its command, and `:=` / `::=` / `:::=` expand their right side, at read time and
473
+ // under `make -n`. The right side is looked at as if it were assigned (`=`, `?=` and `+=` too: make does not
474
+ // expand those, a call in them is still not proven inert): a call with effects, or a value not understood, is dynamic.
475
+ // A name the file assigned with `override` is kept the same way (the value stays, the right side is read as above), and
476
+ // the answer is not definite: what make keeps is decided by where a name came from, which the later line does not show.
477
+ const keptByCommandLine = commandLine.has(name);
478
+ if (!overrideKeyword && (keptByCommandLine || overridden.has(name))) {
479
+ if (op === '!=')
480
+ dynamic = true;
481
+ else {
482
+ const state = { bad: false, fn: false };
483
+ probing = true;
484
+ try {
485
+ expand(value, state);
486
+ }
487
+ finally {
488
+ probing = false;
489
+ }
490
+ if (exhausted)
491
+ return overBudget;
492
+ if (state.fn)
493
+ dynamic = true;
494
+ }
495
+ if (!keptByCommandLine)
496
+ dynamic = true;
497
+ continue;
498
+ }
499
+ const existing = variables.get(name);
500
+ if (op === '!=') {
501
+ dynamic = true;
502
+ variables.set(name, { flavor: 'simple', value: '', unresolved: true });
503
+ }
504
+ else if (op === '=')
505
+ variables.set(name, { flavor: 'recursive', value, unresolved: false });
506
+ else if (op === ':=' || op === '::=') {
507
+ const state = { bad: false, fn: false };
508
+ variables.set(name, { flavor: 'simple', value: expand(value, state), unresolved: state.bad });
509
+ if (state.fn)
510
+ dynamic = true;
511
+ }
512
+ else if (op === ':::=') {
513
+ const state = { bad: false, fn: false };
514
+ variables.set(name, { flavor: 'recursive', value: expand(value, state).replace(/\$/g, '$$$$'), unresolved: state.bad });
515
+ if (state.fn)
516
+ dynamic = true;
517
+ }
518
+ else if (op === '?=') {
519
+ // Defined already (in the file or on the command line): unchanged. Otherwise the environment decides.
520
+ if (existing === undefined) {
521
+ const fromEnv = envValue(name);
522
+ if (fromEnv !== undefined)
523
+ variables.set(name, { flavor: 'recursive', value: fromEnv, unresolved: false });
524
+ else if (env && !isBuiltin(name))
525
+ variables.set(name, { flavor: 'recursive', value, unresolved: false });
526
+ else
527
+ variables.set(name, { flavor: 'recursive', value, unresolved: true });
528
+ }
529
+ }
530
+ else if (op === '+=') {
531
+ if (existing === undefined) {
532
+ const fromEnv = envValue(name);
533
+ if (fromEnv !== undefined)
534
+ variables.set(name, { flavor: 'recursive', value: `${fromEnv} ${value}`, unresolved: false });
535
+ else if (env && !isBuiltin(name))
536
+ variables.set(name, { flavor: 'recursive', value, unresolved: false });
537
+ else
538
+ variables.set(name, { flavor: 'recursive', value, unresolved: true });
539
+ }
540
+ else if (existing.flavor === 'simple') {
541
+ const state = { bad: false, fn: false };
542
+ const added = expand(value, state);
543
+ variables.set(name, { flavor: 'simple', value: `${existing.value} ${added}`.trim(), unresolved: existing.unresolved || state.bad });
544
+ if (state.fn)
545
+ dynamic = true;
546
+ }
547
+ else
548
+ variables.set(name, { flavor: 'recursive', value: `${existing.value} ${value}`.trim(), unresolved: existing.unresolved });
549
+ }
550
+ // `?=` on a name that is already defined assigns nothing: it does not make the name an overridden one.
551
+ if (overrideKeyword && !(op === '?=' && existing !== undefined))
552
+ overridden.add(name);
553
+ const stored = variables.get(name);
554
+ if (stored !== undefined && stored.value.length > exports.MAX_MAKE_EXPANDED_BYTES)
555
+ return overBudget;
556
+ if (exhausted)
557
+ return overBudget;
558
+ continue;
559
+ }
560
+ const rule = /^([^:=]+)::?([^=]*)$/.exec(trimmed);
561
+ if (!rule) {
562
+ // Target-specific variables (`t: V = x`), `.RECIPEPREFIX`, anything else this parse does not model.
563
+ dynamic = true;
564
+ current = null;
565
+ continue;
566
+ }
567
+ let prereqs = rule[2].trim();
568
+ let inlineRecipe = null;
569
+ const semi = prereqs.indexOf(';');
570
+ if (semi >= 0) {
571
+ inlineRecipe = prereqs.slice(semi + 1).trim();
572
+ prereqs = prereqs.slice(0, semi);
573
+ }
574
+ const nameState = { bad: false, fn: false };
575
+ const names = words(expand(rule[1].trim(), nameState));
576
+ if (exhausted)
577
+ return overBudget;
578
+ // A name that cannot be expanded may be the wanted target.
579
+ if (nameState.bad)
580
+ dynamic = true;
581
+ if (names.length > exports.MAX_MAKE_TARGETS_PER_RULE)
582
+ return overBudget;
583
+ const prerequisiteState = { bad: false, fn: false };
584
+ const expandedPrerequisites = expand(prereqs, prerequisiteState);
585
+ if (exhausted)
586
+ return overBudget;
587
+ // Make expands prerequisites when it reads the line: a function with effects runs for every rule, wanted or not.
588
+ if (prerequisiteState.fn)
589
+ dynamic = true;
590
+ const prerequisiteWords = words(expandedPrerequisites).filter((word) => word !== '|');
591
+ // `$` left over (automatic variables, `$$`), a second `:` (static pattern rule) or a wildcard cannot be followed.
592
+ const unresolved = prerequisiteState.bad || /[$:%*?[]/.test(expandedPrerequisites);
593
+ const ruleLine = { prerequisites: prerequisiteWords, unresolved, recipe: inlineRecipe ? [inlineRecipe] : [] };
594
+ prerequisiteCount += ruleLine.prerequisites.length;
595
+ if (prerequisiteCount > exports.MAX_MAKE_PREREQUISITES)
596
+ return overBudget;
597
+ current = ruleLine;
598
+ for (const name of names) {
599
+ // make brings the makefile itself up to date first: a rule for it runs whatever target is wanted.
600
+ if (makefileNames.has(name.replace(/^(?:\.\/)+/, '')))
601
+ dynamic = true;
602
+ if (name.includes('%'))
603
+ dynamic = true;
604
+ if (name.startsWith('.') && !MAKE_HARMLESS_SPECIAL_TARGETS.has(name))
605
+ dynamic = true;
606
+ if (name === '.PHONY')
607
+ for (const word of prerequisiteWords)
608
+ phony.add(word);
609
+ if (name.startsWith('.') && MAKE_HARMLESS_SPECIAL_TARGETS.has(name))
610
+ continue;
611
+ if (firstTarget === null && !name.includes('%') && !name.startsWith('.'))
612
+ firstTarget = name;
613
+ let entry = rules.get(name);
614
+ if (!entry) {
615
+ if (rules.size >= exports.MAX_MAKE_RULES)
616
+ return overBudget;
617
+ entry = { lines: [] };
618
+ rules.set(name, entry);
619
+ }
620
+ if (entry.lines[entry.lines.length - 1] !== ruleLine)
621
+ entry.lines.push(ruleLine);
622
+ }
623
+ }
624
+ const commands = [];
625
+ const listings = new Map();
626
+ /**
627
+ * A target with no recipe of its own that is not .PHONY is searched for in
628
+ * make's built-in rules (`%.o: %.c`, `%: %.c`, `%: %.sh`, RCS/SCCS ...), which
629
+ * run `$(CC)`, `$(COMPILE.c)` ... on a source file. With a file in the
630
+ * directory that could be that source (or one that cannot be listed) the
631
+ * commands make runs are not in the file: dynamic.
632
+ */
633
+ const implicitRuleMayApply = (target) => {
634
+ if (/[()$%*?[\]]/.test(target))
635
+ return true;
636
+ const slash = target.lastIndexOf('/');
637
+ const where = slash < 0 ? dir : target.startsWith('/') ? target.slice(0, slash) || '/' : `${dir}/${target.slice(0, slash)}`;
638
+ let entries = listings.get(where);
639
+ if (entries === undefined) {
640
+ try {
641
+ entries = fs.readdir(where);
642
+ }
643
+ catch {
644
+ entries = null;
645
+ }
646
+ listings.set(where, entries);
647
+ }
648
+ if (entries === null)
649
+ return true;
650
+ const base = target.slice(slash + 1);
651
+ const stem = base.split('.')[0];
652
+ return entries.some((entry) => entry === 'RCS' || entry === 'SCCS' || (entry !== base && (entry.startsWith(`${stem}.`) || entry === `${base},v` || entry === `s.${base}`)));
653
+ };
654
+ const visit = (target, depth, seen) => {
655
+ if (exhausted || seen.has(target))
656
+ return;
657
+ seen.add(target);
658
+ const rule = rules.get(target);
659
+ if (!rule?.lines.some((ruleLine) => ruleLine.recipe.length > 0) && !phony.has(target) && implicitRuleMayApply(target))
660
+ dynamic = true;
661
+ if (!rule)
662
+ return;
663
+ // Two rules with a recipe for one target: make keeps one (a warning) and drops the other, unless they are `::`
664
+ // rules; which commands run, and which variables they expand, is not read here.
665
+ if (rule.lines.filter((ruleLine) => ruleLine.recipe.length > 0).length > 1)
666
+ dynamic = true;
667
+ for (const ruleLine of rule.lines) {
668
+ if (ruleLine.unresolved)
669
+ dynamic = true;
670
+ if (depth < exports.MAX_MAKE_DEPTH)
671
+ for (const prerequisite of ruleLine.prerequisites)
672
+ visit(prerequisite, depth + 1, seen);
673
+ else if (ruleLine.prerequisites.length > 0)
674
+ dynamic = true;
675
+ }
676
+ for (const ruleLine of rule.lines) {
677
+ for (const line of ruleLine.recipe) {
678
+ if (commands.length >= exports.MAX_MAKE_COMMANDS) {
679
+ exhausted = true;
680
+ return;
681
+ }
682
+ const state = { bad: false, fn: false };
683
+ const command = expand(line, state).replace(/^[\s@+-]+/, '');
684
+ if (state.bad)
685
+ dynamic = true;
686
+ commands.push(command);
687
+ // `$(MAKE) …` or a command that starts with `make` (also through a variable: RUN = $(MAKE) x).
688
+ if (/\$[({]MAKE[)}]/.test(line) || /^g?make(\s|$)/.test(command)) {
689
+ // A sub-make reads the default makefile of its directory: -f is not passed down in MAKEFLAGS.
690
+ if (options.makefile)
691
+ dynamic = true;
692
+ else
693
+ followSubMake(command, depth, seen);
694
+ }
695
+ }
696
+ }
697
+ };
698
+ // `$(MAKE) a b` runs the same file's targets again: follow plain forms; any
699
+ // other form (-C, -f, VAR=x, a chained command) cannot be followed.
700
+ const followSubMake = (command, depth, seen) => {
701
+ const tokens = /[;&|<>()`$"'\\\n]/.test(command) ? null : words(command);
702
+ if (!tokens || tokens.length === 0 || tokens[0] !== 'make') {
703
+ dynamic = true;
704
+ return;
705
+ }
706
+ const subTargets = [];
707
+ for (const token of tokens.slice(1)) {
708
+ if (MAKE_SIMPLE_FLAGS.has(token) || /^-j\d*$/.test(token))
709
+ continue;
710
+ if (/^[A-Za-z0-9_][A-Za-z0-9_./+-]*$/.test(token))
711
+ subTargets.push(token);
712
+ else {
713
+ dynamic = true;
714
+ return;
715
+ }
716
+ }
717
+ if (depth + 1 > exports.MAX_MAKE_DEPTH) {
718
+ dynamic = true;
719
+ return;
720
+ }
721
+ const wantedHere = subTargets.length > 0 ? subTargets : firstTarget ? [firstTarget] : [];
722
+ if (wantedHere.length === 0)
723
+ dynamic = true;
724
+ for (const sub of wantedHere) {
725
+ if (!rules.has(sub))
726
+ dynamic = true;
727
+ // A fresh `seen`: a sub-make is a new run, though depth still bounds the recursion.
728
+ visit(sub, depth + 1, new Set(seen));
729
+ }
730
+ };
731
+ const wanted = targets.length > 0 ? targets : firstTarget ? [firstTarget] : [];
732
+ for (const target of wanted)
733
+ visit(target, 0, new Set());
734
+ if (exhausted)
735
+ return overBudget;
736
+ // A command-line variable the Makefile never mentions can only act through the environment of the recipes.
737
+ for (const [name, value] of commandLine)
738
+ if (!referenced.has(name) && !(name === 'SHELL' && MAKE_SAFE_SHELL.test(value)))
739
+ dynamic = true;
740
+ return { status: 'ok', makefile: read.path, commands, dynamic: dynamic || wanted.some((t) => !rules.has(t)) };
741
+ }
742
+ const SHELL_PROGRAMS = /^(?:sh|bash|zsh|dash|ksh|ash)$/;
743
+ /** Where a shebang's interpreter (or `env`) is trusted to be the program its name says; a copy elsewhere (a repo's `./bin/bash`) is unknown. */
744
+ const SYSTEM_BIN_DIRS = new Set(['/bin', '/usr/bin', '/usr/local/bin', '/sbin', '/usr/sbin', '/opt/homebrew/bin', '/opt/local/bin', '/usr/pkg/bin']);
745
+ const SHELL_FLAG_LETTERS = 'euxvfnCabhEBHPkT';
746
+ const SHELL_OPTION_NAMES = new Set(['pipefail', 'errexit', 'nounset', 'xtrace', 'noglob', 'noclobber', 'verbose', 'noexec', 'posix', 'allexport', 'errtrace', 'functrace']);
747
+ const SHELL_LONG_FLAGS = new Set(['--posix', '--noprofile', '--norc']);
748
+ const MAX_ENV_SPLIT_DEPTH = 4;
749
+ /**
750
+ * The only shebang lines a definite `shell` answer is given for: at most this
751
+ * many bytes (Linux before 5.1 cuts the line at 127; later kernels fail or cut at
752
+ * 255) of printable ASCII and tab. Anything else (CR, VT, FF, NUL, DEL, other
753
+ * control or non-ASCII characters) is split differently by the kernel, env -S and
754
+ * this parser, so it is never `shell`.
755
+ */
756
+ const MAX_SHEBANG_LINE = 127;
757
+ const PLAIN_SHEBANG_LINE = /^[\x20-\x7e\t]*$/;
758
+ /** A control character (anything below space except tab, DEL, NUL ...). */
759
+ const CONTROL_CHARACTER = /[\x00-\x08\x0a-\x1f\x7f]/;
760
+ /** A line that is not on the allowlist is never a definite shell; an answer that was already `other` or `unknown` is kept (never looser). */
761
+ function degrade(result) {
762
+ return result.kind === 'shell' ? { kind: 'other' } : result;
763
+ }
764
+ /** coreutils `env -S` split-string: quotes, backslash escapes, `#` comment. Null for anything else (`$` expansion, `\c`, unknown escapes, unclosed quote). */
765
+ function splitEnvString(value) {
766
+ const escapes = { n: '\n', t: '\t', r: '\r', f: '\f', v: '\v', '\\': '\\', '"': '"', "'": "'", '#': '#', $: '$' };
767
+ const tokens = [];
768
+ let word = '';
769
+ let inWord = false;
770
+ let quote = null;
771
+ for (let i = 0; i < value.length; i += 1) {
772
+ const ch = value[i];
773
+ if (ch === '$')
774
+ return null;
775
+ if (quote === "'") {
776
+ if (ch === "'")
777
+ quote = null;
778
+ else if (ch === '\\') {
779
+ const next = value[i + 1];
780
+ if (next !== '\\' && next !== "'")
781
+ return null;
782
+ word += next;
783
+ i += 1;
784
+ }
785
+ else
786
+ word += ch;
787
+ continue;
788
+ }
789
+ if (quote === '"') {
790
+ if (ch === '"')
791
+ quote = null;
792
+ else if (ch === '\\') {
793
+ const next = value[i + 1];
794
+ if (next === '_')
795
+ word += ' ';
796
+ else if (next !== undefined && Object.prototype.hasOwnProperty.call(escapes, next))
797
+ word += escapes[next];
798
+ else
799
+ return null;
800
+ i += 1;
801
+ }
802
+ else
803
+ word += ch;
804
+ continue;
805
+ }
806
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f' || ch === '\v') {
807
+ if (inWord)
808
+ tokens.push(word);
809
+ word = '';
810
+ inWord = false;
811
+ continue;
812
+ }
813
+ if (ch === '#' && !inWord)
814
+ break;
815
+ if (ch === "'" || ch === '"') {
816
+ quote = ch;
817
+ inWord = true;
818
+ continue;
819
+ }
820
+ if (ch === '\\') {
821
+ const next = value[i + 1];
822
+ if (next === '_') {
823
+ if (inWord)
824
+ tokens.push(word);
825
+ word = '';
826
+ inWord = false;
827
+ }
828
+ else if (next !== undefined && Object.prototype.hasOwnProperty.call(escapes, next)) {
829
+ word += escapes[next];
830
+ inWord = true;
831
+ }
832
+ else
833
+ return null;
834
+ i += 1;
835
+ continue;
836
+ }
837
+ word += ch;
838
+ inWord = true;
839
+ }
840
+ if (quote)
841
+ return null;
842
+ if (inWord)
843
+ tokens.push(word);
844
+ return tokens;
845
+ }
846
+ /** The words after a shell interpreter: only plain option flags are understood; `-c <command>` is returned as a command. */
847
+ function classifyShellArguments(tokens) {
848
+ let command = false;
849
+ for (let i = 0; i < tokens.length; i += 1) {
850
+ const token = tokens[i];
851
+ if (token === '--')
852
+ return i + 1 < tokens.length || command ? { kind: 'unknown' } : { kind: 'shell' };
853
+ if (!/^[-+]./.test(token)) {
854
+ // An operand: with -c it is the command string (the rest are $0, $1 ...); otherwise a script other than this file.
855
+ return command ? { kind: 'other', command: token } : { kind: 'unknown' };
856
+ }
857
+ if (token.startsWith('--')) {
858
+ if (!SHELL_LONG_FLAGS.has(token))
859
+ return { kind: 'unknown' };
860
+ continue;
861
+ }
862
+ const letters = token.slice(1);
863
+ for (let j = 0; j < letters.length; j += 1) {
864
+ const letter = letters[j];
865
+ if (letter === 'c') {
866
+ command = true;
867
+ }
868
+ else if (letter === 'o') {
869
+ // `-o name` (also `-euo pipefail`): the name is the next word.
870
+ if (j !== letters.length - 1)
871
+ return { kind: 'unknown' };
872
+ const name = tokens[i + 1];
873
+ if (name === undefined || !SHELL_OPTION_NAMES.has(name))
874
+ return { kind: 'unknown' };
875
+ i += 1;
876
+ }
877
+ else if (!SHELL_FLAG_LETTERS.includes(letter))
878
+ return { kind: 'unknown' };
879
+ }
880
+ }
881
+ return command ? { kind: 'unknown' } : { kind: 'shell' };
882
+ }
883
+ /**
884
+ * What runs after `env`: options, then the program. Anything but plain
885
+ * `-i`/`-u NAME`/`-S` and a program is unknown (NAME=value, -C, --argv0 ...).
886
+ * `tokens` are argv words; `raw` (with each word's end offset) is the shebang
887
+ * text itself, which `-S` consumes verbatim and splits with env's own quoting.
888
+ */
889
+ function walkEnv(tokens, ends, raw, depth, flags) {
890
+ if (depth > MAX_ENV_SPLIT_DEPTH)
891
+ return { kind: 'unknown' };
892
+ const split = (attached, index) => {
893
+ let value;
894
+ let consumed = index;
895
+ if (ends)
896
+ value = attached + raw.slice(ends[index]);
897
+ else if (attached !== '')
898
+ value = attached;
899
+ else {
900
+ if (tokens[index + 1] === undefined)
901
+ return { kind: 'unknown' };
902
+ value = tokens[index + 1];
903
+ consumed = index + 1;
904
+ }
905
+ const words = splitEnvString(value);
906
+ if (!words || words.length === 0)
907
+ return { kind: 'unknown' };
908
+ // An escape (`\r`, `\t`, `\f` ...) can put a control character, tab included, into a word.
909
+ if (words.some((word) => /[^\x20-\x7e]/.test(word)))
910
+ flags.degraded = true;
911
+ return walkEnv(ends ? words : [...words, ...tokens.slice(consumed + 1)], null, '', depth + 1, flags);
912
+ };
913
+ for (let i = 0; i < tokens.length; i += 1) {
914
+ const word = tokens[i];
915
+ if (word === '--')
916
+ return classifyProgram(tokens.slice(i + 1), depth, flags);
917
+ if (word === '-')
918
+ continue;
919
+ if (word.startsWith('--')) {
920
+ if (word === '--ignore-environment')
921
+ continue;
922
+ // Harmless to env itself, but not on the allowlist: never a definite shell.
923
+ if (word === '--null' || word === '--debug') {
924
+ flags.degraded = true;
925
+ continue;
926
+ }
927
+ if (word === '--unset') {
928
+ if (tokens[i + 1] === undefined)
929
+ return { kind: 'unknown' };
930
+ i += 1;
931
+ continue;
932
+ }
933
+ if (word.startsWith('--unset='))
934
+ continue;
935
+ if (word === '--split-string')
936
+ return split('', i);
937
+ if (word.startsWith('--split-string='))
938
+ return split(word.slice('--split-string='.length), i);
939
+ return { kind: 'unknown' };
940
+ }
941
+ if (word.startsWith('-')) {
942
+ for (let j = 1; j < word.length; j += 1) {
943
+ const letter = word[j];
944
+ if (letter === 'i')
945
+ continue;
946
+ if (letter === '0' || letter === 'v') {
947
+ flags.degraded = true;
948
+ continue;
949
+ }
950
+ if (letter === 'u') {
951
+ // The name is the rest of this word or the next one.
952
+ if (j === word.length - 1) {
953
+ if (tokens[i + 1] === undefined)
954
+ return { kind: 'unknown' };
955
+ i += 1;
956
+ }
957
+ break;
958
+ }
959
+ if (letter === 'S')
960
+ return split(word.slice(j + 1), i);
961
+ return { kind: 'unknown' };
962
+ }
963
+ continue;
964
+ }
965
+ // NAME=value (BASH_ENV, ENV, PATH, SHELLOPTS, PS4 ...): the interpreter's environment is not modelled.
966
+ if (word.includes('='))
967
+ return { kind: 'unknown' };
968
+ return classifyProgram(tokens.slice(i), depth, flags);
969
+ }
970
+ return { kind: 'unknown' };
971
+ }
972
+ function classifyEnvArguments(raw, flags) {
973
+ const tokens = [];
974
+ const ends = [];
975
+ const re = /[^ \t]+/g;
976
+ for (let match = re.exec(raw); match; match = re.exec(raw)) {
977
+ tokens.push(match[0]);
978
+ ends.push(match.index + match[0].length);
979
+ }
980
+ return walkEnv(tokens, ends, raw, 0, flags);
981
+ }
982
+ function classifyProgram(tokens, depth, flags) {
983
+ const program = tokens[0];
984
+ if (program === undefined || program === '')
985
+ return { kind: 'unknown' };
986
+ const slash = program.lastIndexOf('/');
987
+ const base = slash < 0 ? program : program.slice(slash + 1);
988
+ if (base !== 'env' && !SHELL_PROGRAMS.test(base))
989
+ return { kind: 'other' };
990
+ // A shell or env named by a relative path, or found outside the system bin
991
+ // dirs, is a file the agent may have planted: its name proves nothing.
992
+ if (slash >= 0 && !SYSTEM_BIN_DIRS.has(slash === 0 ? '/' : program.slice(0, slash)))
993
+ return { kind: 'unknown' };
994
+ if (base === 'env')
995
+ return walkEnv(tokens.slice(1), null, '', depth + 1, flags);
996
+ return classifyShellArguments(tokens.slice(1));
997
+ }
998
+ /**
999
+ * `#!` line → what really runs. 'shell' only for a system shell with plain flags
1000
+ * on a plain line; every form that is not understood is 'unknown', and a line
1001
+ * that is understood but not plain (control or non-ASCII characters, too long,
1002
+ * an env option off the allowlist) is 'other', never 'shell'.
1003
+ */
1004
+ function classifyShebang(line) {
1005
+ const body = line.trim();
1006
+ const space = body.search(/[ \t]/);
1007
+ const interpreter = space < 0 ? body : body.slice(0, space);
1008
+ const rest = space < 0 ? '' : body.slice(space).trim();
1009
+ if (!interpreter.startsWith('/'))
1010
+ return { kind: 'unknown' };
1011
+ const slash = interpreter.lastIndexOf('/');
1012
+ const dir = interpreter.slice(0, slash) || '/';
1013
+ const base = interpreter.slice(slash + 1);
1014
+ let result;
1015
+ const flags = { degraded: false };
1016
+ if (base === 'env' || SHELL_PROGRAMS.test(base)) {
1017
+ if (!SYSTEM_BIN_DIRS.has(dir))
1018
+ return { kind: 'unknown' };
1019
+ if (base === 'env')
1020
+ result = classifyEnvArguments(rest, flags);
1021
+ else
1022
+ result = classifyShellArguments(rest.split(/[ \t]+/).filter(Boolean));
1023
+ }
1024
+ else
1025
+ result = { kind: 'other' };
1026
+ // The kernel ends the line at \n only; \r and the other control characters stay in it.
1027
+ if (line.length + 2 > MAX_SHEBANG_LINE || !PLAIN_SHEBANG_LINE.test(line))
1028
+ flags.degraded = true;
1029
+ return flags.degraded ? degrade(result) : result;
1030
+ }
1031
+ /**
1032
+ * A script file run as `./x.sh` or `bash x.sh` (≤64 KB), realpath-ed; the
1033
+ * interpreter comes from the shebang, which is the first line up to `\n` only.
1034
+ * With no shebang the kernel fails the exec and the invoking shell runs the file
1035
+ * as a sh script, unless its first line carries binary magic (control characters).
1036
+ */
1037
+ function readScriptFile(fs, path) {
1038
+ const read = readResolved(fs, path, exports.MAX_SCRIPT_BYTES);
1039
+ if (read.status !== 'ok')
1040
+ return read;
1041
+ const newline = read.text.indexOf('\n');
1042
+ const firstLine = newline < 0 ? read.text : read.text.slice(0, newline);
1043
+ let result;
1044
+ if (read.text.startsWith('#!')) {
1045
+ const line = firstLine.slice(2);
1046
+ result = classifyShebang(line);
1047
+ // Never looser than the answer this parser gave when it cut the line at its first \r (round 3 and before).
1048
+ const cr = line.indexOf('\r');
1049
+ if (cr >= 0 && result.kind !== 'unknown' && classifyShebang(line.slice(0, cr)).kind === 'unknown')
1050
+ result = { kind: 'unknown' };
1051
+ }
1052
+ else
1053
+ result = CONTROL_CHARACTER.test(firstLine) ? { kind: 'other' } : { kind: 'shell' };
1054
+ if (result.kind === 'unknown')
1055
+ return { status: 'unknown', reason: 'shebang' };
1056
+ if (result.kind === 'shell')
1057
+ return { status: 'ok', path: read.path, interpreter: 'shell', text: read.text };
1058
+ return { status: 'ok', path: read.path, interpreter: 'other', text: read.text, ...(result.command !== undefined ? { shebangCommand: result.command } : {}) };
1059
+ }
1060
+ //# sourceMappingURL=seatbelt-scripts.js.map