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
51 changes: 48 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,11 @@ No setup required. Optional config lives in:
| `syncRemote` | remote for `gji sync` (default: `origin`) |
| `syncDefaultBranch` | branch to rebase onto (default: remote `HEAD`) |
| `syncFiles` | files to copy from main worktree into each new worktree; use global per-repo config for private files |
| `syncDirs` | arbitrary directories to clone with filesystem copy-on-write before sync files |
| `dependencyBootstrap` | dependency/build-state policy: `off`, `cow-then-repair`, or `install-only` |
| `dependencyBuildCommand` | optional Cargo repair command used by `dependencyBootstrap` (default: `cargo check`) |
| `skipInstallPrompt` | `true` to disable the auto-install prompt permanently |
| `installSaveTarget` | `"local"` or `"global"` — where **Always**/**Never** choices are persisted (default: `"local"`); set during `gji init <shell> --write` |
| `installSaveTarget` | `"local"` or `"global"` — where dependency policy and legacy **Always**/**Never** choices are persisted (default: `"local"`); set during `gji init <shell> --write` |
| `hooks` | lifecycle scripts (see [Hooks](#hooks)) |
| `repos` | per-repo overrides inside the global config (see below) |

Expand All @@ -298,7 +301,9 @@ No setup required. Optional config lives in:
"branchPrefix": "feature/",
"syncRemote": "upstream",
"syncDefaultBranch": "main",
"syncFiles": [".env.example", ".nvmrc"]
"syncFiles": [".env.example", ".nvmrc"],
"syncDirs": [".next"],
"dependencyBootstrap": "cow-then-repair"
}
```

Expand Down Expand Up @@ -326,6 +331,42 @@ This stores:
}
```

### Instant directory bootstrap

Use `syncDirs` for advanced, arbitrary directories that should be available immediately in each new worktree. Dependency adapters discover their own project-local targets, so you do not need to list `node_modules`, `.venv`, or `target` here:

```json
{
"syncDirs": [".next", ".cache"]
}
```

`gji new` clones these directories with APFS copy-on-write on macOS or mandatory reflinks on Linux, before `syncFiles`. It never falls back to a slow ordinary copy. Unsupported filesystems, external symlink targets, missing sources, and existing destinations are skipped safely; failed CoW attempts are cached in `~/.config/gji/state.json` so repeated worktree creation does not keep waiting on the same unsupported filesystem. `syncDirs` is generic: it does not know or special-case package managers.

Paths are relative to the repository root. Absolute paths, `..` segments, and `.git` paths are rejected in all three config layers.

The human output includes clone timing; dry-run can provide source-size estimates:

```text
⚡ cloned .next (size unavailable → 1.2s)
```

Use `dependencyBootstrap` when a package manager or build cache needs a reusable seed followed by authoritative repair:

```json
{
"dependencyBootstrap": "cow-then-repair"
}
```

`cow-then-repair` supports pnpm (`node_modules` + `pnpm install --frozen-lockfile`), Yarn (`node_modules` + `yarn install --immutable`), uv (`.venv` + `uv sync --locked`), Cargo (`target` + the configured `dependencyBuildCommand`, or `cargo check`), and Bundler (`vendor/bundle` + `BUNDLE_PATH=vendor/bundle bundle install`). npm is install-only (`npm ci`) because it can delete an existing dependency tree; it never seeds `node_modules`. Only project-local targets are eligible, so global Ruby gems and package-manager caches are never cloned. CoW failure never triggers ordinary copying: repair runs from an empty target instead. The lifecycle is `CoW seed → syncFiles → repair/install → after-create`; a sync-file failure stops repair, install prompts, and hooks. A successful dependency seed is reported as `reused and repaired`, not as an install skip.

When a supported lockfile is detected and no `dependencyBootstrap` policy is configured, interactive `gji new` and `gji pr` ask which policy to persist: **Reuse and repair (recommended)**, **Install fresh each time**, or **Skip dependency setup**. The prompt explains the detected tool—for example, pnpm reuses local `node_modules` through CoW and then runs `pnpm install --frozen-lockfile`. The choice is saved locally or in the per-repo global config according to `installSaveTarget`. Headless, JSON, and dry-run commands never prompt and retain the safe `off` default.

Ecosystems dominated by global caches, such as Gradle, Maven, and Go, are not seeded until a safe project-local target and deterministic repair rule are available. Future adapters can add Composer, Poetry/PDM, Mix, Dart/Flutter, or .NET without changing `syncDirs`.

Use `gji new --dry-run` to see the directories and estimated sizes without creating anything. `gji new --json` adds a `cloned` array and structured `dependencyBootstrap` events, including machine-readable reasons for skips and failures. If bootstrap fails, the JSON error includes the created worktree path; text mode prints the same path and a cleanup hint. The benchmark target for a 2 GB dependency tree on supported APFS/Btrfs or XFS filesystems is under 5 seconds; benchmark your repository locally because filesystem and storage behavior determine the result.

### Per-repo overrides in global config

If you work across many repositories, you can scope config to a specific repo inside `~/.config/gji/config.json` without adding a `.gji.json` to that repo:
Expand Down Expand Up @@ -432,7 +473,9 @@ This is useful after cloning on a new machine, recovering a broken worktree, or

## Install prompt

When `gji new` or `gji pr` creates a worktree, `gji` detects the project's package manager from its lockfile and offers to run the install command:
For supported lockfiles, the dependency policy prompt above is the primary setup choice. Keep `after-create` hooks for project-specific work such as `pnpm run generate`, code generation, local-service setup, or environment-specific commands; hooks are not a second dependency-bootstrap configuration.

Projects without a supported dependency adapter can still use the legacy one-shot install prompt:

```
Run `pnpm install` in the new worktree?
Expand All @@ -444,6 +487,8 @@ Run `pnpm install` in the new worktree?

**Always** saves `hooks.afterCreate`; **Never** writes `skipInstallPrompt: true`. Where they are saved depends on `installSaveTarget` (see [Available keys](#available-keys)) — defaults to `.gji.json`.

`syncDirs` remains generic and never suppresses installation or repair. To automate supported dependency/build setup, use `dependencyBootstrap`; the adapter always runs its lockfile/build repair after `syncFiles`. Explicit policies in global defaults, per-repo global config, or `.gji.json` always win over prompting.

## JSON output

Every mutating command supports `--json` for scripting and AI agent use. Success goes to stdout, errors go to stderr with exit code 1.
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"docs:start": "pnpm --dir website start",
"prepublishOnly": "pnpm test && pnpm build && pnpm generate-man",
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --coverage --passWithNoTests",
"test:watch": "vitest",
"lint": "biome lint --write .",
"format": "biome format --write .",
Expand All @@ -65,6 +66,7 @@
"@biomejs/biome": "^2.4.15",
"@j178/prek": "^0.3.13",
"@types/node": "^24.6.0",
"@vitest/coverage-v8": "3.2.4",
"esbuild": "^0.27.0",
"tsx": "^4.22.4",
"typescript": "^5.9.3",
Expand Down
Loading
Loading