@solaqua/gji 0.12.5 → 0.14.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 (216) hide show
  1. package/README.md +12 -0
  2. package/dist/application/worktree/catalog.d.ts +31 -0
  3. package/dist/application/worktree/catalog.js +88 -0
  4. package/dist/application/worktree/context-card.d.ts +14 -0
  5. package/dist/application/worktree/context-card.js +12 -0
  6. package/dist/{go-resolver.d.ts → application/worktree/go-resolution.d.ts} +12 -4
  7. package/dist/{go-resolver.js → application/worktree/go-resolution.js} +50 -46
  8. package/dist/application/worktree/read-models.d.ts +31 -0
  9. package/dist/application/worktree/sources.d.ts +16 -0
  10. package/dist/application/worktree/sources.js +86 -0
  11. package/dist/{back.d.ts → cli/commands/back.d.ts} +3 -1
  12. package/dist/{back.js → cli/commands/back.js} +8 -6
  13. package/dist/{clean.d.ts → cli/commands/clean.d.ts} +6 -3
  14. package/dist/cli/commands/clean.js +555 -0
  15. package/dist/{completion.js → cli/commands/completion.js} +2 -2
  16. package/dist/{config-command.d.ts → cli/commands/config-command.d.ts} +2 -0
  17. package/dist/{config-command.js → cli/commands/config-command.js} +7 -5
  18. package/dist/{doctor.d.ts → cli/commands/doctor.d.ts} +2 -0
  19. package/dist/{doctor.js → cli/commands/doctor.js} +23 -25
  20. package/dist/{done.d.ts → cli/commands/done.d.ts} +2 -0
  21. package/dist/{done.js → cli/commands/done.js} +26 -23
  22. package/dist/{go.d.ts → cli/commands/go.d.ts} +8 -2
  23. package/dist/{go.js → cli/commands/go.js} +68 -48
  24. package/dist/{history-command.d.ts → cli/commands/history-command.d.ts} +2 -0
  25. package/dist/{history-command.js → cli/commands/history-command.js} +3 -1
  26. package/dist/{hub.d.ts → cli/commands/hub.d.ts} +7 -3
  27. package/dist/{hub.js → cli/commands/hub.js} +26 -33
  28. package/dist/{init.d.ts → cli/commands/init.d.ts} +3 -1
  29. package/dist/{init.js → cli/commands/init.js} +37 -34
  30. package/dist/{ls.d.ts → cli/commands/ls.d.ts} +4 -2
  31. package/dist/{ls.js → cli/commands/ls.js} +8 -5
  32. package/dist/{new.d.ts → cli/commands/new.d.ts} +6 -2
  33. package/dist/{new.js → cli/commands/new.js} +237 -109
  34. package/dist/{open.d.ts → cli/commands/open.d.ts} +4 -2
  35. package/dist/{open.js → cli/commands/open.js} +19 -12
  36. package/dist/{pr-open.d.ts → cli/commands/pr-open.d.ts} +4 -2
  37. package/dist/{pr-open.js → cli/commands/pr-open.js} +20 -18
  38. package/dist/{pr.d.ts → cli/commands/pr.d.ts} +5 -3
  39. package/dist/{pr.js → cli/commands/pr.js} +34 -41
  40. package/dist/{remove.d.ts → cli/commands/remove.d.ts} +4 -2
  41. package/dist/{remove.js → cli/commands/remove.js} +23 -17
  42. package/dist/{root.d.ts → cli/commands/root.d.ts} +2 -0
  43. package/dist/{root.js → cli/commands/root.js} +4 -2
  44. package/dist/{run-hook.d.ts → cli/commands/run-hook.d.ts} +2 -0
  45. package/dist/{run-hook.js → cli/commands/run-hook.js} +7 -4
  46. package/dist/{status.d.ts → cli/commands/status.d.ts} +2 -0
  47. package/dist/{status.js → cli/commands/status.js} +10 -7
  48. package/dist/{sync-files-command.d.ts → cli/commands/sync-files-command.d.ts} +2 -0
  49. package/dist/{sync-files-command.js → cli/commands/sync-files-command.js} +11 -9
  50. package/dist/{sync.d.ts → cli/commands/sync.d.ts} +2 -0
  51. package/dist/{sync.js → cli/commands/sync.js} +7 -4
  52. package/dist/{task-command.d.ts → cli/commands/task-command.d.ts} +2 -0
  53. package/dist/{task-command.js → cli/commands/task-command.js} +4 -2
  54. package/dist/{undo.d.ts → cli/commands/undo.d.ts} +8 -6
  55. package/dist/{undo.js → cli/commands/undo.js} +44 -34
  56. package/dist/{warp.d.ts → cli/commands/warp.d.ts} +5 -2
  57. package/dist/{warp.js → cli/commands/warp.js} +25 -13
  58. package/dist/cli/dependencies.d.ts +114 -0
  59. package/dist/cli/dependencies.js +118 -0
  60. package/dist/{cli.d.ts → cli/program.d.ts} +4 -2
  61. package/dist/{cli.js → cli/program.js} +108 -124
  62. package/dist/domain/repository/context.d.ts +6 -0
  63. package/dist/domain/repository/context.js +1 -0
  64. package/dist/domain/repository/registry.d.ts +5 -0
  65. package/dist/domain/repository/registry.js +1 -0
  66. package/dist/domain/shared/concurrency.d.ts +1 -0
  67. package/dist/domain/shared/concurrency.js +20 -0
  68. package/dist/domain/worktree/matching.d.ts +4 -0
  69. package/dist/domain/worktree/matching.js +79 -0
  70. package/dist/domain/worktree/policy.d.ts +2 -0
  71. package/dist/domain/worktree/policy.js +58 -0
  72. package/dist/domain/worktree/pr-reference.d.ts +1 -0
  73. package/dist/domain/worktree/pr-reference.js +9 -0
  74. package/dist/{worktree-source.d.ts → domain/worktree/source.d.ts} +1 -1
  75. package/dist/domain/worktree/source.js +1 -0
  76. package/dist/domain/worktree/types.d.ts +5 -0
  77. package/dist/domain/worktree/types.js +1 -0
  78. package/dist/gji-bundle.mjs +8756 -7666
  79. package/dist/index.js +3 -3
  80. package/dist/{dependency-bootstrap.d.ts → infrastructure/bootstrap/dependency-bootstrap.d.ts} +3 -30
  81. package/dist/{dependency-bootstrap.js → infrastructure/bootstrap/dependency-bootstrap.js} +4 -4
  82. package/dist/infrastructure/git/health.d.ts +9 -0
  83. package/dist/infrastructure/git/health.js +44 -0
  84. package/dist/infrastructure/git/refs.d.ts +6 -0
  85. package/dist/{git.js → infrastructure/git/refs.js} +4 -62
  86. package/dist/infrastructure/git/runner.d.ts +2 -0
  87. package/dist/infrastructure/git/runner.js +16 -0
  88. package/dist/{editor.d.ts → infrastructure/integrations/editor.d.ts} +2 -6
  89. package/dist/{pull-requests.d.ts → infrastructure/integrations/pull-requests.d.ts} +3 -11
  90. package/dist/{config.d.ts → infrastructure/persistence/config.d.ts} +3 -17
  91. package/dist/{config.js → infrastructure/persistence/config.js} +2 -17
  92. package/dist/{history.d.ts → infrastructure/persistence/history.d.ts} +2 -5
  93. package/dist/{task.js → infrastructure/persistence/task.js} +1 -1
  94. package/dist/infrastructure/process/command-runner.d.ts +3 -0
  95. package/dist/{hooks.d.ts → infrastructure/process/hooks.d.ts} +2 -12
  96. package/dist/infrastructure/repository/adapters.d.ts +7 -0
  97. package/dist/infrastructure/repository/adapters.js +23 -0
  98. package/dist/infrastructure/repository/context.d.ts +3 -0
  99. package/dist/infrastructure/repository/context.js +20 -0
  100. package/dist/infrastructure/repository/refs.d.ts +3 -0
  101. package/dist/infrastructure/repository/refs.js +37 -0
  102. package/dist/{repo-registry.d.ts → infrastructure/repository/registry.d.ts} +1 -5
  103. package/dist/{repo-registry.js → infrastructure/repository/registry.js} +1 -1
  104. package/dist/infrastructure/repository/worktrees.d.ts +2 -0
  105. package/dist/infrastructure/repository/worktrees.js +48 -0
  106. package/dist/{uv-validation.d.ts → infrastructure/validation/uv.d.ts} +1 -1
  107. package/dist/{uv-validation.js → infrastructure/validation/uv.js} +1 -1
  108. package/dist/infrastructure/worktree/bootstrap.d.ts +3 -0
  109. package/dist/{worktree-bootstrap.js → infrastructure/worktree/bootstrap.js} +7 -10
  110. package/dist/infrastructure/worktree/info.d.ts +6 -0
  111. package/dist/infrastructure/worktree/info.js +61 -0
  112. package/dist/{worktree-management.d.ts → infrastructure/worktree/lifecycle.d.ts} +0 -6
  113. package/dist/{worktree-management.js → infrastructure/worktree/lifecycle.js} +1 -9
  114. package/dist/ports/bootstrap.d.ts +61 -0
  115. package/dist/ports/bootstrap.js +1 -0
  116. package/dist/ports/config.d.ts +21 -0
  117. package/dist/ports/config.js +17 -0
  118. package/dist/ports/editor.d.ts +6 -0
  119. package/dist/ports/editor.js +1 -0
  120. package/dist/ports/git.d.ts +8 -0
  121. package/dist/ports/git.js +1 -0
  122. package/dist/ports/history.d.ts +5 -0
  123. package/dist/ports/history.js +1 -0
  124. package/dist/ports/hooks.d.ts +12 -0
  125. package/dist/ports/hooks.js +1 -0
  126. package/dist/{command-runner.d.ts → ports/process.d.ts} +0 -1
  127. package/dist/ports/process.js +1 -0
  128. package/dist/ports/pull-requests.d.ts +11 -0
  129. package/dist/ports/pull-requests.js +1 -0
  130. package/dist/ports/repository.d.ts +17 -0
  131. package/dist/ports/repository.js +1 -0
  132. package/dist/{bootstrap-output.d.ts → presentation/bootstrap/output.d.ts} +1 -1
  133. package/dist/presentation/bootstrap/preview.d.ts +2 -0
  134. package/dist/{bootstrap-preview.js → presentation/bootstrap/preview.js} +0 -4
  135. package/dist/{shell-completion.js → presentation/shell/completion.js} +5 -4
  136. package/dist/presentation/terminal/context-card.d.ts +2 -0
  137. package/dist/{context-card.js → presentation/terminal/context-card.js} +3 -12
  138. package/dist/presentation/worktree/format.d.ts +4 -0
  139. package/dist/presentation/worktree/format.js +41 -0
  140. package/dist/{worktree-picker.d.ts → presentation/worktree/picker.d.ts} +16 -17
  141. package/dist/{worktree-picker.js → presentation/worktree/picker.js} +30 -184
  142. package/man/man1/gji-back.1 +1 -1
  143. package/man/man1/gji-clean.1 +1 -1
  144. package/man/man1/gji-completion.1 +1 -1
  145. package/man/man1/gji-config.1 +1 -1
  146. package/man/man1/gji-doctor.1 +1 -1
  147. package/man/man1/gji-done.1 +1 -1
  148. package/man/man1/gji-go.1 +1 -1
  149. package/man/man1/gji-history.1 +1 -1
  150. package/man/man1/gji-init.1 +1 -1
  151. package/man/man1/gji-ls.1 +1 -1
  152. package/man/man1/gji-new.1 +6 -3
  153. package/man/man1/gji-open.1 +1 -1
  154. package/man/man1/gji-pr.1 +1 -1
  155. package/man/man1/gji-remove.1 +1 -1
  156. package/man/man1/gji-root.1 +1 -1
  157. package/man/man1/gji-run-hook.1 +1 -1
  158. package/man/man1/gji-status.1 +1 -1
  159. package/man/man1/gji-sync-files.1 +1 -1
  160. package/man/man1/gji-sync.1 +1 -1
  161. package/man/man1/gji-task.1 +1 -1
  162. package/man/man1/gji-undo.1 +1 -1
  163. package/man/man1/gji-warp.1 +1 -1
  164. package/man/man1/gji.1 +2 -2
  165. package/package.json +1 -1
  166. package/dist/bootstrap-preview.d.ts +0 -9
  167. package/dist/clean.js +0 -313
  168. package/dist/context-card.d.ts +0 -1
  169. package/dist/format-bytes.d.ts +0 -1
  170. package/dist/format-bytes.js +0 -14
  171. package/dist/git.d.ts +0 -18
  172. package/dist/repo.d.ts +0 -20
  173. package/dist/repo.js +0 -161
  174. package/dist/worktree-bootstrap.d.ts +0 -24
  175. package/dist/worktree-info.d.ts +0 -37
  176. package/dist/worktree-info.js +0 -123
  177. package/dist/worktree-sources.d.ts +0 -4
  178. package/dist/worktree-sources.js +0 -77
  179. /package/dist/{worktree-source.js → application/worktree/read-models.js} +0 -0
  180. /package/dist/{completion.d.ts → cli/commands/completion.d.ts} +0 -0
  181. /package/dist/{headless.d.ts → cli/runtime/headless.d.ts} +0 -0
  182. /package/dist/{headless.js → cli/runtime/headless.js} +0 -0
  183. /package/dist/{paths.d.ts → domain/shared/paths.d.ts} +0 -0
  184. /package/dist/{paths.js → domain/shared/paths.js} +0 -0
  185. /package/dist/{file-sync.d.ts → infrastructure/filesystem/file-sync.d.ts} +0 -0
  186. /package/dist/{file-sync.js → infrastructure/filesystem/file-sync.js} +0 -0
  187. /package/dist/{fs-utils.d.ts → infrastructure/filesystem/fs-utils.d.ts} +0 -0
  188. /package/dist/{fs-utils.js → infrastructure/filesystem/fs-utils.js} +0 -0
  189. /package/dist/{safe-destination.d.ts → infrastructure/filesystem/safe-destination.d.ts} +0 -0
  190. /package/dist/{safe-destination.js → infrastructure/filesystem/safe-destination.js} +0 -0
  191. /package/dist/{browser.d.ts → infrastructure/integrations/browser.d.ts} +0 -0
  192. /package/dist/{browser.js → infrastructure/integrations/browser.js} +0 -0
  193. /package/dist/{editor.js → infrastructure/integrations/editor.js} +0 -0
  194. /package/dist/{pull-requests.js → infrastructure/integrations/pull-requests.js} +0 -0
  195. /package/dist/{history.js → infrastructure/persistence/history.js} +0 -0
  196. /package/dist/{slots.d.ts → infrastructure/persistence/slots.d.ts} +0 -0
  197. /package/dist/{slots.js → infrastructure/persistence/slots.js} +0 -0
  198. /package/dist/{task.d.ts → infrastructure/persistence/task.d.ts} +0 -0
  199. /package/dist/{command-runner.js → infrastructure/process/command-runner.js} +0 -0
  200. /package/dist/{hooks.js → infrastructure/process/hooks.js} +0 -0
  201. /package/dist/{bootstrap-output.js → presentation/bootstrap/output.js} +0 -0
  202. /package/dist/{conflict.d.ts → presentation/prompts/path-conflict.d.ts} +0 -0
  203. /package/dist/{conflict.js → presentation/prompts/path-conflict.js} +0 -0
  204. /package/dist/{shell-completion.d.ts → presentation/shell/completion.d.ts} +0 -0
  205. /package/dist/{shell-handoff.d.ts → presentation/shell/handoff.d.ts} +0 -0
  206. /package/dist/{shell-handoff.js → presentation/shell/handoff.js} +0 -0
  207. /package/dist/{shell-setup.d.ts → presentation/shell/setup.d.ts} +0 -0
  208. /package/dist/{shell-setup.js → presentation/shell/setup.js} +0 -0
  209. /package/dist/{shell.d.ts → presentation/shell/shell.d.ts} +0 -0
  210. /package/dist/{shell.js → presentation/shell/shell.js} +0 -0
  211. /package/dist/{navigation-output.d.ts → presentation/terminal/navigation.d.ts} +0 -0
  212. /package/dist/{navigation-output.js → presentation/terminal/navigation.js} +0 -0
  213. /package/dist/{terminal-text.d.ts → presentation/terminal/text.d.ts} +0 -0
  214. /package/dist/{terminal-text.js → presentation/terminal/text.js} +0 -0
  215. /package/dist/{worktree-prompts.d.ts → presentation/worktree/prompts.d.ts} +0 -0
  216. /package/dist/{worktree-prompts.js → presentation/worktree/prompts.js} +0 -0
