@solaqua/gji 0.12.5 → 0.13.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 (214) 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} +46 -44
  8. package/dist/application/worktree/read-models.d.ts +31 -0
  9. package/dist/application/worktree/sources.d.ts +7 -0
  10. package/dist/{worktree-sources.js → application/worktree/sources.js} +12 -24
  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} +4 -2
  14. package/dist/{clean.js → cli/commands/clean.js} +32 -24
  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} +6 -1
  23. package/dist/{go.js → cli/commands/go.js} +57 -41
  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} +23 -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} +20 -10
  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 +15 -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 +7035 -6212
  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} +1 -51
  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} +7 -11
  141. package/dist/{worktree-picker.js → presentation/worktree/picker.js} +11 -182
  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/context-card.d.ts +0 -1
  168. package/dist/format-bytes.d.ts +0 -1
  169. package/dist/format-bytes.js +0 -14
  170. package/dist/git.d.ts +0 -18
  171. package/dist/repo.d.ts +0 -20
  172. package/dist/repo.js +0 -161
  173. package/dist/worktree-bootstrap.d.ts +0 -24
  174. package/dist/worktree-info.d.ts +0 -37
  175. package/dist/worktree-info.js +0 -123
  176. package/dist/worktree-sources.d.ts +0 -4
  177. /package/dist/{worktree-source.js → application/worktree/read-models.js} +0 -0
  178. /package/dist/{completion.d.ts → cli/commands/completion.d.ts} +0 -0
  179. /package/dist/{headless.d.ts → cli/runtime/headless.d.ts} +0 -0
  180. /package/dist/{headless.js → cli/runtime/headless.js} +0 -0
  181. /package/dist/{paths.d.ts → domain/shared/paths.d.ts} +0 -0
  182. /package/dist/{paths.js → domain/shared/paths.js} +0 -0
  183. /package/dist/{file-sync.d.ts → infrastructure/filesystem/file-sync.d.ts} +0 -0
  184. /package/dist/{file-sync.js → infrastructure/filesystem/file-sync.js} +0 -0
  185. /package/dist/{fs-utils.d.ts → infrastructure/filesystem/fs-utils.d.ts} +0 -0
  186. /package/dist/{fs-utils.js → infrastructure/filesystem/fs-utils.js} +0 -0
  187. /package/dist/{safe-destination.d.ts → infrastructure/filesystem/safe-destination.d.ts} +0 -0
  188. /package/dist/{safe-destination.js → infrastructure/filesystem/safe-destination.js} +0 -0
  189. /package/dist/{browser.d.ts → infrastructure/integrations/browser.d.ts} +0 -0
  190. /package/dist/{browser.js → infrastructure/integrations/browser.js} +0 -0
  191. /package/dist/{editor.js → infrastructure/integrations/editor.js} +0 -0
  192. /package/dist/{pull-requests.js → infrastructure/integrations/pull-requests.js} +0 -0
  193. /package/dist/{history.js → infrastructure/persistence/history.js} +0 -0
  194. /package/dist/{slots.d.ts → infrastructure/persistence/slots.d.ts} +0 -0
  195. /package/dist/{slots.js → infrastructure/persistence/slots.js} +0 -0
  196. /package/dist/{task.d.ts → infrastructure/persistence/task.d.ts} +0 -0
  197. /package/dist/{command-runner.js → infrastructure/process/command-runner.js} +0 -0
  198. /package/dist/{hooks.js → infrastructure/process/hooks.js} +0 -0
  199. /package/dist/{bootstrap-output.js → presentation/bootstrap/output.js} +0 -0
  200. /package/dist/{conflict.d.ts → presentation/prompts/path-conflict.d.ts} +0 -0
  201. /package/dist/{conflict.js → presentation/prompts/path-conflict.js} +0 -0
  202. /package/dist/{shell-completion.d.ts → presentation/shell/completion.d.ts} +0 -0
  203. /package/dist/{shell-handoff.d.ts → presentation/shell/handoff.d.ts} +0 -0
  204. /package/dist/{shell-handoff.js → presentation/shell/handoff.js} +0 -0
  205. /package/dist/{shell-setup.d.ts → presentation/shell/setup.d.ts} +0 -0
  206. /package/dist/{shell-setup.js → presentation/shell/setup.js} +0 -0
  207. /package/dist/{shell.d.ts → presentation/shell/shell.d.ts} +0 -0
  208. /package/dist/{shell.js → presentation/shell/shell.js} +0 -0
  209. /package/dist/{navigation-output.d.ts → presentation/terminal/navigation.d.ts} +0 -0
  210. /package/dist/{navigation-output.js → presentation/terminal/navigation.js} +0 -0
  211. /package/dist/{terminal-text.d.ts → presentation/terminal/text.d.ts} +0 -0
  212. /package/dist/{terminal-text.js → presentation/terminal/text.js} +0 -0
  213. /package/dist/{worktree-prompts.d.ts → presentation/worktree/prompts.d.ts} +0 -0
  214. /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,72 @@ 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
