@solaqua/gji 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +89 -18
  2. package/dist/back.d.ts +3 -0
  3. package/dist/back.js +19 -7
  4. package/dist/bootstrap-output.d.ts +3 -0
  5. package/dist/bootstrap-output.js +27 -0
  6. package/dist/bootstrap-preview.d.ts +10 -0
  7. package/dist/bootstrap-preview.js +21 -0
  8. package/dist/clean.js +25 -3
  9. package/dist/cli.js +101 -19
  10. package/dist/clone-failure-store.d.ts +18 -0
  11. package/dist/clone-failure-store.js +230 -0
  12. package/dist/command-runner.d.ts +6 -0
  13. package/dist/command-runner.js +38 -0
  14. package/dist/completion.js +7 -2
  15. package/dist/config-command.d.ts +1 -0
  16. package/dist/config-command.js +7 -1
  17. package/dist/config.d.ts +26 -1
  18. package/dist/config.js +110 -6
  19. package/dist/dependency-bootstrap-prompt.d.ts +24 -0
  20. package/dist/dependency-bootstrap-prompt.js +103 -0
  21. package/dist/dependency-bootstrap.d.ts +129 -0
  22. package/dist/dependency-bootstrap.js +566 -0
  23. package/dist/dir-clone.d.ts +33 -0
  24. package/dist/dir-clone.js +505 -0
  25. package/dist/done.d.ts +10 -0
  26. package/dist/done.js +208 -0
  27. package/dist/file-sync.js +71 -22
  28. package/dist/format-bytes.d.ts +1 -0
  29. package/dist/format-bytes.js +14 -0
  30. package/dist/fs-utils.d.ts +4 -0
  31. package/dist/fs-utils.js +26 -0
  32. package/dist/git.d.ts +5 -0
  33. package/dist/git.js +50 -0
  34. package/dist/gji-bundle.mjs +5491 -1253
  35. package/dist/go-resolver.d.ts +35 -0
  36. package/dist/go-resolver.js +185 -0
  37. package/dist/go.d.ts +5 -2
  38. package/dist/go.js +262 -62
  39. package/dist/hooks.d.ts +1 -1
  40. package/dist/hooks.js +13 -7
  41. package/dist/init.js +30 -22
  42. package/dist/install-prompt.d.ts +1 -0
  43. package/dist/install-prompt.js +3 -25
  44. package/dist/navigation-output.d.ts +11 -0
  45. package/dist/navigation-output.js +6 -0
  46. package/dist/new.d.ts +13 -2
  47. package/dist/new.js +414 -39
  48. package/dist/pr.d.ts +6 -2
  49. package/dist/pr.js +135 -30
  50. package/dist/remove.js +20 -0
  51. package/dist/repo.d.ts +3 -0
  52. package/dist/repo.js +57 -6
  53. package/dist/safe-destination.d.ts +15 -0
  54. package/dist/safe-destination.js +166 -0
  55. package/dist/shell-completion.js +43 -12
  56. package/dist/sync-directories.d.ts +33 -0
  57. package/dist/sync-directories.js +97 -0
  58. package/dist/sync-plan.d.ts +16 -0
  59. package/dist/sync-plan.js +128 -0
  60. package/dist/sync.js +7 -13
  61. package/dist/undo.d.ts +36 -0
  62. package/dist/undo.js +392 -0
  63. package/dist/warp.d.ts +3 -1
  64. package/dist/warp.js +32 -125
  65. package/dist/worktree-bootstrap.d.ts +35 -0
  66. package/dist/worktree-bootstrap.js +93 -0
  67. package/dist/worktree-management.d.ts +1 -0
  68. package/dist/worktree-management.js +23 -4
  69. package/dist/worktree-picker.d.ts +21 -8
  70. package/dist/worktree-picker.js +107 -15
  71. package/dist/worktree-prompts.js +1 -1
  72. package/dist/worktree-source.d.ts +6 -0
  73. package/dist/worktree-source.js +1 -0
  74. package/dist/worktree-sources.d.ts +3 -0
  75. package/dist/worktree-sources.js +31 -0
  76. package/man/man1/gji-back.1 +1 -1
  77. package/man/man1/gji-clean.1 +1 -1
  78. package/man/man1/gji-completion.1 +1 -1
  79. package/man/man1/gji-config.1 +1 -1
  80. package/man/man1/gji-doctor.1 +1 -1
  81. package/man/man1/gji-done.1 +19 -0
  82. package/man/man1/gji-go.1 +9 -3
  83. package/man/man1/gji-history.1 +1 -1
  84. package/man/man1/gji-init.1 +1 -1
  85. package/man/man1/gji-ls.1 +1 -1
  86. package/man/man1/gji-new.1 +18 -3
  87. package/man/man1/gji-open.1 +1 -1
  88. package/man/man1/gji-pr.1 +7 -1
  89. package/man/man1/gji-remove.1 +3 -3
  90. package/man/man1/gji-root.1 +1 -1
  91. package/man/man1/gji-run-hook.1 +1 -1
  92. package/man/man1/gji-status.1 +1 -1
  93. package/man/man1/gji-sync-files.1 +1 -1
  94. package/man/man1/gji-sync.1 +1 -1
  95. package/man/man1/gji-undo.1 +16 -0
  96. package/man/man1/gji-warp.1 +3 -6
  97. package/man/man1/gji.1 +13 -5
  98. package/package.json +3 -1
