type PackageManager = "pnpm" | "npm" | "yarn"; type ManagerParseResult = Readonly<{ dependencies: readonly string[]; unsafeLifecycle: boolean; unsupportedManagerSyntax: boolean; }>; type SuppressionState = { effective: boolean | undefined; contradictory: boolean; malformed: boolean; }; type ShellNpmScopeEnvironmentState = { autoExport: boolean; forbidden: boolean; uncertain: boolean; }; type ShellCommandPrefix = Readonly<{ assignments: readonly Readonly<{ dynamicName: boolean; name: string | null; }>[]; commandIndex: number; uncertain: boolean; }>; type EnvironmentCommandPrefix = Readonly<{ assignmentNames: readonly string[]; uncertain: boolean; }>; type TokenizedShellSegment = Readonly<{ tokens: readonly string[]; expansionTokens: readonly boolean[]; }>; const managerNames = new Set(["pnpm", "npm", "yarn"]); const managerOptionsWithValue = new Set([ "-C", "--cache", "--cache-folder", "--config-dir", "--cwd", "--dir", "--filter", "--global-dir", "--globalconfig", "--home", "--lockfile-dir", "--mutex", "--prefix", "--registry", "--store-dir", "--userconfig", "--workspace", "--workspace-dir", ]); const managerBooleanOptions = new Set([ "--color", "--global", "--no-color", "--offline", "--prefer-offline", "--silent", "--use-stderr", "--verbose", "-g", "-s", ]); const npmScriptDispatchBooleanOptions = new Set([ "--foreground-scripts", "--if-present", "--ignore-scripts", ]); const npmScriptDispatchScopeOptions = new Set([ "--prefix", "--workspace", "--workspaces", ]); const npmDispatchScopeEnvironmentNames = new Set([ "npm_config_globalconfig", "npm_config_prefix", "npm_config_userconfig", "npm_config_workspace", "npm_config_workspaces", ]); const npmIndirectConfigAuthorityOptions = new Set([ "--globalconfig", "--userconfig", ]); const manifestScopeOptions: Readonly>> = { pnpm: new Set(["-C", "--dir", "--filter", "--workspace-dir"]), npm: new Set(["--prefix", "--workspace"]), yarn: new Set(["--cwd"]), }; const lifecycleMutationCommands = new Set([ "add", "ci", "dedupe", "i", "install", "link", "pack", "prune", "publish", "rebuild", "remove", "rm", "uninstall", "unlink", "up", "update", "upgrade", ]); const managerBuiltinAliases: Readonly< Record> > = { pnpm: new Map([["ln", "link"]]), npm: new Map(), yarn: new Map(), }; const unsupportedBuiltinDispatchers: Readonly< Record> > = { pnpm: new Set(["dlx", "exec"]), npm: new Set(["exec"]), yarn: new Set(["dlx", "exec", "workspace", "workspaces"]), }; const lifecycleBooleanOptions: Readonly< Record> > = { pnpm: new Set([ "--dry-run", "--force", "--frozen-lockfile", "--lockfile-only", "--no-optional", "--prefer-frozen-lockfile", "--recursive", "--workspace-root", "-D", "-P", "-r", "-w", ]), npm: new Set([ "--audit", "--dry-run", "--force", "--foreground-scripts", "--fund", "--package-lock-only", ]), yarn: new Set([ "--check-cache", "--frozen-lockfile", "--ignore-engines", "--ignore-optional", "--immutable", "--immutable-cache", "--inline-builds", "--no-lockfile", "--non-interactive", "--pure-lockfile", ]), }; const lifecycleOptionsWithValue: Readonly< Record> > = { pnpm: new Set(["--child-concurrency", "--modules-dir", "--reporter"]), npm: new Set(["--include", "--install-strategy", "--omit"]), yarn: new Set(["--mode", "--modules-folder", "--production"]), }; const knownBuiltinCommands: Readonly>> = { pnpm: new Set([ "audit", "config", "deploy", "dlx", "exec", "fetch", "help", "list", "ls", "outdated", "root", "server", "setup", "store", "view", "why", ]), npm: new Set([ "access", "audit", "bugs", "cache", "completion", "config", "diff", "docs", "doctor", "exec", "explore", "fund", "help", "help-search", "hook", "init", "list", "login", "logout", "ls", "org", "outdated", "owner", "ping", "pkg", "prefix", "profile", "query", "repo", "root", "search", "star", "stars", "team", "token", "unstar", "version", "view", "whoami", ]), yarn: new Set([ "cache", "config", "constraints", "dedupe", "dlx", "exec", "help", "info", "npm", "plugin", "set", "stage", "version", "why", ]), }; const exactBareSafeBuiltinCommands: Readonly< Record> > = { pnpm: new Set(["audit"]), npm: new Set(["audit"]), yarn: new Set(), }; const npmImplicitScripts = new Set(["restart", "start", "stop", "test"]); export function validatePackageScriptGraph( scripts: Readonly>, entryScript: string, ): string[] { const failures: string[] = []; const visiting = new Set(); const visited = new Set(); const stack: string[] = []; const visit = (scriptName: string): void => { if (visiting.has(scriptName)) { const start = stack.indexOf(scriptName); failures.push(`package script cycle: ${[...stack.slice(start), scriptName].join(" -> ")}`); return; } if (visited.has(scriptName)) return; const command = scripts[scriptName]; if (command === undefined) { failures.push(`package script missing: ${scriptName}`); return; } visiting.add(scriptName); stack.push(scriptName); if (/\bscripts\/run-ci-gate(?:\.[cm]?[jt]s)?\b/u.test(command)) { failures.push(`${scriptName} must not invoke the CI gate runner`); } const parsed = parseManagerCommands(command, scripts); if (parsed.unsupportedManagerSyntax) { failures.push(`package script manager invocation is not safely parseable: ${scriptName}`); } for (const dependency of parsed.dependencies) { if (dependency === "ci:gate") { failures.push(`${scriptName} must not invoke ci:gate`); } else if (!(dependency in scripts)) { failures.push(`package script missing: ${scriptName} -> ${dependency}`); } else { visit(dependency); } } stack.pop(); visiting.delete(scriptName); visited.add(scriptName); }; visit(entryScript); return [...new Set(failures)]; } export function validateInstallScriptPolicy( scripts: Readonly>, entryScripts: readonly string[], ): string[] { const failures: string[] = []; const visited = new Set(); const visit = (scriptName: string): void => { if (visited.has(scriptName)) return; visited.add(scriptName); const command = scripts[scriptName]; if (command === undefined) { failures.push(`package script missing: ${scriptName}`); return; } const parsed = parseManagerCommands(command, scripts); if (parsed.unsafeLifecycle || parsed.unsupportedManagerSyntax) { failures.push( `install-bearing package script must use --ignore-scripts: ${scriptName}`, ); } for (const dependency of parsed.dependencies) { if (dependency !== "ci:gate") visit(dependency); } }; for (const entryScript of entryScripts) visit(entryScript); return [...new Set(failures)]; } export function validateNpmScopeEnvironment( environment: Readonly>, ): string[] { return Object.keys(environment) .filter((name) => npmDispatchScopeEnvironmentNames.has(name.toLowerCase())) .map((name) => `npm scope environment is not allowed: ${name}`); } function parseManagerCommands( command: string, scripts: Readonly>, ): ManagerParseResult { const tokenized = tokenizeShellSegments(command); const dependencies: string[] = []; let unsafeLifecycle = false; let unsupportedManagerSyntax = false; if (!tokenized) { return Object.freeze({ dependencies: Object.freeze([]), unsafeLifecycle: false, unsupportedManagerSyntax: containsManagerReference(command), }); } unsupportedManagerSyntax ||= tokenized.unsupportedControl && containsManagerReference(command); const npmScopeEnvironmentState: ShellNpmScopeEnvironmentState = { autoExport: false, forbidden: false, uncertain: false, }; for (const segment of tokenized.segments) { const { tokens, expansionTokens } = segment; updateShellNpmScopeEnvironmentState(segment, npmScopeEnvironmentState); for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index]!; if (token === "corepack") { if (hasUnsafeManagerCommandPrefix(tokens, expansionTokens, index)) { unsupportedManagerSyntax = true; break; } const wrapped = tokens[index + 1]; if (!wrapped || !isPackageManager(wrapped)) { unsupportedManagerSyntax = true; break; } const parsed = parseManagerInvocation( wrapped, tokens, expansionTokens, index + 2, scripts, hasUnsafeNpmScopeEnvironment( npmScopeEnvironmentState, tokens, expansionTokens, index, ), ); dependencies.push(...parsed.dependencies); unsafeLifecycle ||= parsed.unsafeLifecycle; unsupportedManagerSyntax ||= parsed.unsupportedManagerSyntax; break; } if (isPackageManager(token)) { if (hasUnsafeManagerCommandPrefix(tokens, expansionTokens, index)) { unsupportedManagerSyntax = true; break; } const parsed = parseManagerInvocation( token, tokens, expansionTokens, index + 1, scripts, hasUnsafeNpmScopeEnvironment( npmScopeEnvironmentState, tokens, expansionTokens, index, ), ); dependencies.push(...parsed.dependencies); unsafeLifecycle ||= parsed.unsafeLifecycle; unsupportedManagerSyntax ||= parsed.unsupportedManagerSyntax; break; } if (containsManagerReference(token)) { unsupportedManagerSyntax = true; break; } } } return Object.freeze({ dependencies: Object.freeze([...new Set(dependencies)]), unsafeLifecycle, unsupportedManagerSyntax, }); } function parseManagerInvocation( manager: PackageManager, tokens: readonly string[], expansionTokens: readonly boolean[], start: number, scripts: Readonly>, hasNpmScopeEnvironment: boolean, ): ManagerParseResult { let cursor = start; let changesManifestScope = false; let hasNpmConfigAuthority = false; let consumedManagerSyntax = false; const suppression: SuppressionState = { effective: undefined, contradictory: false, malformed: false, }; while (cursor < tokens.length && tokens[cursor]!.startsWith("-")) { consumedManagerSyntax = true; const option = tokens[cursor]!; const parsedSuppression = consumeSuppressionOption( manager, tokens, cursor, suppression, ); if (parsedSuppression.recognized) { if (parsedSuppression.unsupported) return unsupportedResult(); cursor = parsedSuppression.nextIndex; continue; } const equals = option.indexOf("="); const name = equals < 0 ? option : option.slice(0, equals); if (managerOptionsWithValue.has(name)) { changesManifestScope ||= manifestScopeOptions[manager].has(name); hasNpmConfigAuthority ||= manager === "npm" && npmIndirectConfigAuthorityOptions.has(name); if (equals >= 0) { if (option.slice(equals + 1).length === 0) return unsupportedResult(); } else { cursor += 1; if (cursor >= tokens.length || tokens[cursor]!.startsWith("-")) { return unsupportedResult(); } } } else if (managerBooleanOptions.has(name)) { if (equals >= 0 && !/^(?:true|false)$/u.test(option.slice(equals + 1))) { return unsupportedResult(); } } else if (option !== "--") { return unsupportedResult(); } cursor += 1; } const subcommand = tokens[cursor]; if (!subcommand) return unsupportedResult(); if (manager === "npm" && (hasNpmScopeEnvironment || hasNpmConfigAuthority)) { return unsupportedResult(); } const argumentsAfterCommand = tokens.slice(cursor + 1); if (subcommand === "run" || subcommand === "run-script") { const dependency = argumentsAfterCommand[0]; if (!dependency || dependency.startsWith("-")) return unsupportedResult(); if (changesManifestScope) return unsupportedResult(); if ( manager === "npm" && (expansionTokens.slice(start, cursor + 2).some(Boolean) || !areNpmScriptDispatchArgumentsSupported( argumentsAfterCommand.slice(1), expansionTokens.slice(cursor + 2), suppression, )) ) { return unsupportedResult(); } return manager === "npm" ? npmScriptDependencyResult(dependency, scripts, suppression) : dependencyResult(dependency); } const canonicalSubcommand = managerBuiltinAliases[manager].get(subcommand) ?? subcommand; if (unsupportedBuiltinDispatchers[manager].has(canonicalSubcommand)) { return unsupportedResult(); } if (lifecycleMutationCommands.has(canonicalSubcommand)) { const lifecycleArgumentsSupported = parseLifecycleArguments( manager, argumentsAfterCommand, suppression, ); return Object.freeze({ dependencies: Object.freeze([]), unsafeLifecycle: !lifecycleArgumentsSupported || !hasEffectiveLifecycleSuppression(suppression), unsupportedManagerSyntax: !lifecycleArgumentsSupported, }); } if (knownBuiltinCommands[manager].has(canonicalSubcommand)) { return exactBareSafeBuiltinCommands[manager].has(canonicalSubcommand) && !consumedManagerSyntax && argumentsAfterCommand.length === 0 ? emptyResult() : unsupportedResult(); } const isKnownRootScript = Object.prototype.hasOwnProperty.call(scripts, subcommand); const supportsImplicit = /^[A-Za-z0-9:_-]+$/u.test(subcommand) && ( ((manager === "pnpm" || manager === "yarn") && isKnownRootScript) || (manager === "npm" && npmImplicitScripts.has(subcommand)) ); if (supportsImplicit) { if (changesManifestScope) return unsupportedResult(); if ( manager === "npm" && (expansionTokens.slice(start, cursor + 1).some(Boolean) || !areNpmScriptDispatchArgumentsSupported( argumentsAfterCommand, expansionTokens.slice(cursor + 1), suppression, )) ) { return unsupportedResult(); } return manager === "npm" ? npmScriptDependencyResult(subcommand, scripts, suppression) : dependencyResult(subcommand); } return unsupportedResult(); } function areNpmScriptDispatchArgumentsSupported( tokens: readonly string[], expansionTokens: readonly boolean[], suppression: SuppressionState, ): boolean { let index = 0; while (index < tokens.length) { const token = tokens[index]!; if (token === "--") return true; if (expansionTokens[index]) return false; if (!token.startsWith("-")) { index += 1; continue; } const parsedSuppression = consumeSuppressionOption( "npm", tokens, index, suppression, ); if (parsedSuppression.recognized) { if (parsedSuppression.unsupported) return false; index = parsedSuppression.nextIndex; continue; } const equals = token.indexOf("="); const name = equals < 0 ? token : token.slice(0, equals); const isShortWorkspaceOption = token === "-w" || /^-w(?:=)?.+/u.test(token); if (npmScriptDispatchScopeOptions.has(name) || isShortWorkspaceOption) { return false; } if (!npmScriptDispatchBooleanOptions.has(name)) return false; if (equals >= 0 && !/^(?:true|false)$/u.test(token.slice(equals + 1))) { return false; } index += 1; } return true; } function hasUnsafeNpmScopeEnvironment( state: Readonly, tokens: readonly string[], expansionTokens: readonly boolean[], commandIndex: number, ): boolean { return state.forbidden || state.uncertain || hasUnsafeImmediateNpmScopeEnvironment(tokens, expansionTokens, commandIndex); } function hasUnsafeManagerCommandPrefix( tokens: readonly string[], expansionTokens: readonly boolean[], commandIndex: number, ): boolean { const prefix = parseShellCommandPrefix(tokens, expansionTokens); if (prefix.uncertain) return true; let cursor = prefix.commandIndex; if (cursor === commandIndex) return false; if (cursor > commandIndex || !isEnvironmentCommand(tokens[cursor], expansionTokens[cursor] ?? false)) { return true; } return parseEnvironmentCommandPrefix( tokens, expansionTokens, cursor, commandIndex, ).uncertain; } function hasUnsafeImmediateNpmScopeEnvironment( tokens: readonly string[], expansionTokens: readonly boolean[], commandIndex: number, ): boolean { const prefix = parseShellCommandPrefix(tokens, expansionTokens); if (prefix.uncertain || prefix.assignments.some( ({ name }) => name !== null && isNpmScopeEnvironmentName(name), )) return true; let cursor = prefix.commandIndex; if (cursor === commandIndex) return false; if (!isEnvironmentCommand(tokens[cursor], expansionTokens[cursor] ?? false)) return false; const environmentPrefix = parseEnvironmentCommandPrefix( tokens, expansionTokens, cursor, commandIndex, ); return environmentPrefix.uncertain || environmentPrefix.assignmentNames.some( (name) => isNpmScopeEnvironmentName(name), ); } function parseEnvironmentCommandPrefix( tokens: readonly string[], expansionTokens: readonly boolean[], start: number, commandIndex: number, ): EnvironmentCommandPrefix { const assignmentNames: string[] = []; let cursor = start + 1; let uncertain = false; while (cursor < commandIndex && tokens[cursor]!.startsWith("-")) { const option = tokens[cursor]!; if (expansionTokens[cursor]) uncertain = true; if (option === "--") { cursor += 1; break; } if (option === "-i" || option === "--ignore-environment") { cursor += 1; continue; } if (option === "-u" || option === "--unset") { cursor += 1; if (cursor >= commandIndex || tokens[cursor]!.startsWith("-")) { uncertain = true; break; } uncertain ||= expansionTokens[cursor] ?? false; cursor += 1; continue; } if (/^--unset=.+/u.test(option)) { cursor += 1; continue; } uncertain = true; cursor += 1; } while (cursor < commandIndex) { const token = tokens[cursor]!; if (hasDynamicAssignmentName(token, expansionTokens[cursor] ?? false)) { uncertain = true; } const assignmentName = parseEnvironmentAssignmentName(token); if (assignmentName) assignmentNames.push(assignmentName); else uncertain = true; cursor += 1; } return Object.freeze({ assignmentNames: Object.freeze(assignmentNames), uncertain, }); } function parseShellCommandPrefix( tokens: readonly string[], expansionTokens: readonly boolean[], ): ShellCommandPrefix { const assignments: Array<{ dynamicName: boolean; name: string | null; }> = []; let cursor = 0; let uncertain = false; while (cursor < tokens.length) { const token = tokens[cursor]!; const name = parseAssignmentName(token); const dynamicName = hasDynamicAssignmentName( token, expansionTokens[cursor] ?? false, ); if (!name && !dynamicName) break; assignments.push({ dynamicName, name }); uncertain ||= dynamicName; cursor += 1; } while (cursor < tokens.length) { const wrapper = tokens[cursor]; if (expansionTokens[cursor]) { uncertain = true; break; } if (wrapper !== "command" && wrapper !== "exec") break; cursor += 1; while (cursor < tokens.length && tokens[cursor]!.startsWith("-")) { const option = tokens[cursor]!; if (option === "--") { cursor += 1; break; } if (wrapper === "command" && option === "-p") { cursor += 1; continue; } uncertain = true; cursor += 1; if (wrapper === "exec" && option === "-a" && cursor < tokens.length) { cursor += 1; } } } if (expansionTokens[cursor]) uncertain = true; return Object.freeze({ assignments: Object.freeze(assignments.map((assignment) => Object.freeze(assignment))), commandIndex: cursor, uncertain, }); } function updateShellNpmScopeEnvironmentState( segment: TokenizedShellSegment, state: ShellNpmScopeEnvironmentState, ): void { const { tokens, expansionTokens } = segment; const prefix = parseShellCommandPrefix(tokens, expansionTokens); state.uncertain ||= prefix.uncertain; const command = tokens[prefix.commandIndex]; if (!command) { for (const assignment of prefix.assignments) { if (state.autoExport && assignment.name && isNpmScopeEnvironmentName(assignment.name)) { state.forbidden = true; } } return; } if (command === "eval" || command === "." || command === "source") { state.uncertain = true; return; } if (command === "unset" || command === "typeset" || command === "declare" || command === "local" || command === "readonly") { state.uncertain = true; return; } if (command === "set") { if (tokens.slice(prefix.commandIndex + 1).includes("-a")) state.autoExport = true; if (tokens.slice(prefix.commandIndex + 1).includes("+a")) { state.autoExport = false; state.uncertain = true; } return; } if (command === "export") { let cursor = prefix.commandIndex + 1; for (; cursor < tokens.length; cursor += 1) { const token = tokens[cursor]!; if (token === "--") continue; if (token === "-n" || token.startsWith("-")) { state.uncertain = true; continue; } const assignmentName = parseAssignmentName(token); const bareName = /^[A-Za-z_][A-Za-z0-9_]*$/u.test(token) ? token : null; if (assignmentName || bareName) { if (isNpmScopeEnvironmentName(assignmentName ?? bareName!)) { state.forbidden = true; } } else if (hasDynamicAssignmentName(token, expansionTokens[cursor] ?? false) || expansionTokens[cursor]) { state.uncertain = true; } } return; } } function isEnvironmentCommand(token: string | undefined, hasExpansion: boolean): boolean { if (!token || hasExpansion) return false; return token.split("/").at(-1) === "env"; } function parseEnvironmentAssignmentName(token: string): string | null { const equals = token.indexOf("="); return equals > 0 ? token.slice(0, equals) : null; } function isNpmScopeEnvironmentName(name: string): boolean { return npmDispatchScopeEnvironmentNames.has(name.toLowerCase()); } function hasDynamicAssignmentName(token: string, hasExpansion: boolean): boolean { const equals = token.indexOf("="); return hasExpansion && equals > 0 && parseAssignmentName(token) === null; } function parseAssignmentName(token: string): string | null { return /^([A-Za-z_][A-Za-z0-9_]*)=/u.exec(token)?.[1] ?? null; } function parseLifecycleArguments( manager: PackageManager, tokens: readonly string[], suppression: SuppressionState, ): boolean { let cursor = 0; while (cursor < tokens.length) { const token = tokens[cursor]!; if (!token.startsWith("-")) { cursor += 1; continue; } const parsedSuppression = consumeSuppressionOption( manager, tokens, cursor, suppression, ); if (parsedSuppression.recognized) { if (parsedSuppression.unsupported) return false; cursor = parsedSuppression.nextIndex; continue; } const parsedOption = consumeAllowedLifecycleOption(manager, tokens, cursor); if (parsedOption === null) return false; cursor = parsedOption; } return true; } function consumeSuppressionOption( manager: PackageManager, tokens: readonly string[], index: number, state: SuppressionState, ): Readonly<{ recognized: boolean; unsupported: boolean; nextIndex: number }> { const token = tokens[index]!; if (token === "--no-ignore-scripts") { recordSuppression(state, false); return { recognized: true, unsupported: false, nextIndex: index + 1 }; } const equalsForms = ["--ignore-scripts=", "--config.ignore-scripts="] as const; for (const prefix of equalsForms) { if (!token.startsWith(prefix)) continue; if (prefix.startsWith("--config.") && manager !== "pnpm") { state.malformed = true; return { recognized: true, unsupported: true, nextIndex: index + 1 }; } const raw = token.slice(prefix.length); if (raw !== "true" && raw !== "false") { state.malformed = true; return { recognized: true, unsupported: true, nextIndex: index + 1 }; } recordSuppression(state, raw === "true"); return { recognized: true, unsupported: false, nextIndex: index + 1 }; } if (token !== "--ignore-scripts" && token !== "--config.ignore-scripts") { return { recognized: false, unsupported: false, nextIndex: index }; } if (token === "--config.ignore-scripts" && manager !== "pnpm") { state.malformed = true; return { recognized: true, unsupported: true, nextIndex: index + 1 }; } const next = tokens[index + 1]; if (next === "true" || next === "false") { const supportsSplitValue = manager === "npm" || manager === "pnpm"; if (!supportsSplitValue) { state.malformed = true; return { recognized: true, unsupported: true, nextIndex: index + 2 }; } recordSuppression(state, next === "true"); return { recognized: true, unsupported: false, nextIndex: index + 2 }; } recordSuppression(state, true); return { recognized: true, unsupported: false, nextIndex: index + 1 }; } function recordSuppression( state: SuppressionState, value: boolean, ): void { if (state.effective !== undefined && state.effective !== value) { state.contradictory = true; } state.effective = value; } function hasEffectiveLifecycleSuppression(state: SuppressionState): boolean { return state.effective === true && !state.contradictory && !state.malformed; } function consumeAllowedLifecycleOption( manager: PackageManager, tokens: readonly string[], index: number, ): number | null { const option = tokens[index]!; const equals = option.indexOf("="); const name = equals < 0 ? option : option.slice(0, equals); const booleanOption = managerBooleanOptions.has(name) || lifecycleBooleanOptions[manager].has(name); if (booleanOption) { if (equals >= 0 && !/^(?:true|false)$/u.test(option.slice(equals + 1))) return null; return index + 1; } const valuedOption = managerOptionsWithValue.has(name) || lifecycleOptionsWithValue[manager].has(name); if (!valuedOption) return null; if (equals >= 0) return option.slice(equals + 1).length > 0 ? index + 1 : null; const value = tokens[index + 1]; if (!value || value.startsWith("-")) return null; return index + 2; } function dependencyResult(dependency: string): ManagerParseResult { return dependenciesResult([dependency]); } function npmScriptDependencyResult( dependency: string, scripts: Readonly>, suppression: SuppressionState, ): ManagerParseResult { if (hasEffectiveLifecycleSuppression(suppression)) { return dependenciesResult([dependency]); } return dependenciesResult( [`pre${dependency}`, dependency, `post${dependency}`] .filter((scriptName) => scriptName === dependency || scriptName in scripts), ); } function dependenciesResult(dependencies: readonly string[]): ManagerParseResult { return Object.freeze({ dependencies: Object.freeze([...dependencies]), unsafeLifecycle: false, unsupportedManagerSyntax: false, }); } function emptyResult(): ManagerParseResult { return Object.freeze({ dependencies: Object.freeze([]), unsafeLifecycle: false, unsupportedManagerSyntax: false, }); } function unsupportedResult(): ManagerParseResult { return Object.freeze({ dependencies: Object.freeze([]), unsafeLifecycle: false, unsupportedManagerSyntax: true, }); } function isPackageManager(value: string): value is PackageManager { return managerNames.has(value as PackageManager); } function containsManagerReference(value: string): boolean { return /(?:^|[^A-Za-z0-9_-])(?:corepack|pnpm|npm|yarn)(?:[^A-Za-z0-9_-]|$)/u .test(value); } function tokenizeShellSegments(command: string): Readonly<{ segments: readonly TokenizedShellSegment[]; unsupportedControl: boolean; }> | null { const segments: Array<{ tokens: string[]; expansionTokens: boolean[] }> = [ { tokens: [], expansionTokens: [] }, ]; let token = ""; let tokenHasExpansion = false; let quote: "'" | '"' | null = null; let escaping = false; let unsupportedControl = false; const pushToken = (): void => { if (token.length > 0) { segments.at(-1)!.tokens.push(token); segments.at(-1)!.expansionTokens.push(tokenHasExpansion); } token = ""; tokenHasExpansion = false; }; const pushSegment = (): void => { pushToken(); if (segments.at(-1)!.tokens.length > 0) { segments.push({ tokens: [], expansionTokens: [] }); } }; for (let index = 0; index < command.length; index += 1) { const character = command[index]!; if (escaping) { token += character; escaping = false; continue; } if (character === "\\" && quote !== "'") { escaping = true; continue; } if (quote) { if (character === quote) quote = null; else { if (quote === '"' && character === "$") tokenHasExpansion = true; token += character; } continue; } if (character === "'" || character === '"') { quote = character; continue; } if (character === "`" || (character === "$" && command[index + 1] === "(")) { unsupportedControl = true; if (character === "$") tokenHasExpansion = true; token += character; continue; } if (character === "$" || character === "*" || character === "?" || character === "[") { tokenHasExpansion = true; } if (character === "#") { unsupportedControl = true; pushToken(); while ( index + 1 < command.length && command[index + 1] !== "\n" && command[index + 1] !== "\r" ) { index += 1; } continue; } if (character === "<" || character === ">" || character === "(" || character === ")") { unsupportedControl = true; token += character; continue; } if (/\s/u.test(character)) { pushToken(); if (character === "\n" || character === "\r") pushSegment(); continue; } if (character === ";" || character === "|" || character === "&") { pushSegment(); if (command[index + 1] === character) index += 1; continue; } token += character; } if (quote || escaping) return null; pushToken(); return Object.freeze({ segments: Object.freeze( segments .filter((segment) => segment.tokens.length > 0) .map((segment) => Object.freeze({ tokens: Object.freeze(segment.tokens), expansionTokens: Object.freeze(segment.expansionTokens), })), ), unsupportedControl, }); }