package/README.md CHANGED
@@ -22,6 +22,18 @@ directory. For repeatable workflows, you can still provide a branch directly:
22
22
  gji new feature/payment-refactor
23
23
  ```
24
24
 
25
+ If you prefer to reuse the current worktree, create a branch in place and
26
+ refresh the configured default branch first:
27
+
28
+ ```sh
29
+ gji new --branch-only feature/payment-refactor
30
+ ```
31
+
32
+ This creates the new branch from the latest configured remote default branch
33
+ without creating another worktree or installing dependencies. The current
34
+ worktree must be clean; use `--no-fetch` to create from the local default
35
+ branch as-is.
36
+
25
37
  ## The problem
26
38
 
27
39
  You are halfway through a feature when someone asks you to review a pull
@@ -0,0 +1,31 @@
1
+ import type { RepositoryContext } from "../../domain/repository/context.js";
2
+ import type { WorktreeSource } from "../../domain/worktree/source.js";
3
+ import type { WorktreeEntry } from "../../domain/worktree/types.js";
4
+ import type { PullRequestInfo, PullRequestPort } from "../../ports/pull-requests.js";
5
+ import type { RepositoryContextPort, WorktreePort } from "../../ports/repository.js";
6
+ import type { WorktreeInfo } from "./read-models.js";
7
+ export interface LinkedWorktreeContext {
8
+ linkedWorktrees: WorktreeEntry[];
9
+ repository: RepositoryContext;
10
+ }
11
+ export type WorktreeMetadataMode = "full" | "fast";
12
+ export interface HydratedWorktree {
13
+ info: WorktreeInfo;
14
+ lastUsedTimestamp: number | null;
15
+ pullRequests: PullRequestInfo[];
16
+ source: WorktreeSource;
17
+ }
18
+ export interface WorktreeCatalogDependencies {
19
+ loadHistory: () => Promise<ReadonlyArray<{
20
+ path: string;
21
+ timestamp: number;
22
+ }>>;
23
+ readTask: (worktreePath: string) => Promise<{
24
+ task: string;
25
+ } | null>;
26
+ readWorktreeInfos: (worktrees: WorktreeEntry[]) => Promise<WorktreeInfo[]>;
27
+ queryPullRequests?: PullRequestPort["listOpenPullRequests"];
28
+ queryRepositoryPullRequests?: PullRequestPort["listOpenPullRequestsForRepository"];
29
+ }
30
+ export declare function loadWorktreeCatalog(sources: WorktreeSource[], metadata: WorktreeMetadataMode, dependencies: WorktreeCatalogDependencies): Promise<HydratedWorktree[]>;
31
+ export declare function loadLinkedWorktrees(cwd: string, repositoryPort: RepositoryContextPort & WorktreePort): Promise<LinkedWorktreeContext>;
@@ -0,0 +1,88 @@
1
+ import { mapWithConcurrency } from "../../domain/shared/concurrency.js";
2
+ const MAX_PULL_REQUEST_REPOSITORY_QUERY_CONCURRENCY = 4;
3
+ const MAX_TASK_READ_CONCURRENCY = 8;
4
+ export async function loadWorktreeCatalog(sources, metadata, dependencies) {
5
+ const includeMetadata = metadata === "full";
6
+ const repositoryPullRequests = includeMetadata && dependencies.queryRepositoryPullRequests !== undefined
7
+ ? readRepositoryPullRequests(sources, dependencies.queryRepositoryPullRequests)
8
+ : Promise.resolve(null);
9
+ const [history, infos, pullRequestsByRepository] = await Promise.all([
10
+ dependencies.loadHistory(),
11
+ includeMetadata
12
+ ? dependencies.readWorktreeInfos(sources.map((source) => source.worktree))
13
+ : mapWithConcurrency(sources, MAX_TASK_READ_CONCURRENCY, async (source) => createUnhydratedWorktreeInfo(source.worktree, dependencies.readTask)),
14
+ repositoryPullRequests,
15
+ ]);
16
+ const pullRequests = includeMetadata
17
+ ? await Promise.all(sources.map((source) => readSourcePullRequests(source, dependencies.queryPullRequests, pullRequestsByRepository)))
18
+ : sources.map(() => []);
19
+ const historyByPath = new Map(history.map((entry) => [entry.path, entry]));
20
+ return sources.map((source, index) => ({
21
+ info: infos[index],
22
+ lastUsedTimestamp: historyByPath.get(source.worktree.path)?.timestamp ?? null,
23
+ pullRequests: pullRequests[index],
24
+ source,
25
+ }));
26
+ }
27
+ export async function loadLinkedWorktrees(cwd, repositoryPort) {
28
+ const repository = await repositoryPort.detectRepository(cwd);
29
+ const linkedWorktrees = (await repositoryPort.listWorktrees(cwd)).filter((worktree) => worktree.path !== repository.repoRoot);
30
+ return {
31
+ linkedWorktrees,
32
+ repository,
33
+ };
34
+ }
35
+ async function createUnhydratedWorktreeInfo(worktree, readTask) {
36
+ let task = null;
37
+ try {
38
+ task = (await readTask(worktree.path))?.task ?? null;
39
+ }
40
+ catch {
41
+ // Task metadata is optional picker decoration.
42
+ }
43
+ return {
44
+ ...worktree,
45
+ lastCommitTimestamp: null,
46
+ slot: null,
47
+ status: "unknown",
48
+ task,
49
+ upstream: { kind: "unknown" },
50
+ };
51
+ }
52
+ async function readSourcePullRequests(source, queryPullRequests, pullRequestsByRepository) {
53
+ if (source.repoRoot === undefined || source.worktree.branch === null) {
54
+ return [];
55
+ }
56
+ if (pullRequestsByRepository !== null) {
57
+ return sortPullRequests(pullRequestsByRepository
58
+ .get(source.repoRoot)
59
+ ?.filter((pullRequest) => pullRequest.sourceBranch === source.worktree.branch));
60
+ }
61
+ if (queryPullRequests === undefined)
62
+ return [];
63
+ try {
64
+ return sortPullRequests(await queryPullRequests(source.repoRoot, source.worktree.branch));
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ async function readRepositoryPullRequests(sources, queryPullRequests) {
71
+ const repoRoots = [
72
+ ...new Set(sources.flatMap((source) => source.repoRoot === undefined || source.worktree.branch === null
73
+ ? []
74
+ : [source.repoRoot])),
75
+ ];
76
+ const results = await mapWithConcurrency(repoRoots, MAX_PULL_REQUEST_REPOSITORY_QUERY_CONCURRENCY, async (repoRoot) => {
77
+ try {
78
+ return [repoRoot, sortPullRequests(await queryPullRequests(repoRoot))];
79
+ }
80
+ catch {
81
+ return [repoRoot, []];
82
+ }
83
+ });
84
+ return new Map(results);
85
+ }
86
+ function sortPullRequests(pullRequests) {
87
+ return [...(pullRequests ?? [])].sort((left, right) => left.number - right.number);
88
+ }
@@ -0,0 +1,14 @@
1
+ import type { WorktreeEntry } from "../../domain/worktree/types.js";
2
+ import type { WorktreeInfo } from "./read-models.js";
3
+ export interface ContextCardDependencies {
4
+ listWorktrees: (cwd: string) => Promise<WorktreeEntry[]>;
5
+ readTask: (worktreePath: string) => Promise<{
6
+ task: string;
7
+ } | null>;
8
+ readWorktreeInfo: (worktree: WorktreeEntry) => Promise<WorktreeInfo>;
9
+ }
10
+ export interface ContextCardModel {
11
+ info: WorktreeInfo;
12
+ task: string;
13
+ }
14
+ export declare function loadContextCardModel(worktreePath: string, dependencies: ContextCardDependencies): Promise<ContextCardModel | null>;
@@ -0,0 +1,12 @@
1
+ export async function loadContextCardModel(worktreePath, dependencies) {
2
+ const worktree = (await dependencies.listWorktrees(worktreePath)).find((entry) => entry.path === worktreePath);
3
+ if (!worktree)
4
+ return null;
5
+ const task = await dependencies.readTask(worktreePath);
6
+ if (!task)
7
+ return null;
8
+ return {
9
+ info: await dependencies.readWorktreeInfo(worktree),
10
+ task: task.task,
11
+ };
12
+ }
@@ -1,6 +1,8 @@
1
- import { type RepositoryContext } from "./repo.js";
2
- import type { WorktreeSource } from "./worktree-source.js";
3
- export type GoBranchResolution = {
1
+ import type { RepositoryContext } from "../../domain/repository/context.js";
2
+ import type { WorktreeSource } from "../../domain/worktree/source.js";
3
+ import type { ConfigPort } from "../../ports/config.js";
4
+ import type { RepositoryContextPort, RepositoryRefPort, RepositoryRegistryPort, WorktreePort } from "../../ports/repository.js";
5
+ type GoBranchResolutionBase = {
4
6
  kind: "existing";
5
7
  source: WorktreeSource;
6
8
  } | {
@@ -26,10 +28,16 @@ export type GoBranchResolution = {
26
28
  kind: "error";
27
29
  message: string;
28
30
  };
31
+ export type GoBranchResolution = GoBranchResolutionBase & {
32
+ configWarnings?: readonly string[];
33
+ };
29
34
  export declare function resolveGoBranch(options: {
30
35
  branch: string;
31
- configStderr?: (chunk: string) => void;
32
36
  cwd: string;
33
37
  currentSources: WorktreeSource[];
34
38
  repository: RepositoryContext | null;
39
+ repositoryPort: RepositoryRefPort;
40
+ sourceDependencies: RepositoryContextPort & RepositoryRegistryPort & WorktreePort;
41
+ configPort: ConfigPort;
35
42
  }): Promise<GoBranchResolution>;
43
+ export {};
@@ -1,17 +1,15 @@
1
- import { loadEffectiveConfig, resolveConfigString } from "./config.js";
2
- import { parsePrInput } from "./pr.js";
3
- import { getRepositoryRemoteUrl, hasLocalBranch, hasRemoteBranch, } from "./repo.js";
4
- import { resolveExactWorktreeQueryMatches, resolveWorktreeQuery, resolveWorktreeQueryMatches, } from "./worktree-picker.js";
5
- import { listRegisteredWorktreeSources } from "./worktree-sources.js";
1
+ import { resolveExactWorktreeQueryMatches, resolveWorktreeQuery, resolveWorktreeQueryMatches, } from "../../domain/worktree/matching.js";
2
+ import { parsePrInput } from "../../domain/worktree/pr-reference.js";
3
+ import { listRegisteredWorktreeSources } from "./sources.js";
6
4
  export async function resolveGoBranch(options) {
7
- const { branch, configStderr, cwd, currentSources, repository } = options;
5
+ const { branch, configPort, cwd, currentSources, repository, repositoryPort, sourceDependencies, } = options;
8
6
  const pullRequestNumber = parsePrInput(branch);
9
7
  const exactCurrentMatches = resolveExistingExactWorktreeMatches(currentSources, branch, false);
10
8
  if (exactCurrentMatches.length === 1) {
11
9
  return { kind: "existing", source: exactCurrentMatches[0] };
12
10
  }
13
11
  const localBranch = repository
14
- ? await hasLocalBranch(repository.repoRoot, branch)
12
+ ? await repositoryPort.hasLocalBranch(repository.repoRoot, branch)
15
13
  : false;
16
14
  if (!repository && pullRequestNumber !== null) {
17
15
  return {
@@ -25,11 +23,9 @@ export async function resolveGoBranch(options) {
25
23
  const configWarnings = [];
26
24
  if (repository) {
27
25
  try {
28
- const config = await loadEffectiveConfig(repository.repoRoot, undefined, configStderr === undefined
29
- ? undefined
30
- : (warning) => configWarnings.push(warning));
31
- remote = resolveConfigString(config, "syncRemote") ?? "origin";
32
- remoteBranch = await hasRemoteBranch(repository.repoRoot, remote, branch);
26
+ const config = await configPort.loadEffectiveConfig(repository.repoRoot, undefined, (warning) => configWarnings.push(warning));
27
+ remote = configPort.resolveConfigString(config, "syncRemote") ?? "origin";
28
+ remoteBranch = await repositoryPort.hasRemoteBranch(repository.repoRoot, remote, branch);
33
29
  }
34
30
  catch (error) {
35
31
  configError = {
@@ -38,14 +34,8 @@ export async function resolveGoBranch(options) {
38
34
  };
39
35
  }
40
36
  }
41
- const flushConfigWarnings = () => {
42
- if (configStderr === undefined)
43
- return;
44
- for (const warning of configWarnings)
45
- configStderr(warning);
46
- };
47
37
  const pullRequestBelongsToRepository = repository && pullRequestNumber !== null
48
- ? await isPullRequestForRepository(repository.repoRoot, branch)
38
+ ? await isPullRequestForRepository(repository.repoRoot, branch, repositoryPort)
49
39
  : true;
50
40
  if (pullRequestNumber !== null &&
51
41
  !localBranch &&
@@ -53,60 +43,74 @@ export async function resolveGoBranch(options) {
53
43
  pullRequestBelongsToRepository) {
54
44
  const pullRequestMatches = resolveExistingExactWorktreeMatches(currentSources, branch, true);
55
45
  if (pullRequestMatches.length === 1) {
56
- return { kind: "existing", source: pullRequestMatches[0] };
46
+ return finish({ kind: "existing", source: pullRequestMatches[0] }, false);
57
47
  }
58
48
  }
59
- let skippedRegisteredRepos = 0;
60
- const registeredSources = await listRegisteredWorktreeSources(cwd, () => {
61
- skippedRegisteredRepos++;
49
+ const registered = await listRegisteredWorktreeSources({
50
+ cwd,
51
+ repositoryPort: sourceDependencies,
62
52
  });
53
+ const registeredSources = registered.sources;
54
+ const skippedRegisteredRepos = registered.skipped.length;
63
55
  const crossRepoSources = registeredSources.filter((source) => source.repoRoot !== repository?.repoRoot);
64
- const crossMatches = await resolveExistingWorktreeMatches(crossRepoSources, branch, (!localBranch && !remoteBranch) || isPullRequestUrl(branch));
56
+ const crossMatches = await resolveExistingWorktreeMatches(crossRepoSources, branch, (!localBranch && !remoteBranch) || isPullRequestUrl(branch), repositoryPort);
65
57
  if (crossMatches.length === 1) {
66
- return { kind: "existing", source: crossMatches[0] };
58
+ return finish({ kind: "existing", source: crossMatches[0] }, false);
67
59
  }
68
60
  if (crossMatches.length > 1) {
69
- return { kind: "ambiguous", matches: crossMatches };
61
+ return finish({ kind: "ambiguous", matches: crossMatches }, false);
70
62
  }
71
63
  if (!repository) {
72
- return registeredSources.length === 0
64
+ return finish(registeredSources.length === 0
73
65
  ? {
74
66
  kind: "no-repository",
75
67
  staleRegisteredRepos: skippedRegisteredRepos > 0,
76
68
  }
77
- : { kind: "no-match", staleRegisteredRepos: skippedRegisteredRepos > 0 };
69
+ : {
70
+ kind: "no-match",
71
+ staleRegisteredRepos: skippedRegisteredRepos > 0,
72
+ });
78
73
  }
79
74
  if (localBranch) {
80
75
  if (configError)
81
- return configError;
82
- flushConfigWarnings();
83
- return { kind: "create", repository, branch, mode: "checkout" };
76
+ return finish(configError);
77
+ return finish({ kind: "create", repository, branch, mode: "checkout" });
84
78
  }
85
79
  if (remoteBranch) {
86
- flushConfigWarnings();
87
- return { kind: "create", repository, branch, mode: "track", remote };
80
+ return finish({
81
+ kind: "create",
82
+ repository,
83
+ branch,
84
+ mode: "track",
85
+ remote,
86
+ });
88
87
  }
89
88
  const localMatch = !pullRequestNumber || !isPullRequestUrl(branch)
90
89
  ? resolveWorktreeQuery(currentSources, branch)
91
90
  : null;
92
91
  if (localMatch) {
93
- return { kind: "existing", source: localMatch };
92
+ return finish({ kind: "existing", source: localMatch });
94
93
  }
95
94
  if (configError)
96
- return configError;
95
+ return finish(configError);
97
96
  if (pullRequestNumber !== null && !pullRequestBelongsToRepository) {
98
- flushConfigWarnings();
99
- return {
97
+ return finish({
100
98
  kind: "error",
101
99
  message: "PR URL does not belong to this repository; run gji go from the matching checkout",
102
- };
100
+ });
103
101
  }
104
102
  if (pullRequestNumber !== null) {
105
- flushConfigWarnings();
106
- return { kind: "pull-request", repository, input: branch };
103
+ return finish({ kind: "pull-request", repository, input: branch });
104
+ }
105
+ return finish({
106
+ kind: "no-match",
107
+ staleRegisteredRepos: skippedRegisteredRepos > 0,
108
+ });
109
+ function finish(resolution, includeWarnings = true) {
110
+ return !includeWarnings || configWarnings.length === 0
111
+ ? resolution
112
+ : { ...resolution, configWarnings };
107
113
  }
108
- flushConfigWarnings();
109
- return { kind: "no-match", staleRegisteredRepos: skippedRegisteredRepos > 0 };
110
114
  }
111
115
  function resolveExistingExactWorktreeMatches(sources, query, allowPullRequestFallback) {
112
116
  const exactMatches = resolveExactWorktreeQueryMatches(sources, query);
@@ -117,7 +121,7 @@ function resolveExistingExactWorktreeMatches(sources, query, allowPullRequestFal
117
121
  ? []
118
122
  : resolveExactWorktreeQueryMatches(sources, `pr/${pullRequestNumber}`);
119
123
  }
120
- async function resolveExistingWorktreeMatches(sources, query, allowPullRequestFallback) {
124
+ async function resolveExistingWorktreeMatches(sources, query, allowPullRequestFallback, repositoryPort) {
121
125
  const exactMatches = resolveExactWorktreeQueryMatches(sources, query);
122
126
  if (exactMatches.length > 0)
123
127
  return exactMatches;
@@ -132,7 +136,7 @@ async function resolveExistingWorktreeMatches(sources, query, allowPullRequestFa
132
136
  const repoRoot = source.repoRoot ?? source.worktree.path;
133
137
  let ownership = ownershipByRepository.get(repoRoot);
134
138
  if (ownership === undefined) {
135
- ownership = isPullRequestForRepository(repoRoot, query);
139
+ ownership = isPullRequestForRepository(repoRoot, query, repositoryPort);
136
140
  ownershipByRepository.set(repoRoot, ownership);
137
141
  }
138
142
  return (await ownership) ? source : null;
@@ -146,7 +150,7 @@ async function resolveExistingWorktreeMatches(sources, query, allowPullRequestFa
146
150
  function isPullRequestUrl(input) {
147
151
  return /^[a-z][a-z\d+.-]*:\/\//i.test(input);
148
152
  }
149
- async function isPullRequestForRepository(repoRoot, input) {
153
+ async function isPullRequestForRepository(repoRoot, input, repositoryPort) {
150
154
  if (/^\d+$/.test(input) || /^#\d+$/.test(input))
151
155
  return true;
152
156
  let pullRequestUrl;
@@ -156,7 +160,7 @@ async function isPullRequestForRepository(repoRoot, input) {
156
160
  catch {
157
161
  return true;
158
162
  }
159
- const remoteUrl = await getRepositoryRemoteUrl(repoRoot, "origin");
163
+ const remoteUrl = await repositoryPort.getRepositoryRemoteUrl(repoRoot, "origin");
160
164
  if (!remoteUrl)
161
165
  return true;
162
166
  return (normalizeRepositoryUrl(remoteUrl) ===
@@ -0,0 +1,31 @@
1
+ import type { WorktreeEntry } from "../../domain/worktree/types.js";
2
+ export type WorktreeStatus = "clean" | "dirty" | "unknown";
3
+ export type UpstreamState = {
4
+ kind: "detached";
5
+ } | {
6
+ kind: "no-upstream";
7
+ } | {
8
+ kind: "stale";
9
+ } | {
10
+ kind: "tracked";
11
+ ahead: number;
12
+ behind: number;
13
+ } | {
14
+ kind: "unknown";
15
+ };
16
+ export interface WorktreeInfo extends WorktreeEntry {
17
+ lastCommitTimestamp: number | null;
18
+ slot: number | null;
19
+ status: WorktreeStatus;
20
+ task: string | null;
21
+ upstream: UpstreamState;
22
+ }
23
+ export interface SerializedWorktreeInfo {
24
+ branch: string | null;
25
+ lastCommitTimestamp: number | null;
26
+ path: string;
27
+ slot: number | null;
28
+ status: WorktreeStatus;
29
+ task: string | null;
30
+ upstream: UpstreamState;
31
+ }
@@ -0,0 +1,16 @@
1
+ import type { RepoRegistryEntry } from "../../domain/repository/registry.js";
2
+ import type { WorktreeSource } from "../../domain/worktree/source.js";
3
+ import type { RepositoryContextPort, RepositoryRegistryPort, WorktreePort } from "../../ports/repository.js";
4
+ export type WorktreeSourceDependencies = RepositoryContextPort & RepositoryRegistryPort & WorktreePort;
5
+ export interface WorktreeSourceDiscovery {
6
+ skipped: RepoRegistryEntry[];
7
+ sources: WorktreeSource[];
8
+ }
9
+ export interface ListWorktreeSourcesOptions {
10
+ cwd: string;
11
+ repositoryPort: WorktreeSourceDependencies;
12
+ signal?: AbortSignal;
13
+ }
14
+ export declare function listRegisteredWorktreeSources(options: ListWorktreeSourcesOptions): Promise<WorktreeSourceDiscovery>;
15
+ export declare function listDiscoverableWorktreeSources(options: ListWorktreeSourcesOptions): Promise<WorktreeSourceDiscovery>;
16
+ export declare function deduplicateWorktreeSources(sources: WorktreeSource[]): WorktreeSource[];
@@ -0,0 +1,86 @@
1
+ import { mapWithConcurrency } from "../../domain/shared/concurrency.js";
2
+ const MAX_REPOSITORY_DISCOVERY_CONCURRENCY = 4;
3
+ export async function listRegisteredWorktreeSources(options) {
4
+ const { cwd, repositoryPort, signal } = options;
5
+ throwIfAborted(signal);
6
+ const registry = await repositoryPort.loadRegistry();
7
+ const currentRoot = await repositoryPort
8
+ .detectRepository(cwd)
9
+ .then((repository) => repository.currentRoot)
10
+ .catch(() => null);
11
+ throwIfAborted(signal);
12
+ const results = await mapWithConcurrency(registry, MAX_REPOSITORY_DISCOVERY_CONCURRENCY, async (entry) => {
13
+ throwIfAborted(signal);
14
+ try {
15
+ const worktrees = await repositoryPort.listWorktrees(entry.path);
16
+ return { entry, skipped: false, worktrees };
17
+ }
18
+ catch {
19
+ return { entry, skipped: true, worktrees: [] };
20
+ }
21
+ }, signal);
22
+ const allItems = [];
23
+ const skipped = [];
24
+ for (const result of results) {
25
+ if (result.skipped) {
26
+ skipped.push(result.entry);
27
+ continue;
28
+ }
29
+ const { entry, worktrees } = result;
30
+ for (const worktree of worktrees) {
31
+ allItems.push({
32
+ repoRoot: entry.path,
33
+ repoName: entry.name,
34
+ worktree: {
35
+ ...worktree,
36
+ isCurrent: currentRoot !== null && worktree.path === currentRoot,
37
+ },
38
+ });
39
+ }
40
+ }
41
+ return { skipped, sources: allItems };
42
+ }
43
+ export async function listDiscoverableWorktreeSources(options) {
44
+ const { cwd, repositoryPort } = options;
45
+ const currentRepository = await repositoryPort
46
+ .detectRepository(cwd)
47
+ .catch(() => null);
48
+ const registered = await listRegisteredWorktreeSources(options);
49
+ if (currentRepository === null) {
50
+ return {
51
+ skipped: registered.skipped,
52
+ sources: deduplicateWorktreeSources(registered.sources),
53
+ };
54
+ }
55
+ let currentSources = [];
56
+ try {
57
+ currentSources = (await repositoryPort.listWorktrees(cwd)).map((worktree) => ({
58
+ repoName: currentRepository.repoName,
59
+ repoRoot: currentRepository.repoRoot,
60
+ worktree,
61
+ }));
62
+ }
63
+ catch {
64
+ // Registered repositories remain discoverable when the current checkout is transiently unavailable.
65
+ }
66
+ return {
67
+ skipped: registered.skipped,
68
+ sources: deduplicateWorktreeSources([
69
+ ...currentSources,
70
+ ...registered.sources,
71
+ ]),
72
+ };
73
+ }
74
+ export function deduplicateWorktreeSources(sources) {
75
+ const seen = new Set();
76
+ return sources.filter((source) => {
77
+ if (seen.has(source.worktree.path))
78
+ return false;
79
+ seen.add(source.worktree.path);
80
+ return true;
81
+ });
82
+ }
83
+ function throwIfAborted(signal) {
84
+ if (signal?.aborted)
85
+ throw new Error("Operation cancelled");
86
+ }
@@ -1,4 +1,5 @@
1
- import { type HistoryEntry } from "./history.js";
1
+ import type { HistoryEntry } from "../../ports/history.js";
2
+ import { type CliRuntime } from "../dependencies.js";
2
3
  export declare const BACK_OUTPUT_FILE_ENV = "GJI_BACK_OUTPUT_FILE";
3
4
  export interface BackCommandOptions {
4
5
  cwd: string;
@@ -8,6 +9,7 @@ export interface BackCommandOptions {
8
9
  commandName?: string;
9
10
  outputEnv?: string;
10
11
  print?: boolean;
12
+ runtime?: CliRuntime<"config" | "historyStore" | "hooks" | "repositoryContext">;
11
13
  stderr: (chunk: string) => void;
12
14
  stdout: (chunk: string) => void;
13
15
  }
@@ -1,13 +1,15 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import { basename } from "node:path";
3
- import { loadEffectiveConfig } from "./config.js";
4
- import { appendHistory, loadHistory } from "./history.js";
5
- import { extractHooks, runHook } from "./hooks.js";
6
- import { createNavigationRepository, createNavigationTarget, } from "./navigation-output.js";
7
- import { detectRepository } from "./repo.js";
8
- import { writeShellOutput } from "./shell-handoff.js";
3
+ import { writeShellOutput } from "../../presentation/shell/handoff.js";
4
+ import { createNavigationRepository, createNavigationTarget, } from "../../presentation/terminal/navigation.js";
5
+ import { defaultCliDependencies } from "../dependencies.js";
9
6
  export const BACK_OUTPUT_FILE_ENV = "GJI_BACK_OUTPUT_FILE";
10
7
  export async function runBackCommand(options) {
8
+ const runtime = options.runtime ?? defaultCliDependencies;
9
+ const { loadEffectiveConfig } = runtime.config;
10
+ const { appendHistory, loadHistory } = runtime.historyStore;
11
+ const { extractHooks, runHook } = runtime.hooks;
12
+ const { detectRepository } = runtime.repositoryContext;
11
13
  const history = await loadHistory(options.home);
12
14
  const steps = options.n ?? 1;
13
15
  if (steps < 1) {
@@ -1,11 +1,14 @@
1
- import type { WorktreeEntry } from "./repo.js";
2
- import { type WorktreePromptEntry } from "./worktree-picker.js";
1
+ import { type WorktreeCatalogDependencies } from "../../application/worktree/catalog.js";
2
+ import type { WorktreeEntry } from "../../domain/worktree/types.js";
3
+ import { type WorktreePromptEntry, type WorktreePromptScope } from "../../presentation/worktree/picker.js";
4
+ import { type CliRuntime } from "../dependencies.js";
3
5
  export interface CleanCommandOptions {
4
6
  cwd: string;
5
7
  dryRun?: boolean;
6
8
  force?: boolean;
7
9
  json?: boolean;
8
10
  stale?: boolean;
11
+ runtime?: CliRuntime<"git" | "config" | "configStore" | "slots" | "repositoryContext" | "repositoryRegistry" | "worktrees" | "worktreeInfo" | "worktreeLifecycle" | "worktreeCatalog">;
9
12
  stderr: (chunk: string) => void;
10
13
  stdout: (chunk: string) => void;
11
14
  }
@@ -13,7 +16,7 @@ export interface CleanCommandDependencies {
13
16
  confirmForceDeleteBranch: (branch: string) => Promise<boolean>;
14
17
  confirmForceRemoveWorktree: (worktreePath: string) => Promise<boolean>;
15
18
  confirmRemoval: (worktrees: WorktreeEntry[]) => Promise<boolean>;
16
- promptForWorktrees: (worktrees: WorktreePromptEntry[]) => Promise<string[] | null>;
19
+ promptForWorktrees: (worktrees: WorktreePromptEntry[], scope?: WorktreePromptScope, catalog?: WorktreeCatalogDependencies) => Promise<string[] | null>;
17
20
  }
18
21
  export declare function createCleanCommand(dependencies?: Partial<CleanCommandDependencies>): (options: CleanCommandOptions) => Promise<number>;
19
22
  export declare const runCleanCommand: (options: CleanCommandOptions) => Promise<number>;