mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 23:14:33 +01:00
fix(windows): custom-root installs keep their paths; git helpers work with MSYS_NO_PATHCONV=1 (#3498, #3468)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
c830c9b8ba
commit
cc28128d27
14 files changed
+228
-7
No files matched your search
@@ -531,6 +531,8 @@ check_windows_bash() {
|
||||
row fail "windows bash" "$refused" "set GSTACK_GIT_BASH to Git Bash's bash.exe for good (PowerShell user variable or the env block of ~/.claude/settings.json), then restart Claude Code ($DOCS#bash-unavailable)"
|
||||
elif [ -z "$bash" ]; then
|
||||
row warn "windows bash" "not checked: gstack-windows-bash exited $rc ($(printf '%s\n' "$out" | grep -v '^[[:space:]]*$' | head -1 | cut -c1-200))" "cd $SRC && ./setup"
|
||||
elif [ -n "${MSYS_NO_PATHCONV:-}" ]; then
|
||||
row warn "windows bash" "$bash (chosen by $source); MSYS_NO_PATHCONV is set, so gstack's Bun commands cannot read --config=/dev/null and fail before they start (#3468)" "unset MSYS_NO_PATHCONV and set it only on the command that needs it, such as MSYS_NO_PATHCONV=1 docker ... ($DOCS#msys-no-pathconv)"
|
||||
elif [ -n "$warning" ]; then
|
||||
row warn "windows bash" "$bash (chosen by $source); $warning" "unset GSTACK_BASH, or set it to the same path"
|
||||
else
|
||||
|
||||
@@ -130,6 +130,9 @@ HB_PID=$!
|
||||
# ── Inside the lock ──
|
||||
HEAD_OID=$(g rev-parse -q --verify 'HEAD^{commit}' 2>/dev/null) || refuse 2 not-a-checkout "$DIR has no HEAD commit"
|
||||
TMP=$(mktemp -d "${TMPDIR:-/tmp}/gstack-ff-XXXXXX" 2>/dev/null) || refuse 3 autostash-unrecoverable "cannot create a temp directory under ${TMPDIR:-/tmp}; nothing was changed"
|
||||
# Its index files go to Git for Windows, which cannot open /tmp/... with MSYS
|
||||
# path conversion off (#3468): use the native spelling where cygpath exists.
|
||||
if command -v cygpath >/dev/null 2>&1 && _ff_native=$(cygpath -am "$TMP" 2>/dev/null) && [ -n "$_ff_native" ]; then TMP=$_ff_native; fi
|
||||
g ls-files -u -z > "$TMP/unmerged.raw" 2>"$TMP/err" || refuse 5 index-unreadable "cannot read the index of $DIR ($(head -c 200 "$TMP/err")); nothing was changed"
|
||||
UNMERGED=$(tr '\0' '\n' < "$TMP/unmerged.raw")
|
||||
# Nothing to move: an up-to-date install keeps its edits and staged state as they are.
|
||||
|
||||
@@ -131,9 +131,12 @@ hook_report() {
|
||||
hook_incoming_hold() {
|
||||
_hc_dir=$(mktemp -d "${TMPDIR:-/tmp}/gstack-session-hooks-XXXXXX" 2>/dev/null) \
|
||||
|| { echo "hook-check-unavailable: cannot create a temp directory under ${TMPDIR:-/tmp}"; return 0; }
|
||||
# With MSYS path conversion off, Git for Windows cannot open /tmp/... (#3468).
|
||||
_hc_git=$_hc_dir
|
||||
[ -z "${MSYS_NO_PATHCONV:-}" ] || _hc_git=$(gstack_path_native "$_hc_dir")
|
||||
_hc_out="" _hc_why=""
|
||||
if ! ( export GIT_INDEX_FILE="$_hc_dir.index"
|
||||
git -C "$GSTACK_DIR" read-tree "$1" && git -C "$GSTACK_DIR" -c core.symlinks=false checkout-index -a --prefix="$_hc_dir/" ) 2>"$_hc_dir.err"; then
|
||||
if ! ( export GIT_INDEX_FILE="$_hc_git.index"
|
||||
git -C "$GSTACK_DIR" read-tree "$1" && git -C "$GSTACK_DIR" -c core.symlinks=false checkout-index -a --prefix="$_hc_git/" ) 2>"$_hc_dir.err"; then
|
||||
_hc_why="cannot extract the incoming revision: $(ff_flatten "$(head -c 200 "$_hc_dir.err" 2>/dev/null)" 200)"
|
||||
elif [ -f "$_hc_dir/bin/gstack-hook-check" ] && hook_report "$_hc_dir/bin/gstack-hook-check" "$_hc_dir"; then
|
||||
_hc_out=$_hr_out
|
||||
|
||||
+5
-3
@@ -59,12 +59,14 @@ TMPIDX=$(mktemp "${TMPDIR:-/tmp}/gstack-wtree-XXXXXX" 2>/dev/null) || {
|
||||
}
|
||||
trap 'rm -f "$TMPIDX"' EXIT
|
||||
trap 'exit 130' INT TERM
|
||||
export GIT_INDEX_FILE="$TMPIDX"
|
||||
# shellcheck source=bin/gstack-path-spelling.sh
|
||||
. "$(cd "$(dirname "$0")" && pwd)/gstack-path-spelling.sh"
|
||||
# Git for Windows gets the native spelling: with MSYS path conversion off
|
||||
# (MSYS_NO_PATHCONV=1) it cannot open /tmp/... (#3468).
|
||||
export GIT_INDEX_FILE="$(gstack_path_native "$TMPIDX")"
|
||||
# Resolve relative --git-path output against the repo root. Git for Windows
|
||||
# answers in drive-letter form (C:/…) wherever the path is absolute (a linked
|
||||
# worktree, a submodule, --separate-git-dir); that is absolute too (#3101).
|
||||
# shellcheck source=bin/gstack-path-spelling.sh
|
||||
. "$(cd "$(dirname "$0")" && pwd)/gstack-path-spelling.sh"
|
||||
if [ -n "$REAL_INDEX" ] && ! gstack_path_absolute "$REAL_INDEX"; then REAL_INDEX="$TOP/$REAL_INDEX"; fi
|
||||
if [ -n "$REAL_OBJECTS" ] && ! gstack_path_absolute "$REAL_OBJECTS"; then REAL_OBJECTS="$TOP/$REAL_OBJECTS"; fi
|
||||
if [ -n "$OBJ_DIR" ]; then
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
**Windows custom-root installs keep their own paths, and gstack's git helpers work with `MSYS_NO_PATHCONV=1` exported.** Git Bash hands native Bun a setup install root like `/c/Users/you/isolated/...` as `C:/Users/you/isolated/...`, and the renderer refused any root that did not start with `/`. Setup warned and the isolated install kept the default-install paths (#3498, reported by DanCanadian). With `MSYS_NO_PATHCONV=1` exported (common for docker), Git for Windows received raw `/tmp/...` temp index paths it cannot open, so `gstack-wtree` and every review record built on it failed. `gstack-ff-update` and the session updater's hook check failed the same way (#3468, reported by Simba3696).
|
||||
|
||||
### Fixed
|
||||
- **`scripts/gen-skill-docs.ts --install-root`** accepts a drive-letter root and renders it in the `/c/...` spelling setup checks. A root with characters outside the plain set still refuses.
|
||||
- **`setup`** converts a drive-form root with `cygpath -u` before its plain-path guard, so the guard, the render and setup's own post-render check agree.
|
||||
- **`gstack-wtree`, `gstack-ff-update` and `gstack-session-update`** hand Git their temp index paths in Windows form.
|
||||
- **`gstack-doctor`** warns on the `windows bash` row when `MSYS_NO_PATHCONV` is exported. Bun-based gstack commands start with `--config=/dev/null`, and with path conversion off native Bun cannot open `/dev/null`. No spelling works for both Bun on Windows and Linux/macOS (checked on windows-latest), so the fix is to set the variable per command. See docs/troubleshooting.md#msys-no-pathconv.
|
||||
|
||||
### Tests
|
||||
- `test/gen-skill-docs-install-root-windows.test.ts`: a `C:/` root renders as `/c/...`, and a root with a space refuses.
|
||||
- `test/setup-install-root-windows.test.ts` (windows-latest): `_render_install` takes `/c/...` and `C:/...` roots past its guard to native Bun.
|
||||
- `test/gstack-wtree-windows-paths.test.ts` and `test/ff-update-windows-msys.test.ts` (windows-latest): both run with `MSYS_NO_PATHCONV=1`.
|
||||
- `test/gstack-doctor.test.ts`: the new warning.
|
||||
@@ -2155,6 +2155,23 @@ or in Windows PowerShell:
|
||||
|
||||
---
|
||||
|
||||
<a id="msys-no-pathconv"></a>
|
||||
### `gstack-doctor`: `windows bash ... MSYS_NO_PATHCONV is set`, or a gstack command fails with `EINVAL ... while reading config "/dev/null"`
|
||||
|
||||
**Meaning.** gstack's Bun commands start with `bun --config=/dev/null` so a
|
||||
`bunfig.toml` in your project cannot change them. Git Bash normally turns
|
||||
`/dev/null` and `/tmp/...` into paths native Windows programs can open. With
|
||||
`MSYS_NO_PATHCONV=1` exported, it passes them through unchanged: native Bun
|
||||
cannot open `/dev/null`, so every Bun-based gstack command fails before it
|
||||
starts. `gstack-wtree`, `gstack-ff-update` and the session updater hand Git
|
||||
their temp files in Windows form, so they work either way (#3468).
|
||||
|
||||
**Fix.** Don't export `MSYS_NO_PATHCONV`. Set it only on the command that needs
|
||||
it, for example `MSYS_NO_PATHCONV=1 docker run -v /c/src:/src ...`, or remove
|
||||
the export from `~/.bashrc` and open a new shell.
|
||||
|
||||
---
|
||||
|
||||
<a id="bash-unavailable"></a>
|
||||
### `gstack: no Git Bash found to run gstack's bash scripts on Windows (tried ...); fix: ... (BASH_UNAVAILABLE)`
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import { insertRuntimePreludes } from './resolvers/runtime-root';
|
||||
import { generateSharedPreambleFile, hoistsPreamble, SHARED_PREAMBLE_FILE } from './resolvers/preamble-hoist';
|
||||
import { extractVoiceTriggers } from './resolvers/voice-triggers';
|
||||
import { frontmatterList, withTriggerLead } from '../lib/skill-triggers';
|
||||
import { toMsysPath } from '../lib/bin-context';
|
||||
import { ALL_HOST_NAMES, resolveHostArg, getHostConfig } from '../hosts/index';
|
||||
import type { HostConfig } from './host-config';
|
||||
|
||||
@@ -154,9 +155,11 @@ function parseGenerationArgs(args: string[]): GenerationOptions {
|
||||
if (modules && (outDir === undefined || path.resolve(outDir) === ROOT)) {
|
||||
throw new ModuleError('MODULE_SPEC_INVALID', `--${value('--modules') === undefined ? 'loadout' : 'modules'} with exclusions needs an --out-dir outside the checkout root; the committed tree is always the full render`, 'bin/gstack-modules render --host <host> --modules <spec> --out-dir <dir>');
|
||||
}
|
||||
const installRoot = value('--install-root');
|
||||
// Git Bash hands native Bun its /c/... root as C:/... (#3498): render the spelling setup checked.
|
||||
const givenInstallRoot = value('--install-root');
|
||||
const installRoot = givenInstallRoot !== undefined && /^[A-Za-z]:[\\/]/.test(givenInstallRoot) ? toMsysPath(givenInstallRoot) : givenInstallRoot;
|
||||
if (installRoot !== undefined && !INSTALL_ROOT_PATTERN.test(installRoot)) {
|
||||
throw new Error(`--install-root must be an absolute path of letters, digits and . _ - + @ / (got ${JSON.stringify(installRoot)})`);
|
||||
throw new Error(`--install-root must be an absolute path (/x, or C:/x on Windows) of letters, digits and . _ - + @ / (got ${JSON.stringify(givenInstallRoot)})`);
|
||||
}
|
||||
// Swap-in callers use --link-root for the FINAL serving path (#2692).
|
||||
// Direct --out-dir callers retain their existing links into the render.
|
||||
|
||||
@@ -338,6 +338,8 @@ export const KNOWN_WINDOWS_SAFE: Array<{ file: string; reason: string }> = [
|
||||
{ file: 'test/copilot-windows-bash.test.ts', reason: 'the Copilot glossary line that runs bash blocks in Git for Windows Bash; reads host config only' },
|
||||
{ file: 'test/ship-hook-windows-paths.test.ts', reason: 'runs bin/ helpers through explicit bash and Bun argv with forward-slash paths; never executes a shebang; the path-spelling simulation is skipIf win32' },
|
||||
{ file: 'test/gstack-wtree-windows-paths.test.ts', reason: '#3101: runs bin/gstack-wtree through explicit bash in an os.tmpdir linked worktree with forward-slash paths; never executes a shebang; the drive-letter simulation is skipIf win32' },
|
||||
{ file: 'test/setup-install-root-windows.test.ts', reason: '#3498: runs setup\'s _render_install through Git Bash with a native Bun recorder; the drive-path conversion it guards only happens on Windows (skipIf not win32)' },
|
||||
{ file: 'test/ff-update-windows-msys.test.ts', reason: '#3468: runs gstack-ff-update through lib/script-invocation.ts with MSYS path conversion off; the /tmp index path only fails in Git for Windows' },
|
||||
{ file: 'test/setup-hook-syntax-gate.test.ts', reason: '#3088/#3100: runs bin/gstack-hook-check and the setup hook sections through explicit bash with forward-slash paths; never executes a shebang. Its Bun drive-path spelling is the Windows bug it guards' },
|
||||
{ file: 'test/bun-config-shell-bins.test.ts', reason: '#3415: runs the record bins through lib/script-invocation.ts (Git Bash on Windows) with MSYS path conversion off, the case it guards; its bun and cygpath stand-ins and simulated-OSTYPE cases are skipIf win32' },
|
||||
{ file: 'test/path-spelling.test.ts', reason: 'sources bin/gstack-path-spelling.sh through explicit bash and checks lib/path-spelling.ts; on Windows it exercises the real Git Bash spellings' },
|
||||
|
||||
@@ -1124,6 +1124,11 @@ _render_install() {
|
||||
&& [ "$got" = "$(cd "$(dirname "$default")" 2>/dev/null && pwd -P)" ] && return 0
|
||||
render="$(gstack_install_render_dir "$host" "$root")"
|
||||
named="$root"
|
||||
# A drive-form root (C:/x) gets the /c/x spelling the renderer writes, so the
|
||||
# guard below and both post-render checks agree with the render (#3498).
|
||||
if [ "$IS_WINDOWS" -eq 1 ] && command -v cygpath >/dev/null 2>&1; then
|
||||
named="$(cygpath -u "$root" 2>/dev/null)" || named="$root"
|
||||
fi
|
||||
if ! printf '%s' "$named" | grep -Eq '^/[A-Za-z0-9_.@+/-]*$'; then
|
||||
named="$render.root"
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || ! printf '%s' "$named" | grep -Eq '^/[A-Za-z0-9_.@+/-]*$'; then
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* #3468: gstack-ff-update stages its capture indexes under a mktemp
|
||||
* directory and hands them to git as GIT_INDEX_FILE. With MSYS path
|
||||
* conversion off (MSYS_NO_PATHCONV=1, common for docker), Git for Windows got
|
||||
* the raw /tmp/... path and could not open it, so an install could not move.
|
||||
*
|
||||
* Windows-safe by construction: gstack-ff-update runs through
|
||||
* lib/script-invocation.ts (Git Bash on win32), fixtures live under os.tmpdir().
|
||||
*/
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { runScriptSync } from '../lib/script-invocation';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
let base = '';
|
||||
let env: NodeJS.ProcessEnv = {};
|
||||
const posix = (p: string) => p.replace(/\\/g, '/');
|
||||
|
||||
function git(cwd: string, ...args: string[]): string {
|
||||
const r = spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: 30_000 });
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
return r.stdout.trim();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
base = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-ff-msys-')));
|
||||
const home = path.join(base, 'home');
|
||||
fs.mkdirSync(home);
|
||||
fs.writeFileSync(path.join(home, '.gitconfig'), '[user]\n\tname = t\n\temail = t@t\n[commit]\n\tgpgsign = false\n');
|
||||
env = { ...process.env, HOME: home, USERPROFILE: home, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: path.join(home, '.gitconfig'), GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined };
|
||||
});
|
||||
afterEach(() => fs.rmSync(base, { recursive: true, force: true }));
|
||||
|
||||
test('an install moves to a new release with MSYS path conversion off (#3468)', () => {
|
||||
const origin = path.join(base, 'origin.git');
|
||||
const seed = path.join(base, 'seed');
|
||||
const install = path.join(base, 'install');
|
||||
git(base, 'init', '-q', '--bare', '-b', 'main', origin);
|
||||
fs.mkdirSync(seed);
|
||||
fs.writeFileSync(path.join(seed, 'VERSION'), '1.0.0\n');
|
||||
git(seed, 'init', '-q', '-b', 'main');
|
||||
git(seed, 'add', '-A');
|
||||
git(seed, 'commit', '-q', '-m', 'seed');
|
||||
git(seed, 'remote', 'add', 'origin', origin);
|
||||
git(seed, 'push', '-q', 'origin', 'main');
|
||||
git(base, 'clone', '-q', origin, install);
|
||||
fs.writeFileSync(path.join(seed, 'VERSION'), '1.1.0\n');
|
||||
git(seed, 'commit', '-q', '-am', 'release');
|
||||
git(seed, 'push', '-q', 'origin', 'main');
|
||||
git(install, 'fetch', '-q');
|
||||
fs.writeFileSync(path.join(install, 'local-note.txt'), 'kept\n');
|
||||
const r = runScriptSync(path.join(ROOT, 'bin', 'gstack-ff-update'), [posix(install), 'origin/main'], {
|
||||
cwd: base, env: { ...env, MSYS_NO_PATHCONV: '1' }, encoding: 'utf8', timeout: 120_000,
|
||||
});
|
||||
expect(r.error).toBeUndefined();
|
||||
expect(r.status, `${r.stdout}${r.stderr}`).toBe(0);
|
||||
expect(git(install, 'rev-parse', 'HEAD')).toBe(git(seed, 'rev-parse', 'HEAD'));
|
||||
expect(fs.readFileSync(path.join(install, 'local-note.txt'), 'utf8')).toBe('kept\n');
|
||||
}, 150_000);
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* #3498: on Windows, Git Bash converts setup's /c/... install root to C:/...
|
||||
* when it hands the argument to native Bun, and the renderer refused any root
|
||||
* not starting with "/", so a custom-root install kept the default-install
|
||||
* paths. A drive-letter root is accepted and rendered in the /c/... form that
|
||||
* setup verified; anything else outside the plain charset still refuses.
|
||||
*/
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { bunIsolationArgs } from '../lib/bun-isolation';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
let out = '';
|
||||
|
||||
beforeEach(() => { out = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-install-root-'))); });
|
||||
afterEach(() => fs.rmSync(out, { recursive: true, force: true }));
|
||||
|
||||
const render = (installRoot: string) => spawnSync(process.execPath, [
|
||||
...bunIsolationArgs(), path.join(ROOT, 'scripts', 'gen-skill-docs.ts'), '--host', 'claude', '--out-dir', path.join(out, 'render'), '--install-root', installRoot,
|
||||
], { cwd: ROOT, encoding: 'utf8', timeout: 240_000 });
|
||||
|
||||
test('a drive-letter install root renders as its /c/... spelling (#3498)', () => {
|
||||
const r = render('C:/Users/example/isolated/claude-home/skills/gstack');
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
const skill = fs.readFileSync(path.join(out, 'render', 'SKILL.md'), 'utf8');
|
||||
expect(skill).toContain('/c/Users/example/isolated/claude-home/skills/gstack/bin/gstack-skill-start --skill');
|
||||
expect(skill).not.toContain('C:/Users/example');
|
||||
}, 260_000);
|
||||
|
||||
test('a root with a space still refuses (#3498)', () => {
|
||||
const r = render('C:/Users/First Last/skills/gstack');
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('--install-root must be');
|
||||
}, 260_000);
|
||||
@@ -1131,6 +1131,14 @@ describe('gstack-doctor: rescue refs, CLAUDE.md skill lists, Windows bash', () =
|
||||
expect(override.row('windows bash').detail).toBe('D:/tools/bash.exe (chosen by GSTACK_GIT_BASH)');
|
||||
});
|
||||
|
||||
test('windows bash: an exported MSYS_NO_PATHCONV warns that Bun commands cannot start (#3468)', () => {
|
||||
const f = makeFixture();
|
||||
const r = doctor(f, [], { GSTACK_DOCTOR_PLATFORM: 'windows', MSYS_NO_PATHCONV: '1', GSTACK_DOCTOR_BASH_PROBE: probe(f, { exists: [GIT_BASH], gitExecPath: 'C:/Program Files/Git/mingw64/libexec/git-core' }) });
|
||||
expect(r.row('windows bash').state, r.out).toBe('warn');
|
||||
expect(r.row('windows bash').detail).toContain('MSYS_NO_PATHCONV is set');
|
||||
expect(r.row('windows bash').fix).toContain('#msys-no-pathconv');
|
||||
});
|
||||
|
||||
test('windows bash: GSTACK_GIT_BASH and a different deprecated GSTACK_BASH warn naming both', () => {
|
||||
const f = makeFixture();
|
||||
const differ = doctor(f, [], { GSTACK_DOCTOR_PLATFORM: 'windows', GSTACK_GIT_BASH: 'D:/a/bash.exe', GSTACK_BASH: 'D:/b/bash.exe', GSTACK_DOCTOR_BASH_PROBE: probe(f, { exists: ['D:/a/bash.exe', 'D:/b/bash.exe'] }) });
|
||||
|
||||
@@ -63,6 +63,14 @@ test('a linked worktree with a private object directory gets its fingerprint (#3
|
||||
expectFingerprintOfHead();
|
||||
});
|
||||
|
||||
// #3468: with MSYS path conversion off (MSYS_NO_PATHCONV=1, common for docker
|
||||
// calls), Git for Windows received the raw /tmp/... temp index path and could
|
||||
// not open it.
|
||||
test('the temp index reaches git with MSYS path conversion off (#3468)', () => {
|
||||
env = { ...env, MSYS_NO_PATHCONV: '1' };
|
||||
expectFingerprintOfHead();
|
||||
});
|
||||
|
||||
// Linux and macOS reproduce Git for Windows: `--git-path` answers in drive-letter
|
||||
// form, and GIT_ALTERNATE_OBJECT_DIRECTORIES is one ;-separated list in which C:/…
|
||||
// is absolute. Linux git splits on ':', so the shim hands that list over as one
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* #3498, installer side: setup's _render_install must accept a custom Windows
|
||||
* install root in either spelling (/c/... or C:/...), pass its plain-path
|
||||
* guard, and hand native Bun an --install-root the renderer accepts. Bun is a
|
||||
* native program, so Git Bash converts /c/... to C:/... on the way in; the
|
||||
* renderer test (gen-skill-docs-install-root-windows) covers what it does then.
|
||||
*
|
||||
* Windows only: the guard and the conversion are Git Bash behavior. The
|
||||
* function is sliced from setup and run through explicit bash; bun_cmd is a
|
||||
* native Bun that records the argv it received and fails the render.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { runBashSync } from '../lib/script-invocation';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const SETUP = fs.readFileSync(path.join(ROOT, 'setup'), 'utf8');
|
||||
const posix = (p: string) => p.replace(/\\/g, '/');
|
||||
|
||||
function slice(name: string): string {
|
||||
const start = SETUP.indexOf(`\n${name}() {`);
|
||||
const end = SETUP.indexOf('\n}\n', start);
|
||||
if (start < 0 || end < 0) throw new Error(`cannot find ${name}() in setup`);
|
||||
return SETUP.slice(start + 1, end + 2);
|
||||
}
|
||||
|
||||
let tmp = '';
|
||||
beforeEach(() => { tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-setup-root-'))); });
|
||||
afterEach(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
|
||||
describe.skipIf(process.platform !== 'win32')('setup _render_install with a custom Windows root (#3498)', () => {
|
||||
for (const root of ['/c/Users/example/isolated/claude-home/skills/gstack', 'C:/Users/example/isolated/claude-home/skills/gstack']) {
|
||||
test(`root spelled ${root.slice(0, 2)}... passes the guard and reaches the renderer`, () => {
|
||||
const argvFile = posix(path.join(tmp, 'argv.json'));
|
||||
const script = [
|
||||
'IS_WINDOWS=1',
|
||||
`HOME=${JSON.stringify(posix(tmp))}`,
|
||||
`SOURCE_GSTACK_DIR=${JSON.stringify(posix(ROOT))}`,
|
||||
`GSTACK_STATE_ROOT=${JSON.stringify(posix(path.join(tmp, 'state')))}`,
|
||||
'_GSTACK_OVERLAY=claude',
|
||||
`gstack_install_render_dir() { printf '%s\\n' ${JSON.stringify(posix(path.join(tmp, 'render')))}; }`,
|
||||
`bun_cmd() { ${JSON.stringify(posix(process.execPath))} -e 'require("fs").writeFileSync(process.env.ARGV_FILE, JSON.stringify(process.argv.slice(1)))' -- "$@"; return 1; }`,
|
||||
`export ARGV_FILE=${JSON.stringify(argvFile)}`,
|
||||
slice('_render_install'),
|
||||
`_render_install claude ${JSON.stringify(root)}; echo "rc=$?"`,
|
||||
].join('\n');
|
||||
const r = runBashSync(script, { encoding: 'utf8', timeout: 60_000 });
|
||||
expect(r.stderr).not.toContain('is not a plain path');
|
||||
const argv: string[] = JSON.parse(fs.readFileSync(path.join(tmp, 'argv.json'), 'utf8'));
|
||||
const given = argv[argv.indexOf('--install-root') + 1];
|
||||
expect(given, JSON.stringify(argv)).toMatch(/^(?:[Cc]:|\/c)\/Users\/example\/isolated\/claude-home\/skills\/gstack$/);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user