mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
fix(worktrees): stop silently switching existing Windows setup scripts to Git Bash
#6967 derived the Windows setup-runner shell from `terminalWindowsShell`. On upgrade, any Windows user whose terminal preference resolved to Git Bash had their existing `orca.yaml` setup script (and issue command) handed to bash instead of cmd.exe. Scripts authored against the cmd runner — `copy`, `xcopy`, `set VAR=value`, `if errorlevel 1`, `%VAR%`, backslash paths — broke with no migration and no warning, and the failure looked like Orca broke the project. The conflation is also wrong in the steady state: a terminal preference is per-user, so two people on the same repo got different interpreters for the same orca.yaml and no project could write a setup script that worked for all of its Windows contributors. The interpreter is now a property of the script, declared the standard way: a leading `#!` line. Native Windows keeps the historical `.cmd` runner unless the script declares a POSIX shell, so no existing script changes behavior. `resolveSetupRunnerShell` keeps its role as the feasibility gate — a bash runner still requires the terminal to resolve to Git Bash, since the launch command is typed into that shell and uses MSYS `/c/...` paths. `buildWindowsRunnerScript` now drops a leading `#!` line rather than `call`ing it, so a declared-bash script that falls back to cmd (Git Bash missing) fails on a real setup line instead of aborting on errorlevel at line one. WSL worktrees, POSIX platforms, and SSH hosts are untouched.
This commit is contained in:
@@ -100,6 +100,7 @@ docs/**
|
||||
!docs/reference/headless-linux-server.md
|
||||
!docs/reference/linux-glibc-compatibility.md
|
||||
!docs/reference/relay-grace-time-reconfiguration.md
|
||||
!docs/reference/windows-setup-shell.md
|
||||
|
||||
# Stably CLI (only docs/ are tracked)
|
||||
.stably/*
|
||||
|
||||
@@ -29,6 +29,7 @@ Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior beh
|
||||
- **Keyboard shortcuts**: Never hardcode `e.metaKey`. Use a platform check (`navigator.userAgent.includes('Mac')`) to pick `metaKey` on Mac and `ctrlKey` on Linux/Windows. Electron menu accelerators should use `CmdOrCtrl`.
|
||||
- **Shortcut labels in UI**: Display `⌘` / `⇧` on Mac and `Ctrl+` / `Shift+` on other platforms.
|
||||
- **File paths**: Use `path.join` or Electron/Node path utilities — never assume `/` or `\`.
|
||||
- **Windows setup scripts**: the setup/issue-command runner is a `.cmd` batch file unless the script starts with a `#!` line. Never derive it from the user's terminal-shell preference. See [`docs/reference/windows-setup-shell.md`](./docs/reference/windows-setup-shell.md).
|
||||
- **Linux native modules**: keep the glibc floor at Ubuntu 20.04 / glibc 2.31. A module compiled from source on a newer runner can reference symbol versions absent on the floor and crash the app on startup. See [`docs/reference/linux-glibc-compatibility.md`](./docs/reference/linux-glibc-compatibility.md); packaging fails if a bundled native binary needs newer glibc.
|
||||
|
||||
## SSH Use Case
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Windows setup-runner shell
|
||||
|
||||
On native Windows, Orca writes the `orca.yaml` setup script (and the issue command) to a generated
|
||||
runner file and types a launch command into a terminal. The runner is a **`.cmd` batch file by
|
||||
default**, exactly as it has been since setup hooks shipped.
|
||||
|
||||
A script opts into bash by starting with a `#!` interpreter line:
|
||||
|
||||
```yaml
|
||||
scripts:
|
||||
setup: |
|
||||
#!/usr/bin/env bash
|
||||
[ -f .env ] || cp .env.example .env
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Without that line the script keeps running under `cmd.exe`:
|
||||
|
||||
```yaml
|
||||
scripts:
|
||||
setup: |
|
||||
copy .env.example .env
|
||||
xcopy /E assets dist
|
||||
```
|
||||
|
||||
## Why the script declares it, not the terminal preference
|
||||
|
||||
`terminalWindowsShell` says which shell *interactive terminals* open in. It says nothing about the
|
||||
language a project's setup script is written in. Deriving the runner from it had two consequences:
|
||||
|
||||
- Windows users with batch-syntax setup scripts silently switched to bash on upgrade, so `copy`,
|
||||
`xcopy`, `set VAR=value`, and `if errorlevel 1` stopped working.
|
||||
- Two people on the same repo got different interpreters for the same `orca.yaml`, so no project
|
||||
could write a setup script that worked for all of its Windows contributors.
|
||||
|
||||
A `#!` line is per-project, explicit, and identical for everyone who checks the repo out.
|
||||
|
||||
## Requirements for the bash runner
|
||||
|
||||
A `#!` line only takes effect when Orca can actually launch bash from the configured terminal — the
|
||||
terminal shell must resolve to Git Bash (`resolveWindowsGitBashShellPath`). The generated runner
|
||||
uses MSYS `/c/...` paths, which Cygwin and the WSL shim do not accept, and the launch command is
|
||||
typed into whatever shell the terminal opened with. When Git Bash is not available the script falls
|
||||
back to the `.cmd` runner; the `#!` line is dropped rather than executed.
|
||||
|
||||
WSL worktrees and non-Windows platforms are unaffected: they always use the bash runner. SSH hosts
|
||||
choose their runner from the remote path format, never from local Windows preferences.
|
||||
@@ -94,7 +94,7 @@ describe('createSetupRunnerScript', () => {
|
||||
const result = createSetupRunnerScript(
|
||||
{ ...makeRepo(), path: 'C:\\Users\\jinwo\\git\\orca' },
|
||||
'C:\\repo\\feature',
|
||||
'pnpm install',
|
||||
'#!/usr/bin/env bash\npnpm install',
|
||||
undefined,
|
||||
{ family: 'posix' }
|
||||
)
|
||||
@@ -113,7 +113,7 @@ describe('createSetupRunnerScript', () => {
|
||||
})
|
||||
expect(vi.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
||||
'C:\\repo\\.git\\worktrees\\feature\\orca\\setup-runner.sh',
|
||||
'#!/usr/bin/env bash\nset -e\npnpm install\n',
|
||||
'#!/usr/bin/env bash\nset -e\n#!/usr/bin/env bash\npnpm install\n',
|
||||
'utf-8'
|
||||
)
|
||||
// Why: chmod over a native Windows path is meaningless; only the WSL branch sets the bit.
|
||||
@@ -186,6 +186,17 @@ describe('createSetupRunnerScript', () => {
|
||||
expect(runner).toContain('call echo hello!world!')
|
||||
})
|
||||
|
||||
it('drops a leading shebang instead of calling it as a batch command', async () => {
|
||||
const { buildWindowsRunnerScript } = await import('./hooks')
|
||||
|
||||
// Why: a shebang script falls back here when Git Bash is unavailable; `call #!/...`
|
||||
// would abort on errorlevel before any real setup line ran.
|
||||
const runner = buildWindowsRunnerScript('#!/usr/bin/env bash\npnpm install')
|
||||
|
||||
expect(runner).not.toContain('#!')
|
||||
expect(runner).toContain('call pnpm install')
|
||||
})
|
||||
|
||||
it('derives ORCA_WORKSPACE_NAME from a POSIX worktree path', async () => {
|
||||
const originalPlatform = process.platform
|
||||
|
||||
|
||||
+76
-11
@@ -1255,7 +1255,7 @@ describe('createSetupRunnerScript', () => {
|
||||
}
|
||||
}) as unknown as Repo
|
||||
|
||||
it('writes POSIX setup runners for Git Bash on native Windows paths', async () => {
|
||||
it('writes POSIX setup runners for shebang-declared scripts on native Windows paths', async () => {
|
||||
gitExecFileSyncMock.mockReset()
|
||||
gitExecFileSyncMock.mockReturnValue('C:\\repo\\.git\\orca\\setup-runner.sh\n')
|
||||
const fs = await import('node:fs')
|
||||
@@ -1271,7 +1271,7 @@ describe('createSetupRunnerScript', () => {
|
||||
const result = createSetupRunnerScript(
|
||||
makeRepo(),
|
||||
'C:\\repo-worktree',
|
||||
'pnpm install\r\nnpm run build',
|
||||
'#!/usr/bin/env bash\r\npnpm install\r\nnpm run build',
|
||||
undefined,
|
||||
{ family: 'posix' }
|
||||
)
|
||||
@@ -1282,7 +1282,7 @@ describe('createSetupRunnerScript', () => {
|
||||
)
|
||||
expect(writeFileSyncMock).toHaveBeenCalledWith(
|
||||
'C:\\repo\\.git\\orca\\setup-runner.sh',
|
||||
'#!/usr/bin/env bash\nset -e\npnpm install\nnpm run build\n',
|
||||
'#!/usr/bin/env bash\nset -e\n#!/usr/bin/env bash\npnpm install\nnpm run build\n',
|
||||
'utf-8'
|
||||
)
|
||||
expect(chmodSyncMock).not.toHaveBeenCalled()
|
||||
@@ -1295,6 +1295,45 @@ describe('createSetupRunnerScript', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps batch setup scripts on cmd.exe when the terminal is Git Bash', async () => {
|
||||
// Regression (#6967): a Git Bash terminal preference used to hand pre-existing
|
||||
// batch setup scripts to bash, where `copy`/`xcopy`/`if errorlevel` do not exist.
|
||||
gitExecFileSyncMock.mockReset()
|
||||
gitExecFileSyncMock.mockReturnValue('C:\\repo\\.git\\orca\\setup-runner.cmd\n')
|
||||
const fs = await import('node:fs')
|
||||
const writeFileSyncMock = vi.mocked(fs.writeFileSync)
|
||||
writeFileSyncMock.mockClear()
|
||||
const originalPlatform = process.platform
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
|
||||
try {
|
||||
const { createSetupRunnerScript } = await import('./hooks')
|
||||
const result = createSetupRunnerScript(
|
||||
makeRepo(),
|
||||
'C:\\repo-worktree',
|
||||
'copy .env.example .env\r\nxcopy /E assets dist',
|
||||
undefined,
|
||||
{ family: 'posix' }
|
||||
)
|
||||
|
||||
expect(gitExecFileSyncMock).toHaveBeenCalledWith(
|
||||
['rev-parse', '--git-path', 'orca/setup-runner.cmd'],
|
||||
{ cwd: 'C:\\repo-worktree' }
|
||||
)
|
||||
expect(writeFileSyncMock).toHaveBeenCalledWith(
|
||||
'C:\\repo\\.git\\orca\\setup-runner.cmd',
|
||||
expect.stringContaining('call copy .env.example .env\r\n'),
|
||||
'utf-8'
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.cmd',
|
||||
shell: { family: 'cmd' }
|
||||
})
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves cmd.exe setup runner semantics for configured cmd users', async () => {
|
||||
gitExecFileSyncMock.mockReset()
|
||||
gitExecFileSyncMock.mockReturnValue('C:\\repo\\.git\\orca\\setup-runner.cmd\n')
|
||||
@@ -1409,7 +1448,7 @@ describe('createIssueCommandRunnerScript', () => {
|
||||
hookSettings: { mode: 'auto', scripts: { setup: '', archive: '' } }
|
||||
}) as unknown as Repo
|
||||
|
||||
it('writes a POSIX issue-command runner when setup resolves to Git Bash', async () => {
|
||||
it('writes a POSIX issue-command runner when a shebang declares bash and setup resolves to Git Bash', async () => {
|
||||
gitExecFileSyncMock.mockReset()
|
||||
gitExecFileSyncMock.mockReturnValue('C:\\repo\\.git\\orca\\issue-command-runner.sh\n')
|
||||
const fs = await import('node:fs')
|
||||
@@ -1418,6 +1457,37 @@ describe('createIssueCommandRunnerScript', () => {
|
||||
const originalPlatform = process.platform
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
|
||||
try {
|
||||
const { createIssueCommandRunnerScript } = await import('./hooks')
|
||||
const result = createIssueCommandRunnerScript(
|
||||
makeRepo(),
|
||||
'C:\\repo-worktree',
|
||||
'#!/usr/bin/env bash\ngh issue view 42',
|
||||
undefined,
|
||||
{ family: 'posix' }
|
||||
)
|
||||
|
||||
expect(gitExecFileSyncMock).toHaveBeenCalledWith(
|
||||
['rev-parse', '--git-path', 'orca/issue-command-runner.sh'],
|
||||
{ cwd: 'C:\\repo-worktree' }
|
||||
)
|
||||
expect(writeFileSyncMock).toHaveBeenCalledWith(
|
||||
'C:\\repo\\.git\\orca\\issue-command-runner.sh',
|
||||
'#!/usr/bin/env bash\nset -e\n#!/usr/bin/env bash\ngh issue view 42\n',
|
||||
'utf-8'
|
||||
)
|
||||
expect(result.shell).toEqual({ family: 'posix' })
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps a plain issue command on the cmd runner under a Git Bash terminal', async () => {
|
||||
gitExecFileSyncMock.mockReset()
|
||||
gitExecFileSyncMock.mockReturnValue('C:\\repo\\.git\\orca\\issue-command-runner.cmd\n')
|
||||
const originalPlatform = process.platform
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
|
||||
try {
|
||||
const { createIssueCommandRunnerScript } = await import('./hooks')
|
||||
const result = createIssueCommandRunnerScript(
|
||||
@@ -1429,15 +1499,10 @@ describe('createIssueCommandRunnerScript', () => {
|
||||
)
|
||||
|
||||
expect(gitExecFileSyncMock).toHaveBeenCalledWith(
|
||||
['rev-parse', '--git-path', 'orca/issue-command-runner.sh'],
|
||||
['rev-parse', '--git-path', 'orca/issue-command-runner.cmd'],
|
||||
{ cwd: 'C:\\repo-worktree' }
|
||||
)
|
||||
expect(writeFileSyncMock).toHaveBeenCalledWith(
|
||||
'C:\\repo\\.git\\orca\\issue-command-runner.sh',
|
||||
'#!/usr/bin/env bash\nset -e\ngh issue view 42\n',
|
||||
'utf-8'
|
||||
)
|
||||
expect(result.shell).toEqual({ family: 'posix' })
|
||||
expect(result.shell).toEqual({ family: 'cmd' })
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
|
||||
}
|
||||
|
||||
+21
-8
@@ -9,6 +9,7 @@ import { shouldWaitForSetupBeforeAgentStartup } from '../shared/setup-agent-star
|
||||
import { TERMINAL_GIT_CREDENTIAL_GUARD_POLICY_ENV } from '../shared/terminal-git-credential-guard'
|
||||
import { parseOrcaYaml } from '../shared/orca-yaml'
|
||||
import { nativeWindowsPathToPosixShellPath } from '../shared/setup-runner-command'
|
||||
import { isShebangLine, scriptDeclaresPosixShell } from '../shared/setup-script-shebang'
|
||||
import { resolveWindowsShellStartupFamily } from '../shared/windows-terminal-shell'
|
||||
import { resolveWindowsGitBashShellPath } from './git-bash'
|
||||
import { gitExecFileSync, promptGuardShellEnv } from './git/runner'
|
||||
@@ -401,9 +402,17 @@ export function buildWindowsRunnerScript(script: string): string {
|
||||
// Why: launchers invoke this runner under `cmd /v:on`, and EnableExtensions does not reset an
|
||||
// inherited delayed-expansion state — without this, every `!` in a user setup line is eaten.
|
||||
let runnerScript = '@echo off\r\nsetlocal EnableExtensions DisableDelayedExpansion\r\n'
|
||||
let isFirstLine = true
|
||||
|
||||
for (const rawLine of iterateLfScriptLines(script)) {
|
||||
const command = rawLine.trim()
|
||||
// Why: a leading `#!` declares an interpreter; `call`ing it would abort the run on errorlevel.
|
||||
if (isFirstLine) {
|
||||
isFirstLine = false
|
||||
if (isShebangLine(command)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (!command) {
|
||||
runnerScript += '\r\n'
|
||||
continue
|
||||
@@ -500,8 +509,8 @@ export function createIssueCommandRunnerScript(
|
||||
script: command,
|
||||
runnerBaseName: 'issue-command-runner',
|
||||
runtimeTarget: getHookRuntimeTarget(projectRuntime),
|
||||
// Why: issue commands run in the same terminal as setup, so a Git Bash setup
|
||||
// runner must not be paired with a cmd issue runner in one session.
|
||||
// Why: issue commands share the setup session's shell resolution; the runner still
|
||||
// needs its own `#!` line to be treated as bash.
|
||||
setupShell
|
||||
})
|
||||
}
|
||||
@@ -528,8 +537,14 @@ function createWorktreeRunnerScript(args: {
|
||||
// Why: WSL worktrees are Linux fs even though process.platform is 'win32'; use bash for WSL, .cmd for native Windows.
|
||||
const wslWorktree = isWslPath(worktreePath) || Boolean(runtimeTarget?.wslDistro)
|
||||
const nativeWindowsWorktree = process.platform === 'win32' && !wslWorktree
|
||||
// Why: the terminal-shell preference says nothing about the language a project's script is
|
||||
// written in, and every pre-existing Windows script was authored against the cmd runner. Only a
|
||||
// `#!` line opts a script into bash, so the same orca.yaml runs identically for every Windows
|
||||
// user of the repo instead of following whichever terminal each of them happens to prefer.
|
||||
const runnerShell: SetupRunnerShell = nativeWindowsWorktree
|
||||
? (setupShell ?? { family: 'cmd' })
|
||||
? setupShell?.family === 'posix' && scriptDeclaresPosixShell(script)
|
||||
? setupShell
|
||||
: { family: 'cmd' }
|
||||
: { family: 'posix' }
|
||||
const launchShell: SetupRunnerShell | undefined = nativeWindowsWorktree
|
||||
? runnerShell
|
||||
@@ -583,8 +598,7 @@ function createWorktreeRunnerScript(args: {
|
||||
envVars,
|
||||
// Why: WSL git returns /mnt paths that Node converts back to C:\ for file
|
||||
// writes; retain the runtime signal so launch converts them to /mnt again.
|
||||
// Issue-command runners take the same resolved shell, so one session never
|
||||
// mixes a bash setup runner with a cmd issue runner.
|
||||
// On native Windows it mirrors the family the runner file was written in.
|
||||
...(launchShell ? { shell: launchShell } : {}),
|
||||
...(waitForAgentStartup === true ? { waitForAgentStartup: true } : {})
|
||||
}
|
||||
@@ -615,9 +629,8 @@ export function resolveSetupRunnerShell(
|
||||
options.resolveGitBashShellPath ??
|
||||
((shell: string) => resolveWindowsGitBashShellPath(shell, { platform }))
|
||||
if (resolveGitBashShellPath(configuredShell)) {
|
||||
// Note: Git Bash users with batch-syntax orca.yaml setup content get a bash
|
||||
// interpreter from here on. The flip is intentional and documented in
|
||||
// docs/reference/windows-setup-shell.md.
|
||||
// Note: this only reports that a bash runner *could* launch here. Whether one is
|
||||
// used is decided per script by its `#!` line — see docs/reference/windows-setup-shell.md.
|
||||
return { family: 'posix' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { isShebangLine, scriptDeclaresPosixShell } from './setup-script-shebang'
|
||||
|
||||
describe('scriptDeclaresPosixShell', () => {
|
||||
it('accepts the common env and absolute-path forms', () => {
|
||||
expect(scriptDeclaresPosixShell('#!/usr/bin/env bash\npnpm install')).toBe(true)
|
||||
expect(scriptDeclaresPosixShell('#!/bin/sh -e\npnpm install')).toBe(true)
|
||||
expect(scriptDeclaresPosixShell('#!/usr/bin/env -S bash -euo pipefail\npnpm install')).toBe(
|
||||
true
|
||||
)
|
||||
expect(scriptDeclaresPosixShell('#!/bin/zsh')).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects scripts with no interpreter line', () => {
|
||||
// Regression: batch-syntax setup scripts must stay on the cmd runner.
|
||||
expect(scriptDeclaresPosixShell('copy .env.example .env\nxcopy /E assets dist')).toBe(false)
|
||||
expect(scriptDeclaresPosixShell('')).toBe(false)
|
||||
expect(scriptDeclaresPosixShell('pnpm install\n#!/usr/bin/env bash')).toBe(false)
|
||||
expect(scriptDeclaresPosixShell('# !/usr/bin/env bash\npnpm install')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects interpreters that are not POSIX shells', () => {
|
||||
expect(scriptDeclaresPosixShell('#!/usr/bin/env node\nconsole.log(1)')).toBe(false)
|
||||
expect(scriptDeclaresPosixShell('#!/usr/bin/env python3\nprint(1)')).toBe(false)
|
||||
})
|
||||
|
||||
it('tolerates CRLF and Windows-style interpreter paths', () => {
|
||||
expect(scriptDeclaresPosixShell('#!/usr/bin/env bash\r\npnpm install')).toBe(true)
|
||||
expect(scriptDeclaresPosixShell('#!C:\\tools\\git\\bin\\bash.exe\r\npnpm install')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isShebangLine', () => {
|
||||
it('detects interpreter lines regardless of leading whitespace', () => {
|
||||
expect(isShebangLine('#!/usr/bin/env bash')).toBe(true)
|
||||
expect(isShebangLine(' #!/bin/sh')).toBe(true)
|
||||
expect(isShebangLine('#comment')).toBe(false)
|
||||
expect(isShebangLine('pnpm install')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
// Why: the interpreter a setup/issue-command script is written for is a property of the script,
|
||||
// not of the user's terminal preference, so a `#!` line is how a project declares it.
|
||||
|
||||
const POSIX_SHELL_BASENAMES = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'ash'])
|
||||
|
||||
/** True when `line` is a `#!` interpreter line (leading whitespace tolerated). */
|
||||
export function isShebangLine(line: string): boolean {
|
||||
return line.trimStart().startsWith('#!')
|
||||
}
|
||||
|
||||
/** True when the script's first line is a `#!` line naming a POSIX shell. */
|
||||
export function scriptDeclaresPosixShell(script: string): boolean {
|
||||
const firstLine = script.split('\n', 1)[0] ?? ''
|
||||
if (!isShebangLine(firstLine)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const tokens = firstLine.trim().slice(2).trim().split(/\s+/).filter(Boolean)
|
||||
const interpreter = shebangInterpreterBasename(tokens)
|
||||
return interpreter !== null && POSIX_SHELL_BASENAMES.has(interpreter)
|
||||
}
|
||||
|
||||
function shebangInterpreterBasename(tokens: string[]): string | null {
|
||||
for (let index = 0; index < tokens.length; index++) {
|
||||
const basename = executableBasename(tokens[index])
|
||||
// Why: `env` (and its `-S` split-string form) only forwards to the real interpreter.
|
||||
if (basename === 'env' || basename.startsWith('-')) {
|
||||
continue
|
||||
}
|
||||
return basename || null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function executableBasename(token: string): string {
|
||||
return (
|
||||
token
|
||||
.replaceAll('\\', '/')
|
||||
.split('/')
|
||||
.pop()
|
||||
?.toLowerCase()
|
||||
.replace(/\.exe$/, '') ?? ''
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user