@solaqua/gji 0.9.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 (100) hide show
  1. package/README.md +93 -20
  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 +105 -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 +5489 -1206
  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 +14 -2
  47. package/dist/new.js +434 -36
  48. package/dist/open.d.ts +1 -0
  49. package/dist/open.js +14 -2
  50. package/dist/pr.d.ts +6 -2
  51. package/dist/pr.js +135 -30
  52. package/dist/remove.js +20 -0
  53. package/dist/repo.d.ts +3 -0
  54. package/dist/repo.js +57 -6
  55. package/dist/safe-destination.d.ts +15 -0
  56. package/dist/safe-destination.js +166 -0
  57. package/dist/shell-completion.js +47 -14
  58. package/dist/sync-directories.d.ts +33 -0
  59. package/dist/sync-directories.js +97 -0
  60. package/dist/sync-plan.d.ts +16 -0
  61. package/dist/sync-plan.js +128 -0
  62. package/dist/sync.js +7 -13
  63. package/dist/undo.d.ts +36 -0
  64. package/dist/undo.js +392 -0
  65. package/dist/warp.d.ts +3 -1
  66. package/dist/warp.js +32 -125
  67. package/dist/worktree-bootstrap.d.ts +35 -0
  68. package/dist/worktree-bootstrap.js +93 -0
  69. package/dist/worktree-management.d.ts +1 -0
  70. package/dist/worktree-management.js +23 -4
  71. package/dist/worktree-picker.d.ts +21 -8
  72. package/dist/worktree-picker.js +107 -15
  73. package/dist/worktree-prompts.js +1 -1
  74. package/dist/worktree-source.d.ts +6 -0
  75. package/dist/worktree-source.js +1 -0
  76. package/dist/worktree-sources.d.ts +3 -0
  77. package/dist/worktree-sources.js +31 -0
  78. package/man/man1/gji-back.1 +1 -1
  79. package/man/man1/gji-clean.1 +1 -1
  80. package/man/man1/gji-completion.1 +1 -1
  81. package/man/man1/gji-config.1 +1 -1
  82. package/man/man1/gji-doctor.1 +1 -1
  83. package/man/man1/gji-done.1 +19 -0
  84. package/man/man1/gji-go.1 +9 -3
  85. package/man/man1/gji-history.1 +1 -1
  86. package/man/man1/gji-init.1 +1 -1
  87. package/man/man1/gji-ls.1 +1 -1
  88. package/man/man1/gji-new.1 +21 -3
  89. package/man/man1/gji-open.1 +4 -1
  90. package/man/man1/gji-pr.1 +7 -1
  91. package/man/man1/gji-remove.1 +3 -3
  92. package/man/man1/gji-root.1 +1 -1
  93. package/man/man1/gji-run-hook.1 +1 -1
  94. package/man/man1/gji-status.1 +1 -1
  95. package/man/man1/gji-sync-files.1 +1 -1
  96. package/man/man1/gji-sync.1 +1 -1
  97. package/man/man1/gji-undo.1 +16 -0
  98. package/man/man1/gji-warp.1 +3 -6
  99. package/man/man1/gji.1 +13 -5
  100. 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
@@ -107,12 +107,13 @@ gji status
107
107
  gji go feature/dark-mode
108
108
  gji go main
109
109
 
110
- # open any worktree in an editor (interactive picker)
110
+ # open the current worktree in an editor
111
111
  gji open
112
+ gji open --select # choose another worktree interactively
112
113
  gji open feature/dark-mode --editor code
113
114
 
114
- # clean up when done
115
- gji remove feature/dark-mode
115
+ # finish a worktree when done
116
+ gji done feature/dark-mode
116
117
  ```
117
118
 
118
119
  Worktrees land at a deterministic path so your editor bookmarks and scripts always know where to look:
@@ -127,13 +128,22 @@ Set `worktreePath` in your config to use a different base (e.g. `"~/worktrees"`
127
128
 
128
129
  ```sh
129
130
  gji new feature/auth-refactor # new branch + worktree
131
+ gji new feature/auth-tests --from-current # branch from the current worktree
130
132
  gji new --detached # scratch space, auto-named
131
133
 
132
134
  gji pr 1234 # checkout PR locally
133
135
  gji pr https://github.com/org/repo/pull/1234 # or paste the URL
134
136
 
135
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
136
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
137
147
 
138
148
  gji status # health overview + ahead/behind counts
139
149
  gji ls # list with status/upstream/last commit
@@ -144,7 +154,7 @@ gji sync --all # rebase every worktree
144
154
 
145
155
  gji clean # interactive bulk cleanup
146
156
  gji clean --stale # only target safe stale cleanup candidates
147
- gji remove feature/auth-refactor # remove one worktree and its branch
157
+ gji done feature/auth-refactor # finish one worktree and its branch
148
158
 
149
159
  gji trigger-hook afterCreate # re-run setup in the current worktree
