Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions docs/design/2026-08-29-pnpm-worktree-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,20 @@ registry access only when that cache-only attempt is incomplete.
This avoids waiting for pnpm to prefetch optional binaries for other platforms
on the common warm-store path. The script sets `QWEN_SKIP_PREPARE=1` plus a
bootstrap-private notice-generation guard, keeping dependency install scripts
enabled while skipping repository build, bundle, Husky setup, and npm-layout
notice generation. Script execution does not
enabled while skipping repository build, bundle, and npm-layout notice
generation. Because `QWEN_SKIP_PREPARE=1` also suppresses the Husky step that
`scripts/prepare.js` runs for an npm install, the bootstrap installs Husky
hooks itself once the frozen install succeeds. It preserves an existing
non-default `core.hooksPath` and honours `HUSKY=0`; in a checkout that does not
own the repository config Husky would write, which git reports as a linked
worktree whose `core.hooksPath` is unset or as no repository at all, it skips
Husky and reports that, since husky's
unguarded `git config` write would otherwise set the hooks path in the config
every worktree of the repository shares while creating `.husky/_` only in the
bootstrapped checkout; and it fails closed when a Husky run leaves the hooks
path unconfigured or writes no hook wrappers. Tree cleanliness is preserved by
the `.gitignore` husky
generates inside `.husky/_`. Script execution does not
implicitly install stale dependencies; the bootstrap command is the explicit
installation boundary. Building from this pnpm layout is deferred to Stage 2.

Expand Down
99 changes: 92 additions & 7 deletions scripts/setup-worktree.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ const env = {
// A spread of process.env is an ordinary object: on Windows the path
// variable canonically arrives as `Path`, so a case-sensitive `env.PATH`
// read misses it and corepack is never found.
function envValue(name) {
if (process.platform !== 'win32') return env[name];
const key = Object.keys(env).find((key) => key.toUpperCase() === name);
return key === undefined ? undefined : env[key];
}

function pathValue() {
if (process.platform !== 'win32') return env.PATH ?? '';
const key = Object.keys(env).find((name) => name.toUpperCase() === 'PATH');
return (key !== undefined ? env[key] : env.PATH) ?? '';
return envValue('PATH') ?? '';
}

function findOnPath(command) {
Expand Down Expand Up @@ -61,8 +65,89 @@ function runPnpm(args) {
});
}

function getHooksPath() {
const result = spawnSync('git', ['config', '--get', 'core.hooksPath'], {
cwd: rootDir,
env,
encoding: 'utf8',
});
if (result.status === 0) return result.stdout.trim();
// git exits 1 when the key is absent, and a spawn failure means git itself
// is unavailable — the ownership probe below reports that shape as having
// no repository. Any other status is a read failure (a refused config on a
// shared host, a config error) the hooks decision must not be made from.
if (result.status === 1 || result.error) return undefined;
console.error(
`worktree setup failed: could not read core.hooksPath (${result.stderr.trim()})`,
);
process.exit(1);
}

// Husky runs `git config core.hooksPath .husky/_` with no --worktree, so the
// value always lands in the config of the root that owns the repository while
// the `.husky/_` wrappers are created in the working directory it was invoked
// from. Git names that root: `--git-dir` differs from `--git-common-dir` only
// in a linked worktree, whose config every sibling worktree shares. The shape
// of `.git` is no proxy for it — a file also means a `--separate-git-dir` clone
// or a submodule, which own their config, and no `.git` means no repository.
function repositoryConfigOwnership() {
const probe = spawnSync(
'git',
['rev-parse', '--git-dir', '--git-common-dir'],
{ cwd: rootDir, env, encoding: 'utf8' },
);
if (probe.status !== 0) return 'none';
const [gitDir, commonDir] = probe.stdout.trim().split(/\r?\n/);
return gitDir === commonDir ? 'owns' : 'linked';
}

function install(cacheMode) {
return runPnpm(['install', '--frozen-lockfile', cacheMode]);
const result = runPnpm(['install', '--frozen-lockfile', cacheMode]);
if (result.status === 0) {
const hooksPath = getHooksPath();
if (
envValue('HUSKY') === '0' ||
(hooksPath !== undefined && hooksPath !== '.husky/_')
) {
exitWithResult(result);
}
// Without a repository there is no config for husky to write. With the key
// unset in a linked worktree, husky's write would add it to the config
// every worktree of this repository shares while only this checkout
// receives `.husky/_`, silently repointing hook resolution for roots that
// never got the wrappers. Leave both alone and say so instead.
const ownership = repositoryConfigOwnership();
if (
ownership === 'none' ||
(hooksPath === undefined && ownership === 'linked')
) {
console.log(
ownership === 'none'
? 'worktree setup: git could not resolve a repository for this ' +
'checkout; skipping Husky because there is no repository config ' +
'for it to write.'
: 'worktree setup: core.hooksPath is unset and this checkout does not ' +
'own the repository config; skipping Husky so the hooks path is ' +
'not rewritten for every other worktree. Re-run this script here ' +
'once hooks are installed in the primary checkout.',
);
exitWithResult(result);
}
// Husky exits 0 on every soft failure (`.git can't be found`, a refused
// `git config` write), so success takes both proofs: the config value says
// git will use the hooks, and a wrapper on disk says husky wrote them here.
const husky = runPnpm(['exec', 'husky']);
Comment thread
yiliang114 marked this conversation as resolved.
Comment thread
yiliang114 marked this conversation as resolved.
if (
husky.status === 0 &&
(getHooksPath() !== '.husky/_' ||
!existsSync(resolve(rootDir, '.husky', '_', 'pre-commit')))
) {
console.error('worktree setup failed: Husky did not install hooks');
process.exit(1);
}
exitWithResult(husky);
Comment thread
yiliang114 marked this conversation as resolved.
Comment thread
yiliang114 marked this conversation as resolved.
}
return result;
}

function exitWithResult(result) {
Expand All @@ -80,10 +165,10 @@ function exitWithResult(result) {
process.exit(result.status ?? 1);
}

// install() exits the process on every path where the install succeeded, so it
// returns only a failed result and the registry retry below is the only
// decision left for this driver to make.
const cachedInstall = install('--offline');
if (cachedInstall.status === 0) {
process.exit(0);
}

if (
cachedInstall.error ||
Expand Down
Loading
Loading