package/README.md CHANGED
@@ -11,7 +11,7 @@ gji new feature/payment-refactor # new branch + worktree, cd in
11
11
  gji pr 1234 # review PR in isolation, cd in
12
12
  gji pr open # open the PR for the current worktree
13
13
  gji go main # jump back, shell changes directory
14
- gji remove feature/payment-refactor
14
+ gji done feature/payment-refactor
15
15
  ```
16
16
 
17
17
  ## Before / After
@@ -112,8 +112,8 @@ gji open
112
112
  gji open --select # choose another worktree interactively
113
113
  gji open feature/dark-mode --editor code
114
114
 
115
- # clean up when done
116
- gji remove feature/dark-mode
115
+ # finish a worktree when done
116
+ gji done feature/dark-mode
117
117
  ```
118
118
 
119
119
  Worktrees land at a deterministic path so your editor bookmarks and scripts always know where to look:
@@ -135,7 +135,15 @@ gji pr 1234 # checkout PR locally
135
135
  gji pr https://github.com/org/repo/pull/1234 # or paste the URL
136
136
 
137
137
  gji go feature/auth-refactor # jump to a worktree
138
+ gji go teammate-branch # open an existing local or remote branch
139
+ gji go - # return to the previous worktree
140
+ gji go --root # return to the main repository checkout
138
141
  gji root # jump to repo root
142
+ gji warp repo-a/main # compatibility spelling for cross-repo navigation
143
+ gji back # return through navigation history
144
+ gji history # inspect recent navigation
145
+
146
+ # with no branch, the chooser starts in the current repo; press Tab for all repos
139
147
 
140
148
  gji status # health overview + ahead/behind counts
141
149
  gji ls # list with status/upstream/last commit
@@ -146,7 +154,7 @@ gji sync --all # rebase every worktree
146
154
 
147
155
  gji clean # interactive bulk cleanup
148
156
  gji clean --stale # only target safe stale cleanup candidates
149
- gji remove feature/auth-refactor # remove one worktree and its branch
157
+ gji done feature/auth-refactor # finish one worktree and its branch
150
158
 
151
159
  gji trigger-hook afterCreate # re-run setup in the current worktree