150
160
  ```
@@ -229,24 +239,38 @@ path=$(gji root --print)
229
239
 
230
240
  | Command | Description |
231
241
  |---|---|
232
- | `gji new [branch] [--detached] [--open] [--editor <cli>] [--json]` | create branch + worktree, cd in (validates branch name against Git rules) |
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 |
233
245
  | `gji pr <ref> [--json]` | fetch PR ref, create worktree, cd in |
234
246
  | `gji pr open [branch|#N] [--select]` | open the current worktree PR, or choose a linked worktree with `--select` |
235
- | `gji open [branch] [--editor <cli>] [--save] [--workspace]` | open a worktree in an editor |
236
- | `gji go [branch] [--print]` | jump to a worktree |
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 |
250
+ | `gji open [branch] [--select] [--editor <cli>] [--save] [--workspace]` | open the current or selected worktree in an editor |
251
+ | `gji go [branch] [--root] [--print] [--json]` | resolve and jump to a worktree, branch, remote, or PR |
237
252
  | `gji root [--print]` | jump to the main repo root |
238
253
  | `gji status [--json]` | repo overview, worktree health, ahead/behind |
239
254
  | `gji ls [--compact] [--json]` | list active worktrees |
240
255
  | `gji sync [--all]` | fetch and rebase worktrees onto default branch |
241
256
  | `gji sync-files [list\|add\|remove] [paths...]` | manage local files copied into new worktrees |
242
- | `gji clean [--stale] [--force] [--json]` | interactively prune linked worktrees |
243
- | `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 |
244
259
  | `gji trigger-hook <hook>` | run a hook in the current worktree |
245
260
  | `gji config [get\|set\|unset] [key] [value]` | manage global defaults |
246
261
  | `gji init [shell]` | interactively set up onboarding, or print/install a shell wrapper |
247
262
  | `gji doctor [--json] [--fix] [--yes]` | check installation and configuration health; optionally remove stale repository entries |
248
263
  | `gji completion [shell]` | print shell completion definitions |
249
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
+
250
274
  ## Configuration
251
275
 
252
276
  No setup required. Optional config lives in:
@@ -264,8 +288,11 @@ No setup required. Optional config lives in:
264
288
  | `syncRemote` | remote for `gji sync` (default: `origin`) |
265
289
  | `syncDefaultBranch` | branch to rebase onto (default: remote `HEAD`) |
266
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`) |
267
294
  | `skipInstallPrompt` | `true` to disable the auto-install prompt permanently |
268
- | `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` |
269
296
  | `hooks` | lifecycle scripts (see [Hooks](#hooks)) |
270
297
  | `repos` | per-repo overrides inside the global config (see below) |
271
298
 
@@ -274,7 +301,9 @@ No setup required. Optional config lives in:
274
301
  "branchPrefix": "feature/",
275
302
  "syncRemote": "upstream",
276
303
  "syncDefaultBranch": "main",
277
- "syncFiles": [".env.example", ".nvmrc"]
304
+ "syncFiles": [".env.example", ".nvmrc"],
305
+ "syncDirs": [".next"],
306
+ "dependencyBootstrap": "cow-then-repair"
278
307
  }
279
308
  ```
280
309
 
@@ -302,6 +331,42 @@ This stores:
302
331
  }
303
332
  ```
304
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
+
305
370
  ### Per-repo overrides in global config
306
371
 
307
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:
@@ -349,7 +414,7 @@ Run scripts automatically at key lifecycle moments:
349
414
  |---|---|
350
415
  | `afterCreate` | after `gji new` or `gji pr` creates a worktree |
351
416
  | `afterEnter` | after `gji go` switches to a worktree |
352
- | `beforeRemove` | before `gji remove` deletes a worktree |
417
+ | `beforeRemove` | before `gji done` or `gji clean` deletes a worktree |
353
418
 
354
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.
355
420
 
@@ -408,7 +473,9 @@ This is useful after cloning on a new machine, recovering a broken worktree, or
408
473
 
409
474
  ## Install prompt
410
475
 
411
- 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:
412
479
 
413
480
  ```
414
481
  Run `pnpm install` in the new worktree?
@@ -420,6 +487,8 @@ Run `pnpm install` in the new worktree?
420
487
 
421
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`.
422
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
+
423
492
  ## JSON output
424
493
 
425
494
  Every mutating command supports `--json` for scripting and AI agent use. Success goes to stdout, errors go to stderr with exit code 1.
@@ -427,18 +496,22 @@ Every mutating command supports `--json` for scripting and AI agent use. Success
427
496
  ```sh
428
497
  # create
429
498
  gji new --json feature/dark-mode
430
- # → { "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" } }
431
500
 
432
501
  # fetch PR
433
502
  gji pr --json 1234
434
- # → { "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" } }
435
508
 
436
509
  # detailed list
437
510
  gji ls --json
438
511
  # → [{ "branch": "...", "status": "clean", "upstream": { "kind": "tracked", ... }, ... }]
439
512
 
440
- # remove
441
- gji remove --json --force feature/dark-mode
513
+ # finish one worktree
514
+ gji done --json --force feature/dark-mode
442
515
  # → { "branch": "feature/dark-mode", "path": "/…", "deleted": true }
443
516
 
444
517
  # bulk clean
@@ -455,7 +528,7 @@ gji clean --stale --json --force
455
528
 
456
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.
457
530
 
458
- `--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.
459
532
 
460
533
  `gji ls --json` and `gji status --json` also produce structured output — see `gji status --json | jq` for the full schema.
461
534
 
@@ -463,7 +536,7 @@ gji clean --stale --json --force
463
536
 
464
537
  ```sh
465
538
  GJI_NO_TUI=1 gji new feature/ci-branch
466
- GJI_NO_TUI=1 gji remove --force feature/ci-branch
539
+ GJI_NO_TUI=1 gji done --force feature/ci-branch
467
540
  GJI_NO_TUI=1 gji clean --force
468
541
  ```
469
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;