49
  let skippedRegisteredRepos = 0;
60
- const registeredSources = await listRegisteredWorktreeSources(cwd, () => {
50
+ const registeredSources = await listRegisteredWorktreeSources(cwd, sourceDependencies, () => {
61
51
  skippedRegisteredRepos++;
62
52
  });
63
53
  const crossRepoSources = registeredSources.filter((source) => source.repoRoot !== repository?.repoRoot);
64
- const crossMatches = await resolveExistingWorktreeMatches(crossRepoSources, branch, (!localBranch && !remoteBranch) || isPullRequestUrl(branch));
54
+ const crossMatches = await resolveExistingWorktreeMatches(crossRepoSources, branch, (!localBranch && !remoteBranch) || isPullRequestUrl(branch), repositoryPort);
65
55
  if (crossMatches.length === 1) {
66
- return { kind: "existing", source: crossMatches[0] };
56
+ return finish({ kind: "existing", source: crossMatches[0] }, false);
67
57
  }
68
58
  if (crossMatches.length > 1) {
69
- return { kind: "ambiguous", matches: crossMatches };
59
+ return finish({ kind: "ambiguous", matches: crossMatches }, false);
70
60
  }
71
61
  if (!repository) {
72
- return registeredSources.length === 0
62
+ return finish(registeredSources.length === 0
73
63
  ? {
74
64
  kind: "no-repository",
75
65
  staleRegisteredRepos: skippedRegisteredRepos > 0,
76
66
  }
77
- : { kind: "no-match", staleRegisteredRepos: skippedRegisteredRepos > 0 };
67
+ : {
68
+ kind: "no-match",
69
+ staleRegisteredRepos: skippedRegisteredRepos > 0,
70
+ });
78
71
  }
79
72
  if (localBranch) {
80
73
  if (configError)
81
- return configError;
82
- flushConfigWarnings();
83
- return { kind: "create", repository, branch, mode: "checkout" };
74
+ return finish(configError);
75
+ return finish({ kind: "create", repository, branch, mode: "checkout" });
84
76
  }
85
77
  if (remoteBranch) {
86
- flushConfigWarnings();
87
- return { kind: "create", repository, branch, mode: "track", remote };
78
+ return finish({
79
+ kind: "create",
80
+ repository,
81
+ branch,
82
+ mode: "track",
83
+ remote,
84
+ });
88
85
  }
89
86
  const localMatch = !pullRequestNumber || !isPullRequestUrl(branch)
90
87
  ? resolveWorktreeQuery(currentSources, branch)
91
88
  : null;
92
89
  if (localMatch) {
93
- return { kind: "existing", source: localMatch };
90
+ return finish({ kind: "existing", source: localMatch });
94
91
  }
95
92
  if (configError)
96
- return configError;
93
+ return finish(configError);
97
94
  if (pullRequestNumber !== null && !pullRequestBelongsToRepository) {
98
- flushConfigWarnings();
99
- return {
95
+ return finish({
100
96
  kind: "error",
101
97
  message: "PR URL does not belong to this repository; run gji go from the matching checkout",
102
- };
98
+ });
103
99
  }
104
100
  if (pullRequestNumber !== null) {
105
- flushConfigWarnings();
106
- return { kind: "pull-request", repository, input: branch };
101
+ return finish({ kind: "pull-request", repository, input: branch });
102
+ }
103
+ return finish({
104
+ kind: "no-match",
105
+ staleRegisteredRepos: skippedRegisteredRepos > 0,
106
+ });
107
+ function finish(resolution, includeWarnings = true) {
108
+ return !includeWarnings || configWarnings.length === 0
109
+ ? resolution
110
+ : { ...resolution, configWarnings };
107
111
  }
108
- flushConfigWarnings();
109
- return { kind: "no-match", staleRegisteredRepos: skippedRegisteredRepos > 0 };
110
112
  }
111
113
  function resolveExistingExactWorktreeMatches(sources, query, allowPullRequestFallback) {
112
114
  const exactMatches = resolveExactWorktreeQueryMatches(sources, query);
@@ -117,7 +119,7 @@ function resolveExistingExactWorktreeMatches(sources, query, allowPullRequestFal
117
119
  ? []
118
120
  : resolveExactWorktreeQueryMatches(sources, `pr/${pullRequestNumber}`);
119
121
  }
120
- async function resolveExistingWorktreeMatches(sources, query, allowPullRequestFallback) {
122
+ async function resolveExistingWorktreeMatches(sources, query, allowPullRequestFallback, repositoryPort) {
121
123
  const exactMatches = resolveExactWorktreeQueryMatches(sources, query);
122
124
  if (exactMatches.length > 0)
123
125
  return exactMatches;
@@ -132,7 +134,7 @@ async function resolveExistingWorktreeMatches(sources, query, allowPullRequestFa
132
134
  const repoRoot = source.repoRoot ?? source.worktree.path;
133
135
  let ownership = ownershipByRepository.get(repoRoot);
134
136
  if (ownership === undefined) {
135
- ownership = isPullRequestForRepository(repoRoot, query);
137
+ ownership = isPullRequestForRepository(repoRoot, query, repositoryPort);
136
138
  ownershipByRepository.set(repoRoot, ownership);
137
139
  }
138
140
  return (await ownership) ? source : null;
@@ -146,7 +148,7 @@ async function resolveExistingWorktreeMatches(sources, query, allowPullRequestFa
146
148
  function isPullRequestUrl(input) {
147
149
  return /^[a-z][a-z\d+.-]*:\/\//i.test(input);
148
150
  }
149
- async function isPullRequestForRepository(repoRoot, input) {
151
+ async function isPullRequestForRepository(repoRoot, input, repositoryPort) {
150
152
  if (/^\d+$/.test(input) || /^#\d+$/.test(input))
151
153
  return true;
152
154
  let pullRequestUrl;
@@ -156,7 +158,7 @@ async function isPullRequestForRepository(repoRoot, input) {
156
158
  catch {
157
159
  return true;
158
160
  }
159
- const remoteUrl = await getRepositoryRemoteUrl(repoRoot, "origin");
161
+ const remoteUrl = await repositoryPort.getRepositoryRemoteUrl(repoRoot, "origin");
160
162
  if (!remoteUrl)
161
163
  return true;
162
164
  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,7 @@
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
+ type WorktreeSourceDependencies = RepositoryContextPort & RepositoryRegistryPort & WorktreePort;
5
+ export declare function listRegisteredWorktreeSources(cwd: string, repositoryPort: WorktreeSourceDependencies, onSkipped?: (entry: RepoRegistryEntry) => void): Promise<WorktreeSource[]>;
6
+ export declare function listDiscoverableWorktreeSources(cwd: string, repositoryPort: WorktreeSourceDependencies, onSkipped?: (entry: RepoRegistryEntry) => void): Promise<WorktreeSource[]>;
7
+ export {};
@@ -1,14 +1,14 @@
1
- import { detectRepository, listWorktrees } from "./repo.js";
2
- import { loadRegistry } from "./repo-registry.js";
1
+ import { mapWithConcurrency } from "../../domain/shared/concurrency.js";
3
2
  const MAX_REPOSITORY_DISCOVERY_CONCURRENCY = 4;
4
- export async function listRegisteredWorktreeSources(cwd, onSkipped) {
5
- const registry = await loadRegistry();
6
- const currentRoot = await detectRepository(cwd)
3
+ export async function listRegisteredWorktreeSources(cwd, repositoryPort, onSkipped) {
4
+ const registry = await repositoryPort.loadRegistry();
5
+ const currentRoot = await repositoryPort
6
+ .detectRepository(cwd)
7
7
  .then((repository) => repository.currentRoot)
8
8
  .catch(() => null);
9
9
  const results = await mapWithConcurrency(registry, MAX_REPOSITORY_DISCOVERY_CONCURRENCY, async (entry) => {
10
10
  try {
11
- const worktrees = await listWorktrees(entry.path);
11
+ const worktrees = await repositoryPort.listWorktrees(entry.path);
12
12
  return { entry, worktrees };
13
13
  }
14
14
  catch {
@@ -34,14 +34,16 @@ export async function listRegisteredWorktreeSources(cwd, onSkipped) {
34
34
  }
35
35
  return allItems;
36
36
  }
37
- export async function listDiscoverableWorktreeSources(cwd, onSkipped) {
38
- const currentRepository = await detectRepository(cwd).catch(() => null);
39
- const registeredSources = await listRegisteredWorktreeSources(cwd, onSkipped);
37
+ export async function listDiscoverableWorktreeSources(cwd, repositoryPort, onSkipped) {
38
+ const currentRepository = await repositoryPort
39
+ .detectRepository(cwd)
40
+ .catch(() => null);
41
+ const registeredSources = await listRegisteredWorktreeSources(cwd, repositoryPort, onSkipped);
40
42
  if (currentRepository === null)
41
43
  return dedupeSources(registeredSources);
42
44
  let currentSources = [];
43
45
  try {
44
- currentSources = (await listWorktrees(cwd)).map((worktree) => ({
46
+ currentSources = (await repositoryPort.listWorktrees(cwd)).map((worktree) => ({
45
47
  repoName: currentRepository.repoName,
46
48
  repoRoot: currentRepository.repoRoot,
47
49
  worktree,
@@ -52,20 +54,6 @@ export async function listDiscoverableWorktreeSources(cwd, onSkipped) {
52
54
  }
53
55
  return dedupeSources([...currentSources, ...registeredSources]);
54
56
  }
55
- async function mapWithConcurrency(items, limit, mapper) {
56
- const results = new Array(items.length);
57
- let nextIndex = 0;
58
- async function readNext() {
59
- for (;;) {
60
- const index = nextIndex++;
61
- if (index >= items.length)
62
- return;
63
- results[index] = await mapper(items[index]);
64
- }
65
- }
66
- await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => readNext()));
67
- return results;
68
- }
69
57
  function dedupeSources(sources) {
70
58
  const seen = new Set();
71
59
  return sources.filter((source) => {
@@ -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,13 @@
1
- import type { WorktreeEntry } from "./repo.js";
2
- import { type WorktreePromptEntry } from "./worktree-picker.js";
1
+ import type { WorktreeEntry } from "../../domain/worktree/types.js";
2
+ import { type WorktreePromptEntry } from "../../presentation/worktree/picker.js";
3
+ import { type CliRuntime } from "../dependencies.js";
3
4
  export interface CleanCommandOptions {
4
5
  cwd: string;
5
6
  dryRun?: boolean;
6
7
  force?: boolean;
7
8
  json?: boolean;
8
9
  stale?: boolean;
10
+ runtime?: CliRuntime<"git" | "config" | "configStore" | "slots" | "repositoryContext" | "repositoryRegistry" | "worktrees" | "worktreeInfo" | "worktreeLifecycle" | "worktreeCatalog">;
9
11
  stderr: (chunk: string) => void;
10
12
  stdout: (chunk: string) => void;
11
13
  }