152
160
  ```
@@ -231,24 +239,38 @@ path=$(gji root --print)
231
239
 
232
240
  | Command | Description |
233
241
  |---|---|
234
- | `gji new [branch] [--from-current] [--detached] [--open] [--editor <cli>] [--json]` | create branch + worktree, optionally based on the current worktree |
242
+ | `gji new [branch] [--from-current] [--detached] [--no-fetch] [--take] [--copy] [--force] [--open] [--editor <cli>] [--dry-run] [--json]` | create branch + worktree from a refreshed remote base by default; `--no-fetch` opts out |
243
+ | `gji done [branch] [--force] [--keep-branch] [--json]` | safely finish a linked worktree and return |
244
+ | `gji undo [id] [--list] [--json]` | restore a journaled cleanup without overwriting work |
235
245
  | `gji pr <ref> [--json]` | fetch PR ref, create worktree, cd in |
236
246
  | `gji pr open [branch|#N] [--select]` | open the current worktree PR, or choose a linked worktree with `--select` |
247
+ | `gji back [n] [--print]` | return to a previous worktree from navigation history |
248
+ | `gji history [--json]` | show navigation history |
249
+ | `gji warp [branch] [--print] [--json]` | compatibility spelling for cross-repository navigation |
237
250
  | `gji open [branch] [--select] [--editor <cli>] [--save] [--workspace]` | open the current or selected worktree in an editor |
238
- | `gji go [branch] [--print]` | jump to a worktree |
251
+ | `gji go [branch] [--root] [--print] [--json]` | resolve and jump to a worktree, branch, remote, or PR |
239
252
  | `gji root [--print]` | jump to the main repo root |
240
253
  | `gji status [--json]` | repo overview, worktree health, ahead/behind |
241
254
  | `gji ls [--compact] [--json]` | list active worktrees |
242
255
  | `gji sync [--all]` | fetch and rebase worktrees onto default branch |
243
256
  | `gji sync-files [list\|add\|remove] [paths...]` | manage local files copied into new worktrees |
244
- | `gji clean [--stale] [--force] [--json]` | interactively prune linked worktrees |
245
- | `gji remove [branch] [--force] [--json]` | remove a worktree and its branch |
257
+ | `gji clean [--stale] [--force] [--dry-run] [--json]` | interactively prune linked worktrees |
258
+ | `gji remove [branch] [--force] [--dry-run] [--json]` (`rm`) | **deprecated**; use `gji done` for one worktree or `gji clean` for bulk cleanup |
246
259
  | `gji trigger-hook <hook>` | run a hook in the current worktree |
247
260
  | `gji config [get\|set\|unset] [key] [value]` | manage global defaults |
248
261
  | `gji init [shell]` | interactively set up onboarding, or print/install a shell wrapper |
249
262
  | `gji doctor [--json] [--fix] [--yes]` | check installation and configuration health; optionally remove stale repository entries |
250
263
  | `gji completion [shell]` | print shell completion definitions |
251
264
 
265
+ The repository registry records projects that gji has visited so `go`, `warp`,
266
+ and the chooser can resolve worktrees across repositories. It is advisory
267
+ metadata: missing paths are skipped and `gji doctor --fix` can remove stale
268
+ entries.
269
+
270
+ `gji remove` and its `rm` alias remain available during the deprecation window,
271
+ but print a migration warning in human-readable mode. Use `gji done <branch>`
272
+ to finish one linked worktree or `gji clean` to prune several worktrees.
273
+
252
274
  ## Configuration
253
275
 
254
276
  No setup required. Optional config lives in:
@@ -266,8 +288,11 @@ No setup required. Optional config lives in:
266
288
  | `syncRemote` | remote for `gji sync` (default: `origin`) |
267
289
  | `syncDefaultBranch` | branch to rebase onto (default: remote `HEAD`) |
268
290
  | `syncFiles` | files to copy from main worktree into each new worktree; use global per-repo config for private files |
291
+ | `syncDirs` | arbitrary directories to clone with filesystem copy-on-write before sync files |
292
+ | `dependencyBootstrap` | dependency/build-state policy: `off`, `cow-then-repair`, or `install-only` |
293
+ | `dependencyBuildCommand` | optional Cargo repair command used by `dependencyBootstrap` (default: `cargo check`) |
269
294
  | `skipInstallPrompt` | `true` to disable the auto-install prompt permanently |
270
- | `installSaveTarget` | `"local"` or `"global"` — where **Always**/**Never** choices are persisted (default: `"local"`); set during `gji init <shell> --write` |
295
+ | `installSaveTarget` | `"local"` or `"global"` — where dependency policy and legacy **Always**/**Never** choices are persisted (default: `"local"`); set during `gji init <shell> --write` |
271
296
  | `hooks` | lifecycle scripts (see [Hooks](#hooks)) |
272
297
  | `repos` | per-repo overrides inside the global config (see below) |
273
298
 
@@ -276,7 +301,9 @@ No setup required. Optional config lives in:
276
301
  "branchPrefix": "feature/",
277
302
  "syncRemote": "upstream",
278
303
  "syncDefaultBranch": "main",
279
- "syncFiles": [".env.example", ".nvmrc"]
304
+ "syncFiles": [".env.example", ".nvmrc"],
305
+ "syncDirs": [".next"],
306
+ "dependencyBootstrap": "cow-then-repair"
280
307
  }
281
308
  ```
282
309
 
@@ -304,6 +331,42 @@ This stores:
304
331
  }
305
332
  ```
306
333
 
334
+ ### Instant directory bootstrap
335
+
336
+ 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:
337
+
338
+ ```json
339
+ {
340
+ "syncDirs": [".next", ".cache"]
341
+ }
342
+ ```
343
+
344
+ `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.
345
+
346
+ Paths are relative to the repository root. Absolute paths, `..` segments, and `.git` paths are rejected in all three config layers.
347
+
348
+ The human output includes clone timing; dry-run can provide source-size estimates:
349
+
350
+ ```text
351
+ ⚡ cloned .next (size unavailable → 1.2s)
352
+ ```
353
+
354
+ Use `dependencyBootstrap` when a package manager or build cache needs a reusable seed followed by authoritative repair:
355
+
356
+ ```json
357
+ {
358
+ "dependencyBootstrap": "cow-then-repair"
359
+ }
360
+ ```
361
+
362
+ `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.
363
+
364
+ 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.
365
+
366
+ 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`.
367
+
368
+ 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.
369
+
307
370
  ### Per-repo overrides in global config
308
371
 
309
372
  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:
@@ -351,7 +414,7 @@ Run scripts automatically at key lifecycle moments:
351
414
  |---|---|
352
415
  | `afterCreate` | after `gji new` or `gji pr` creates a worktree |
353
416
  | `afterEnter` | after `gji go` switches to a worktree |
354
- | `beforeRemove` | before `gji remove` deletes a worktree |
417
+ | `beforeRemove` | before `gji done` or `gji clean` deletes a worktree |
355
418
 
356
419
  Hooks receive `{{branch}}`, `{{path}}`, `{{repo}}` as template variables and `GJI_BRANCH`, `GJI_PATH`, `GJI_REPO` as environment variables. A failing hook emits a warning but never aborts the command.
357
420
 
@@ -410,7 +473,9 @@ This is useful after cloning on a new machine, recovering a broken worktree, or
410
473
 
411
474
  ## Install prompt
412
475
 
413
- 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:
476
+ 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.
477
+
478
+ Projects without a supported dependency adapter can still use the legacy one-shot install prompt:
414
479
 
415
480
  ```
416
481
  Run `pnpm install` in the new worktree?
@@ -422,6 +487,8 @@ Run `pnpm install` in the new worktree?
422
487
 
423
488
  **Always** saves `hooks.afterCreate`; **Never** writes `skipInstallPrompt: true`. Where they are saved depends on `installSaveTarget` (see [Available keys](#available-keys)) — defaults to `.gji.json`.
424
489
 
490
+ `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.
491
+
425
492
  ## JSON output
426
493
 
427
494
  Every mutating command supports `--json` for scripting and AI agent use. Success goes to stdout, errors go to stderr with exit code 1.
@@ -429,18 +496,22 @@ Every mutating command supports `--json` for scripting and AI agent use. Success
429
496
  ```sh
430
497
  # create
431
498
  gji new --json feature/dark-mode
432
- # → { "branch": "feature/dark-mode", "path": "/…/worktrees/repo/feature/dark-mode" }
499
+ # → { "branch": "feature/dark-mode", "path": "/…/worktrees/repo/feature/dark-mode", "repository": { "name": "repo", "root": "/…/repo" } }
433
500
 
434
501
  # fetch PR
435
502
  gji pr --json 1234
436
- # → { "branch": "pr/1234", "path": "/…/worktrees/repo/pr/1234" }
503
+ # → { "branch": "pr/1234", "path": "/…/worktrees/repo/pr/1234", "repository": { "name": "repo", "root": "/…/repo" } }
504
+
505
+ # resolve an existing destination without changing directories
506
+ gji go --json feature/auth-refactor
507
+ # → { "branch": "feature/auth-refactor", "path": "/…/worktrees/repo/feature/auth-refactor", "repository": { "name": "repo", "root": "/…/repo" } }
437
508
 
438
509
  # detailed list
439
510
  gji ls --json
440
511
  # → [{ "branch": "...", "status": "clean", "upstream": { "kind": "tracked", ... }, ... }]
441
512
 
442
- # remove
443
- gji remove --json --force feature/dark-mode
513
+ # finish one worktree
514
+ gji done --json --force feature/dark-mode
444
515
  # → { "branch": "feature/dark-mode", "path": "/…", "deleted": true }
445
516
 
446
517
  # bulk clean
@@ -457,7 +528,7 @@ gji clean --stale --json --force
457
528
 
458
529
  `gji clean --stale` limits cleanup to clean branch worktrees whose upstream is gone and whose branch is already merged into the configured or remote default branch.
459
530
 
460
- `--json` suppresses all interactive prompts. `--force` is required for `remove` and `clean` in JSON mode. `branch` is `null` for detached worktrees.
531
+ `--json` suppresses all interactive prompts. Navigation results include a `repository` object with the stable repository `root` and display `name`. `--force` is required for `done` and `clean` in JSON mode. `branch` is `null` for detached worktrees.
461
532
 
462
533
  `gji ls --json` and `gji status --json` also produce structured output — see `gji status --json | jq` for the full schema.
463
534
 
@@ -465,7 +536,7 @@ gji clean --stale --json --force
465
536
 
466
537
  ```sh
467
538
  GJI_NO_TUI=1 gji new feature/ci-branch
468
- GJI_NO_TUI=1 gji remove --force feature/ci-branch
539
+ GJI_NO_TUI=1 gji done --force feature/ci-branch
469
540
  GJI_NO_TUI=1 gji clean --force
470
541
  ```
471
542
 
package/dist/back.d.ts CHANGED
@@ -3,7 +3,10 @@ export declare const BACK_OUTPUT_FILE_ENV = "GJI_BACK_OUTPUT_FILE";
3
3
  export interface BackCommandOptions {
4
4
  cwd: string;
5
5
  home?: string;
6
+ json?: boolean;
6
7
  n?: number;
8
+ commandName?: string;
9
+ outputEnv?: string;
7
10
  print?: boolean;
8
11
  stderr: (chunk: string) => void;
9
12
  stdout: (chunk: string) => void;
package/dist/back.js CHANGED
@@ -3,6 +3,7 @@ import { basename } from "node:path";
3
3
  import { loadEffectiveConfig } from "./config.js";
4
4
  import { appendHistory, loadHistory } from "./history.js";
5
5
  import { extractHooks, runHook } from "./hooks.js";
6
+ import { createNavigationRepository, createNavigationTarget, } from "./navigation-output.js";
6
7
  import { detectRepository } from "./repo.js";
7
8
  import { writeShellOutput } from "./shell-handoff.js";
8
9
  export const BACK_OUTPUT_FILE_ENV = "GJI_BACK_OUTPUT_FILE";
@@ -31,10 +32,23 @@ export async function runBackCommand(options) {
31
32
  }
32
33
  }
33
34
  if (!target) {
34
- options.stderr("gji back: no previous worktree in history\n");
35
- options.stderr("Hint: Use 'gji go', 'gji new', or 'gji pr' to navigate between worktrees\n");
35
+ const commandName = options.commandName ?? "gji back";
36
+ if (options.json) {
37
+ options.stderr(`${JSON.stringify({ error: "no previous worktree in history" }, null, 2)}\n`);
38
+ }
39
+ else {
40
+ options.stderr(`${commandName}: no previous worktree in history\n`);
41
+ }
42
+ if (!options.json) {
43
+ options.stderr("Hint: Use 'gji go', 'gji new', or 'gji pr' to navigate between worktrees\n");
44
+ }
36
45
  return 1;
37
46
  }
47
+ if (options.json) {
48
+ const repository = await detectRepository(target.path);
49
+ options.stdout(`${JSON.stringify(createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), target.path, target.branch), null, 2)}\n`);
50
+ return 0;
51
+ }
38
52
  try {
39
53
  const repository = await detectRepository(target.path);
40
54
  const config = await loadEffectiveConfig(repository.repoRoot, options.home, options.stderr);
@@ -49,21 +63,19 @@ export async function runBackCommand(options) {
49
63
  // Not in a git repo or hooks unavailable — proceed without hook
50
64
  }
51
65
  await appendHistory(target.path, target.branch, options.home);
52
- await writeShellOutput(BACK_OUTPUT_FILE_ENV, target.path, options.stdout);
66
+ await writeShellOutput(options.outputEnv ?? BACK_OUTPUT_FILE_ENV, target.path, options.stdout);
53
67
  return 0;
54
68
  }
55
69
  export function formatHistoryList(history, cwd) {
56
70
  const branchWidth = Math.max("BRANCH".length, ...history.map((e) => (e.branch ?? "(detached)").length));
57
- const lines = [
58
- " " + "BRANCH".padEnd(branchWidth) + " WHEN PATH",
59
- ];
71
+ const lines = [` ${"BRANCH".padEnd(branchWidth)} WHEN PATH`];
60
72
  for (const entry of history) {
61
73
  const isCurrent = entry.path === cwd;
62
74
  const branch = (entry.branch ?? "(detached)").padEnd(branchWidth);
63
75
  const when = formatAge(entry.timestamp).padEnd(10);
64
76
  lines.push(`${isCurrent ? "*" : " "} ${branch} ${when} ${entry.path}`);
65
77
  }
66
- return lines.join("\n") + "\n";
78
+ return `${lines.join("\n")}\n`;
67
79
  }
68
80
  export function formatAge(timestamp) {
69
81
  const seconds = Math.floor((Date.now() - timestamp) / 1000);
@@ -0,0 +1,3 @@
1
+ import type { DependencyBootstrapReporter } from "./dependency-bootstrap.js";
2
+ import type { SyncDirectoryReporter } from "./sync-directories.js";
3
+ export declare function createBootstrapReporter(write: (chunk: string) => void, json: boolean, measureCloneSize?: boolean): SyncDirectoryReporter & DependencyBootstrapReporter;
@@ -0,0 +1,27 @@
1
+ import { formatBytes } from "./format-bytes.js";
2
+ export function createBootstrapReporter(write, json, measureCloneSize = false) {
3
+ return {
4
+ emitCachedFailureWarnings: !json,
5
+ measureCloneSize: measureCloneSize && !json,
6
+ write,
7
+ cloned: (directory) => {
8
+ if (json)
9
+ return;
10
+ write(`⚡ cloned ${directory.dir} (${formatBytes(directory.bytes)} → ${formatDuration(directory.ms)})\n`);
11
+ },
12
+ skipped: (directory) => {
13
+ if (json)
14
+ return;
15
+ write(`gji: skipped ${directory.dir} — ${directory.reason}\n`);
16
+ },
17
+ dependency: (event) => {
18
+ if (json)
19
+ return;
20
+ const target = event.target ? ` ${event.target}` : "";
21
+ write(`gji: ${event.state}${target} — ${event.message}\n`);
22
+ },
23
+ };
24
+ }
25
+ function formatDuration(ms) {
26
+ return `${(ms / 1000).toFixed(1)}s`;
27
+ }
@@ -0,0 +1,10 @@
1
+ import type { DependencyBootstrapMode } from "./config.js";
2
+ import { type BootstrapTarget, type DependencyBootstrapPreview } from "./dependency-bootstrap.js";
3
+ export declare function createDependencyBootstrapPreview(mode: DependencyBootstrapMode, context: {
4
+ repoRoot: string;
5
+ currentRoot?: string;
6
+ worktreePath: string;
7
+ cargoBuildCommand?: string;
8
+ checkUvRuntime?: (target: BootstrapTarget) => Promise<boolean>;
9
+ }): Promise<DependencyBootstrapPreview>;
10
+ export declare function formatDependencyBootstrapPreview(preview: DependencyBootstrapPreview | undefined): string;
@@ -0,0 +1,21 @@
1
+ import { prepareDependencyBootstrap, previewDependencyBootstrap, } from "./dependency-bootstrap.js";
2
+ export async function createDependencyBootstrapPreview(mode, context) {
3
+ return previewDependencyBootstrap(await prepareDependencyBootstrap(mode, context));
4
+ }
5
+ export function formatDependencyBootstrapPreview(preview) {
6
+ if (!preview)
7
+ return "";
8
+ return preview.targets
9
+ .map(({ adapter, target, strategy }) => `Would ${formatBootstrapStrategy(strategy)} ${target} with ${adapter}\n`)
10
+ .join("");
11
+ }
12
+ function formatBootstrapStrategy(strategy) {
13
+ switch (strategy) {
14
+ case "cow-then-repair":
15
+ return "seed and repair";
16
+ case "repair-only":
17
+ return "repair";
18
+ case "install-only":
19
+ return "install";
20
+ }
21
+ }
package/dist/clean.js CHANGED
@@ -2,8 +2,9 @@ import { confirm, isCancel } from "@clack/prompts";
2
2
  import { loadEffectiveConfig } from "./config.js";
3
3
  import { isBranchMergedInto, readWorktreeHealth, resolveRemoteDefaultBranch, runGit, } from "./git.js";
4
4
  import { isHeadless } from "./headless.js";
5
+ import { finalizeUndoOperation, recordUndoOperation } from "./undo.js";
5
6
  import { formatLastCommit, formatUpstreamState, readWorktreeInfos, serializeWorktreeInfo, } from "./worktree-info.js";
6
- import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeDeletionError, isWorktreeForceRemovalError, loadLinkedWorktrees, removeWorktree, } from "./worktree-management.js";
7
+ import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isSubmoduleWorktreeRemovalError, isWorktreeDeletionError, isWorktreeForceRemovalError, loadLinkedWorktrees, removeWorktree, } from "./worktree-management.js";
7
8
  import { buildWorktreePromptEntries, promptForMultipleWorktrees, } from "./worktree-picker.js";
8
9
  import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree, } from "./worktree-prompts.js";
9
10
  export function createCleanCommand(dependencies = {}) {
@@ -79,6 +80,18 @@ export function createCleanCommand(dependencies = {}) {
79
80
  }
80
81
  const removedWorktrees = [];
81
82
  const failures = [];
83
+ let journal;
84
+ try {
85
+ journal = await recordUndoOperation("clean", repository.repoRoot, selectedWorktrees);
86
+ }
87
+ catch (error) {
88
+ emitError(options, `could not write undo journal; no worktrees were removed: ${toMessage(error)}`);
89
+ return 1;
90
+ }
91
+ if (!journal) {
92
+ emitError(options, "could not capture undo state; no worktrees were removed");
93
+ return 1;
94
+ }
82
95
  for (const worktree of selectedWorktrees) {
83
96
  if (options.stale &&
84
97
  !(await isStaleCleanupCandidate(repository.repoRoot, worktree, staleBaseRef))) {
@@ -97,7 +110,9 @@ export function createCleanCommand(dependencies = {}) {
97
110
  });
98
111
  continue;
99
112
  }
100
- if (options.stale && !isWorktreeDeletionError(error)) {
113
+ if (options.stale &&
114
+ !isWorktreeDeletionError(error) &&
115
+ !isSubmoduleWorktreeRemovalError(error)) {
101
116
  options.stderr(`Skipped ${worktree.path}: no longer a safe stale cleanup candidate\n`);
102
117
  continue;
103
118
  }
@@ -129,7 +144,12 @@ export function createCleanCommand(dependencies = {}) {
129
144
  }
130
145
  catch (error) {
131
146
  if (!isBranchUnmergedError(error)) {
132
- throw error;
147
+ failures.push({
148
+ branch: worktree.branch,
149
+ message: toMessage(error),
150
+ path: worktree.path,
151
+ });
152
+ continue;
133
153
  }
134
154
  if (options.force ||
135
155
  (await confirmForceDeleteBranch(worktree.branch))) {
@@ -146,6 +166,7 @@ export function createCleanCommand(dependencies = {}) {
146
166
  }
147
167
  }
148
168
  }
169
+ await finalizeUndoOperation(journal, removedWorktrees);
149
170
  if (options.json) {
150
171
  const removed = removedWorktrees.map((worktree) => {
151
172
  const info = selectedInfoByPath.get(worktree.path);
@@ -160,6 +181,7 @@ export function createCleanCommand(dependencies = {}) {
160
181
  reportCleanFailures(failures, options.stderr);
161
182
  }
162
183
  else {
184
+ options.stderr("undo: gji undo\n");
163
185
  options.stdout(`${repository.repoRoot}\n`);
164
186
  }
165
187
  return failures.length === 0 ? 0 : 1;