@solaqua/gji 0.10.0 → 0.12.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 (122) hide show
  1. package/README.md +83 -426
  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 +27 -3
  9. package/dist/cli.d.ts +4 -0
  10. package/dist/cli.js +140 -22
  11. package/dist/clone-failure-store.d.ts +18 -0
  12. package/dist/clone-failure-store.js +230 -0
  13. package/dist/command-runner.d.ts +6 -0
  14. package/dist/command-runner.js +38 -0
  15. package/dist/completion.js +7 -2
  16. package/dist/config-command.d.ts +1 -0
  17. package/dist/config-command.js +7 -1
  18. package/dist/config.d.ts +26 -1
  19. package/dist/config.js +110 -6
  20. package/dist/context-card.d.ts +1 -0
  21. package/dist/context-card.js +25 -0
  22. package/dist/dependency-bootstrap-prompt.d.ts +24 -0
  23. package/dist/dependency-bootstrap-prompt.js +103 -0
  24. package/dist/dependency-bootstrap.d.ts +129 -0
  25. package/dist/dependency-bootstrap.js +566 -0
  26. package/dist/dir-clone.d.ts +33 -0
  27. package/dist/dir-clone.js +505 -0
  28. package/dist/doctor.js +21 -0
  29. package/dist/done.d.ts +10 -0
  30. package/dist/done.js +210 -0
  31. package/dist/file-sync.js +71 -22
  32. package/dist/format-bytes.d.ts +1 -0
  33. package/dist/format-bytes.js +14 -0
  34. package/dist/fs-utils.d.ts +4 -0
  35. package/dist/fs-utils.js +26 -0
  36. package/dist/git.d.ts +5 -0
  37. package/dist/git.js +50 -0
  38. package/dist/gji-bundle.mjs +6189 -1313
  39. package/dist/go-resolver.d.ts +35 -0
  40. package/dist/go-resolver.js +185 -0
  41. package/dist/go.d.ts +6 -2
  42. package/dist/go.js +270 -62
  43. package/dist/hooks.d.ts +2 -1
  44. package/dist/hooks.js +20 -8
  45. package/dist/hub.d.ts +31 -0
  46. package/dist/hub.js +287 -0
  47. package/dist/init.js +30 -22
  48. package/dist/install-prompt.d.ts +1 -0
  49. package/dist/install-prompt.js +3 -25
  50. package/dist/ls.js +9 -1
  51. package/dist/navigation-output.d.ts +11 -0
  52. package/dist/navigation-output.js +6 -0
  53. package/dist/new.d.ts +14 -2
  54. package/dist/new.js +422 -38
  55. package/dist/pr-open.js +2 -10
  56. package/dist/pr.d.ts +6 -2
  57. package/dist/pr.js +135 -30
  58. package/dist/pull-requests.d.ts +1 -0
  59. package/dist/pull-requests.js +32 -3
  60. package/dist/remove.js +22 -0
  61. package/dist/repo.d.ts +4 -0
  62. package/dist/repo.js +61 -6
  63. package/dist/run-hook.js +2 -0
  64. package/dist/safe-destination.d.ts +15 -0
  65. package/dist/safe-destination.js +166 -0
  66. package/dist/shell-completion.js +52 -13
  67. package/dist/slots.d.ts +8 -0
  68. package/dist/slots.js +61 -0
  69. package/dist/status.d.ts +2 -0
  70. package/dist/status.js +9 -1
  71. package/dist/sync-directories.d.ts +33 -0
  72. package/dist/sync-directories.js +97 -0
  73. package/dist/sync-plan.d.ts +16 -0
  74. package/dist/sync-plan.js +128 -0
  75. package/dist/sync.js +7 -13
  76. package/dist/task-command.d.ts +9 -0
  77. package/dist/task-command.js +29 -0
  78. package/dist/task.d.ts +9 -0
  79. package/dist/task.js +37 -0
  80. package/dist/terminal-text.d.ts +4 -0
  81. package/dist/terminal-text.js +72 -0
  82. package/dist/undo.d.ts +36 -0
  83. package/dist/undo.js +392 -0
  84. package/dist/warp.d.ts +3 -1
  85. package/dist/warp.js +32 -125
  86. package/dist/worktree-bootstrap.d.ts +35 -0
  87. package/dist/worktree-bootstrap.js +96 -0
  88. package/dist/worktree-info.d.ts +6 -1
  89. package/dist/worktree-info.js +14 -4
  90. package/dist/worktree-management.d.ts +1 -0
  91. package/dist/worktree-management.js +23 -4
  92. package/dist/worktree-picker.d.ts +24 -11
  93. package/dist/worktree-picker.js +176 -95
  94. package/dist/worktree-prompts.js +1 -1
  95. package/dist/worktree-source.d.ts +6 -0
  96. package/dist/worktree-source.js +1 -0
  97. package/dist/worktree-sources.d.ts +4 -0
  98. package/dist/worktree-sources.js +77 -0
  99. package/man/man1/gji-back.1 +1 -1
  100. package/man/man1/gji-clean.1 +1 -1
  101. package/man/man1/gji-completion.1 +1 -1
  102. package/man/man1/gji-config.1 +1 -1
  103. package/man/man1/gji-doctor.1 +1 -1
  104. package/man/man1/gji-done.1 +19 -0
  105. package/man/man1/gji-go.1 +12 -3
  106. package/man/man1/gji-history.1 +1 -1
  107. package/man/man1/gji-init.1 +1 -1
  108. package/man/man1/gji-ls.1 +1 -1
  109. package/man/man1/gji-new.1 +21 -3
  110. package/man/man1/gji-open.1 +1 -1
  111. package/man/man1/gji-pr.1 +7 -1
  112. package/man/man1/gji-remove.1 +3 -3
  113. package/man/man1/gji-root.1 +1 -1
  114. package/man/man1/gji-run-hook.1 +1 -1
  115. package/man/man1/gji-status.1 +1 -1
  116. package/man/man1/gji-sync-files.1 +1 -1
  117. package/man/man1/gji-sync.1 +1 -1
  118. package/man/man1/gji-task.1 +16 -0
  119. package/man/man1/gji-undo.1 +16 -0
  120. package/man/man1/gji-warp.1 +3 -6
  121. package/man/man1/gji.1 +17 -5
  122. package/package.json +3 -1
@@ -0,0 +1,35 @@
1
+ import { type RepositoryContext } from "./repo.js";
2
+ import type { WorktreeSource } from "./worktree-source.js";
3
+ export type GoBranchResolution = {
4
+ kind: "existing";
5
+ source: WorktreeSource;
6
+ } | {
7
+ kind: "ambiguous";
8
+ matches: WorktreeSource[];
9
+ } | {
10
+ kind: "create";
11
+ repository: RepositoryContext;
12
+ branch: string;
13
+ mode: "checkout" | "track";
14
+ remote?: string;
15
+ } | {
16
+ kind: "pull-request";
17
+ repository: RepositoryContext;
18
+ input: string;
19
+ } | {
20
+ kind: "no-repository";
21
+ staleRegisteredRepos: boolean;
22
+ } | {
23
+ kind: "no-match";
24
+ staleRegisteredRepos: boolean;
25
+ } | {
26
+ kind: "error";
27
+ message: string;
28
+ };
29
+ export declare function resolveGoBranch(options: {
30
+ branch: string;
31
+ configStderr?: (chunk: string) => void;
32
+ cwd: string;
33
+ currentSources: WorktreeSource[];
34
+ repository: RepositoryContext | null;
35
+ }): Promise<GoBranchResolution>;
@@ -0,0 +1,185 @@
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";
6
+ export async function resolveGoBranch(options) {
7
+ const { branch, configStderr, cwd, currentSources, repository } = options;
8
+ const pullRequestNumber = parsePrInput(branch);
9
+ const exactCurrentMatches = resolveExistingExactWorktreeMatches(currentSources, branch, false);
10
+ if (exactCurrentMatches.length === 1) {
11
+ return { kind: "existing", source: exactCurrentMatches[0] };
12
+ }
13
+ const localBranch = repository
14
+ ? await hasLocalBranch(repository.repoRoot, branch)
15
+ : false;
16
+ if (!repository && pullRequestNumber !== null) {
17
+ return {
18
+ kind: "error",
19
+ message: "PR references must be resolved from inside a git repository",
20
+ };
21
+ }
22
+ let remote;
23
+ let remoteBranch = false;
24
+ let configError = null;
25
+ const configWarnings = [];
26
+ if (repository) {
27
+ 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);
33
+ }
34
+ catch (error) {
35
+ configError = {
36
+ kind: "error",
37
+ message: `could not load repository config: ${error instanceof Error ? error.message : String(error)}`,
38
+ };
39
+ }
40
+ }
41
+ const flushConfigWarnings = () => {
42
+ if (configStderr === undefined)
43
+ return;
44
+ for (const warning of configWarnings)
45
+ configStderr(warning);
46
+ };
47
+ const pullRequestBelongsToRepository = repository && pullRequestNumber !== null
48
+ ? await isPullRequestForRepository(repository.repoRoot, branch)
49
+ : true;
50
+ if (pullRequestNumber !== null &&
51
+ !localBranch &&
52
+ !remoteBranch &&
53
+ pullRequestBelongsToRepository) {
54
+ const pullRequestMatches = resolveExistingExactWorktreeMatches(currentSources, branch, true);
55
+ if (pullRequestMatches.length === 1) {
56
+ return { kind: "existing", source: pullRequestMatches[0] };
57
+ }
58
+ }
59
+ let skippedRegisteredRepos = 0;
60
+ const registeredSources = await listRegisteredWorktreeSources(cwd, () => {
61
+ skippedRegisteredRepos++;
62
+ });
63
+ const crossRepoSources = registeredSources.filter((source) => source.repoRoot !== repository?.repoRoot);
64
+ const crossMatches = await resolveExistingWorktreeMatches(crossRepoSources, branch, (!localBranch && !remoteBranch) || isPullRequestUrl(branch));
65
+ if (crossMatches.length === 1) {
66
+ return { kind: "existing", source: crossMatches[0] };
67
+ }
68
+ if (crossMatches.length > 1) {
69
+ return { kind: "ambiguous", matches: crossMatches };
70
+ }
71
+ if (!repository) {
72
+ return registeredSources.length === 0
73
+ ? {
74
+ kind: "no-repository",
75
+ staleRegisteredRepos: skippedRegisteredRepos > 0,
76
+ }
77
+ : { kind: "no-match", staleRegisteredRepos: skippedRegisteredRepos > 0 };
78
+ }
79
+ if (localBranch) {
80
+ if (configError)
81
+ return configError;
82
+ flushConfigWarnings();
83
+ return { kind: "create", repository, branch, mode: "checkout" };
84
+ }
85
+ if (remoteBranch) {
86
+ flushConfigWarnings();
87
+ return { kind: "create", repository, branch, mode: "track", remote };
88
+ }
89
+ const localMatch = !pullRequestNumber || !isPullRequestUrl(branch)
90
+ ? resolveWorktreeQuery(currentSources, branch)
91
+ : null;
92
+ if (localMatch) {
93
+ return { kind: "existing", source: localMatch };
94
+ }
95
+ if (configError)
96
+ return configError;
97
+ if (pullRequestNumber !== null && !pullRequestBelongsToRepository) {
98
+ flushConfigWarnings();
99
+ return {
100
+ kind: "error",
101
+ message: "PR URL does not belong to this repository; run gji go from the matching checkout",
102
+ };
103
+ }
104
+ if (pullRequestNumber !== null) {
105
+ flushConfigWarnings();
106
+ return { kind: "pull-request", repository, input: branch };
107
+ }
108
+ flushConfigWarnings();
109
+ return { kind: "no-match", staleRegisteredRepos: skippedRegisteredRepos > 0 };
110
+ }
111
+ function resolveExistingExactWorktreeMatches(sources, query, allowPullRequestFallback) {
112
+ const exactMatches = resolveExactWorktreeQueryMatches(sources, query);
113
+ if (exactMatches.length > 0 || !allowPullRequestFallback)
114
+ return exactMatches;
115
+ const pullRequestNumber = parsePrInput(query);
116
+ return pullRequestNumber === null
117
+ ? []
118
+ : resolveExactWorktreeQueryMatches(sources, `pr/${pullRequestNumber}`);
119
+ }
120
+ async function resolveExistingWorktreeMatches(sources, query, allowPullRequestFallback) {
121
+ const exactMatches = resolveExactWorktreeQueryMatches(sources, query);
122
+ if (exactMatches.length > 0)
123
+ return exactMatches;
124
+ const pullRequestNumber = parsePrInput(query);
125
+ if (pullRequestNumber === null) {
126
+ return resolveWorktreeQueryMatches(sources, query);
127
+ }
128
+ if (!allowPullRequestFallback)
129
+ return [];
130
+ const ownershipByRepository = new Map();
131
+ const eligibleSources = await Promise.all(sources.map(async (source) => {
132
+ const repoRoot = source.repoRoot ?? source.worktree.path;
133
+ let ownership = ownershipByRepository.get(repoRoot);
134
+ if (ownership === undefined) {
135
+ ownership = isPullRequestForRepository(repoRoot, query);
136
+ ownershipByRepository.set(repoRoot, ownership);
137
+ }
138
+ return (await ownership) ? source : null;
139
+ }));
140
+ const pullRequestMatches = resolveExactWorktreeQueryMatches(eligibleSources.filter((source) => source !== null), `pr/${pullRequestNumber}`);
141
+ if (pullRequestMatches.length > 0 || isPullRequestUrl(query)) {
142
+ return pullRequestMatches;
143
+ }
144
+ return resolveWorktreeQueryMatches(sources, query);
145
+ }
146
+ function isPullRequestUrl(input) {
147
+ return /^[a-z][a-z\d+.-]*:\/\//i.test(input);
148
+ }
149
+ async function isPullRequestForRepository(repoRoot, input) {
150
+ if (/^\d+$/.test(input) || /^#\d+$/.test(input))
151
+ return true;
152
+ let pullRequestUrl;
153
+ try {
154
+ pullRequestUrl = new URL(input);
155
+ }
156
+ catch {
157
+ return true;
158
+ }
159
+ const remoteUrl = await getRepositoryRemoteUrl(repoRoot, "origin");
160
+ if (!remoteUrl)
161
+ return true;
162
+ return (normalizeRepositoryUrl(remoteUrl) ===
163
+ normalizeRepositoryUrl(pullRequestUrl.href));
164
+ }
165
+ function normalizeRepositoryUrl(value) {
166
+ return normalizeRemoteUrl(value).replace(/\/(?:pull|pull-requests|(?:-\/)?merge_requests)\/\d+(?:\/.*)?$/, "");
167
+ }
168
+ function normalizeRemoteUrl(value) {
169
+ const trimmed = value.trim();
170
+ const sshMatch = trimmed.match(/^(?:[^@]+@)?([^:]+):(.+)$/);
171
+ if (sshMatch && !trimmed.includes("://")) {
172
+ return `${sshMatch[1]}/${sshMatch[2]}`
173
+ .replace(/\.git$/, "")
174
+ .replace(/\/$/, "");
175
+ }
176
+ try {
177
+ const url = new URL(trimmed);
178
+ return `${url.host}${url.pathname}`
179
+ .replace(/\.git$/, "")
180
+ .replace(/\/$/, "");
181
+ }
182
+ catch {
183
+ return trimmed.replace(/\.git$/, "").replace(/\/$/, "");
184
+ }
185
+ }
package/dist/go.d.ts CHANGED
@@ -1,13 +1,17 @@
1
- import { type QueryWorktreePullRequests, type WorktreePromptEntry } from "./worktree-picker.js";
1
+ import { type QueryWorktreePullRequests, type WorktreePromptEntry, type WorktreePromptScope } from "./worktree-picker.js";
2
2
  export interface GoCommandOptions {
3
3
  branch?: string;
4
4
  cwd: string;
5
+ json?: boolean;
5
6
  print?: boolean;
7
+ root?: boolean;
8
+ quiet?: boolean;
6
9
  stderr: (chunk: string) => void;
7
10
  stdout: (chunk: string) => void;
8
11
  }
9
12
  export interface GoCommandDependencies {
10
- promptForWorktree: (worktrees: WorktreePromptEntry[]) => Promise<string | null>;
13
+ confirmBranchCreation: (branch: string) => Promise<boolean>;
14
+ promptForWorktree: (worktrees: WorktreePromptEntry[], scope?: WorktreePromptScope) => Promise<string | null>;
11
15
  queryPullRequests: QueryWorktreePullRequests;
12
16
  }
13
17
  export declare function createGoCommand(dependencies?: Partial<GoCommandDependencies>): (options: GoCommandOptions) => Promise<number>;
package/dist/go.js CHANGED
@@ -1,85 +1,293 @@
1
1
  import { basename } from "node:path";
2
+ import { confirm, isCancel } from "@clack/prompts";
3
+ import { runBackCommand } from "./back.js";
2
4
  import { loadEffectiveConfig } from "./config.js";
5
+ import { renderContextCard } from "./context-card.js";
6
+ import { resolveGoBranch } from "./go-resolver.js";
3
7
  import { isHeadless } from "./headless.js";
4
8
  import { recordWorktreeUsage } from "./history.js";
5
9
  import { extractHooks, runHook } from "./hooks.js";
6
- import { detectRepository, listWorktrees } from "./repo.js";
10
+ import { createNavigationRepository, createNavigationTarget, } from "./navigation-output.js";
11
+ import { runNewCommand } from "./new.js";
12
+ import { runPrCommand } from "./pr.js";
13
+ import { detectRepository, listWorktrees, } from "./repo.js";
7
14
  import { writeShellOutput } from "./shell-handoff.js";
8
- import { resolveWarpTarget } from "./warp.js";
9
- import { buildWorktreePromptEntries, promptForSingleWorktree, resolveWorktreeQuery, } from "./worktree-picker.js";
15
+ import { getWorktreeSlot } from "./slots.js";
16
+ import { buildWorktreePromptEntries, promptForSingleWorktree, } from "./worktree-picker.js";
17
+ import { listRegisteredWorktreeSources } from "./worktree-sources.js";
10
18
  const GO_OUTPUT_FILE_ENV = "GJI_GO_OUTPUT_FILE";
11
19
  export function createGoCommand(dependencies = {}) {
20
+ const confirmBranchCreation = dependencies.confirmBranchCreation ?? defaultConfirmBranchCreation;
12
21
  const prompt = dependencies.promptForWorktree ?? promptForWorktree;
13
22
  return async function runGoCommand(options) {
14
- let worktrees;
15
- let repository;
16
- try {
17
- [worktrees, repository] = await Promise.all([
18
- listWorktrees(options.cwd),
19
- detectRepository(options.cwd),
20
- ]);
21
- }
22
- catch {
23
- // Not inside a git repo — fall back to cross-repo navigation.
24
- if (isHeadless() && !options.branch) {
25
- options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
26
- return 1;
23
+ if (options.root) {
24
+ if (options.branch !== undefined) {
25
+ return emitError(options, "--root cannot be combined with a branch");
27
26
  }
28
- const target = await resolveWarpTarget({
29
- ...options,
27
+ return navigateToRepositoryRoot(options);
28
+ }
29
+ if (options.branch === "-") {
30
+ return runBackCommand({
30
31
  commandName: "gji go",
32
+ cwd: options.cwd,
33
+ json: options.json,
34
+ outputEnv: GO_OUTPUT_FILE_ENV,
35
+ stderr: options.stderr,
36
+ stdout: options.stdout,
31
37
  });
32
- if (!target)
33
- return 1;
34
- await recordWorktreeUsage(target.path, target.branch);
35
- await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
36
- return 0;
37
- }
38
- if (!options.branch && isHeadless()) {
39
- options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
40
- return 1;
41
38
  }
42
- const promptSources = worktrees.map((worktree) => ({
43
- repoRoot: repository.repoRoot,
44
- repoName: repository.repoName,
45
- worktree,
46
- }));
47
- const promptEntries = options.branch
48
- ? []
49
- : await buildWorktreePromptEntries(promptSources, {
39
+ const [repository, currentWorktrees] = await readCurrentRepository(options.cwd);
40
+ const currentSources = repository
41
+ ? toPromptSources(repository.repoRoot, repository.repoName, currentWorktrees)
42
+ : [];
43
+ if (!options.branch) {
44
+ if (options.json || isHeadless()) {
45
+ return emitError(options, "branch argument is required in non-interactive mode (GJI_NO_TUI=1)");
46
+ }
47
+ let skippedRegisteredRepos = 0;
48
+ let registeredSources = null;
49
+ let promptSources = currentSources;
50
+ const loadAllSources = async () => {
51
+ if (registeredSources === null) {
52
+ registeredSources = await listRegisteredWorktreeSources(options.cwd, () => {
53
+ skippedRegisteredRepos++;
54
+ });
55
+ }
56
+ return deduplicateSources([...currentSources, ...registeredSources]);
57
+ };
58
+ if (!repository) {
59
+ promptSources = await loadAllSources();
60
+ }
61
+ if (promptSources.length === 0 && !repository) {
62
+ return emitNoRepositoryError(options, skippedRegisteredRepos > 0);
63
+ }
64
+ let currentRepositoryScope = true;
65
+ let scope;
66
+ if (repository) {
67
+ scope = {
68
+ label: "current repository",
69
+ toggleLabel: "all repositories",
70
+ toggle: async () => {
71
+ const nextCurrentRepositoryScope = !currentRepositoryScope;
72
+ const nextSources = nextCurrentRepositoryScope
73
+ ? currentSources
74
+ : await loadAllSources();
75
+ currentRepositoryScope = nextCurrentRepositoryScope;
76
+ promptSources = nextSources;
77
+ return {
78
+ entries: await buildWorktreePromptEntries(promptSources, {
79
+ metadata: currentRepositoryScope ? "full" : "fast",
80
+ queryPullRequests: dependencies.queryPullRequests,
81
+ }),
82
+ label: currentRepositoryScope
83
+ ? "current repository"
84
+ : "all repositories",
85
+ toggleLabel: currentRepositoryScope
86
+ ? "all repositories"
87
+ : "current repository",
88
+ };
89
+ },
90
+ };
91
+ }
92
+ const promptEntries = await buildWorktreePromptEntries(promptSources, {
93
+ metadata: repository ? "full" : "fast",
50
94
  queryPullRequests: dependencies.queryPullRequests,
51
95
  });
52
- const queried = options.branch
53
- ? resolveWorktreeQuery(promptSources, options.branch)
54
- : null;
55
- const prompted = options.branch ? null : await prompt(promptEntries);
56
- const resolvedPath = options.branch
57
- ? queried?.worktree.path
58
- : (prompted ?? undefined);
59
- if (!resolvedPath) {
60
- if (options.branch) {
61
- options.stderr(`No worktree found for branch: ${options.branch}\n`);
62
- options.stderr(`Hint: Use 'gji ls' to see available worktrees\n`);
63
- }
64
- else {
96
+ const selectedPath = await prompt(promptEntries, scope);
97
+ if (!selectedPath) {
65
98
  options.stderr("Aborted\n");
99
+ return 1;
66
100
  }
67
- return 1;
101
+ const selected = promptSources.find((source) => source.worktree.path === selectedPath);
102
+ return navigateToExistingWorktree(options, selectedPath, selected?.worktree);
68
103
  }
69
- const chosenWorktree = worktrees.find((w) => w.path === resolvedPath);
70
- const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
71
- const hooks = extractHooks(config);
72
- await runHook(hooks["after-enter"], resolvedPath, {
73
- branch: chosenWorktree?.branch ?? undefined,
74
- path: resolvedPath,
75
- repo: basename(repository.repoRoot),
76
- }, options.stderr);
77
- await recordWorktreeUsage(resolvedPath, chosenWorktree?.branch ?? null);
78
- await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
79
- return 0;
104
+ const resolution = await resolveGoBranch({
105
+ branch: options.branch,
106
+ configStderr: options.json ? undefined : options.stderr,
107
+ cwd: options.cwd,
108
+ currentSources,
109
+ repository,
110
+ });
111
+ return handleGoBranchResolution(options, resolution, confirmBranchCreation, prompt, dependencies.queryPullRequests);
80
112
  };
81
113
  }
82
114
  export const runGoCommand = createGoCommand();
83
- async function promptForWorktree(worktrees) {
84
- return promptForSingleWorktree("Choose a worktree", worktrees);
115
+ async function handleGoBranchResolution(options, resolution, confirmBranchCreation, prompt, queryPullRequests) {
116
+ switch (resolution.kind) {
117
+ case "existing":
118
+ return navigateToExistingWorktree(options, resolution.source.worktree.path, resolution.source.worktree);
119
+ case "ambiguous": {
120
+ if (options.json || isHeadless() || options.print) {
121
+ return emitAmbiguousCrossRepoError(options, resolution.matches);
122
+ }
123
+ const candidates = await buildWorktreePromptEntries(resolution.matches, {
124
+ metadata: "fast",
125
+ queryPullRequests,
126
+ });
127
+ const selectedPath = await prompt(candidates);
128
+ if (!selectedPath) {
129
+ options.stderr("Aborted\n");
130
+ return 1;
131
+ }
132
+ const selected = resolution.matches.find((source) => source.worktree.path === selectedPath);
133
+ return navigateToExistingWorktree(options, selectedPath, selected?.worktree);
134
+ }
135
+ case "create":
136
+ return createExistingBranchWorktree(options, confirmBranchCreation, resolution.repository, resolution.branch, resolution.mode, resolution.remote);
137
+ case "pull-request":
138
+ if (options.json || isHeadless() || options.print) {
139
+ return emitError(options, "PR navigation creates a worktree; use `gji pr <ref>` in an interactive shell");
140
+ }
141
+ return runPrCommand({
142
+ cwd: resolution.repository.repoRoot,
143
+ number: resolution.input,
144
+ outputEnv: GO_OUTPUT_FILE_ENV,
145
+ stderr: options.stderr,
146
+ stdout: options.stdout,
147
+ });
148
+ case "no-repository":
149
+ return emitNoRepositoryError(options, resolution.staleRegisteredRepos);
150
+ case "no-match":
151
+ return emitNoMatchError(options, resolution.staleRegisteredRepos);
152
+ case "error":
153
+ return emitError(options, resolution.message);
154
+ }
155
+ }
156
+ async function readCurrentRepository(cwd) {
157
+ try {
158
+ const [repository, worktrees] = await Promise.all([
159
+ detectRepository(cwd),
160
+ listWorktrees(cwd),
161
+ ]);
162
+ return [repository, worktrees];
163
+ }
164
+ catch {
165
+ return [null, []];
166
+ }
167
+ }
168
+ function toPromptSources(repoRoot, repoName, worktrees) {
169
+ return worktrees.map((worktree) => ({ repoName, repoRoot, worktree }));
170
+ }
171
+ function deduplicateSources(sources) {
172
+ const seen = new Set();
173
+ const deduplicated = [];
174
+ for (const source of sources) {
175
+ if (seen.has(source.worktree.path))
176
+ continue;
177
+ seen.add(source.worktree.path);
178
+ deduplicated.push(source);
179
+ }
180
+ return deduplicated;
181
+ }
182
+ async function createExistingBranchWorktree(options, confirmBranchCreation, repository, branch, mode, remote) {
183
+ if (options.json || isHeadless() || options.print) {
184
+ return emitError(options, `branch "${branch}" exists but has no worktree; use interactive gji go ${branch} to create one`);
185
+ }
186
+ if (!(await confirmBranchCreation(branch))) {
187
+ options.stderr("Aborted\n");
188
+ return 1;
189
+ }
190
+ return runNewCommand({
191
+ branch,
192
+ cwd: repository.repoRoot,
193
+ mode,
194
+ outputEnv: GO_OUTPUT_FILE_ENV,
195
+ remote,
196
+ stderr: options.stderr,
197
+ stdout: options.stdout,
198
+ });
199
+ }
200
+ async function navigateToExistingWorktree(options, path, worktree) {
201
+ const repository = await detectRepository(path);
202
+ if (options.json) {
203
+ options.stdout(`${JSON.stringify(createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), path, worktree?.branch ?? null), null, 2)}\n`);
204
+ return 0;
205
+ }
206
+ const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
207
+ const hooks = extractHooks(config);
208
+ await runHook(hooks["after-enter"], path, {
209
+ branch: worktree?.branch ?? undefined,
210
+ path,
211
+ repo: basename(repository.repoRoot),
212
+ slot: await getWorktreeSlot(path),
213
+ }, options.stderr);
214
+ if (!options.quiet) {
215
+ const card = await renderContextCard(path);
216
+ if (card)
217
+ options.stderr(`${card}\n`);
218
+ }
219
+ await recordWorktreeUsage(path, worktree?.branch ?? null);
220
+ await writeShellOutput(GO_OUTPUT_FILE_ENV, path, options.stdout);
221
+ return 0;
222
+ }
223
+ async function navigateToRepositoryRoot(options) {
224
+ let repository;
225
+ try {
226
+ repository = await detectRepository(options.cwd);
227
+ }
228
+ catch {
229
+ return emitError(options, "not inside a git repository");
230
+ }
231
+ if (options.json) {
232
+ const rootWorktree = (await listWorktrees(repository.repoRoot)).find((worktree) => worktree.path === repository.repoRoot);
233
+ options.stdout(`${JSON.stringify(createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), repository.repoRoot, rootWorktree?.branch ?? null), null, 2)}\n`);
234
+ return 0;
235
+ }
236
+ if (options.print) {
237
+ options.stdout(`${repository.repoRoot}\n`);
238
+ return 0;
239
+ }
240
+ await writeShellOutput(GO_OUTPUT_FILE_ENV, repository.repoRoot, options.stdout);
241
+ return 0;
242
+ }
243
+ function emitAmbiguousCrossRepoError(options, matches) {
244
+ const candidates = matches
245
+ .map((match) => `${match.repoName}/${match.worktree.branch ?? "(detached)"}`)
246
+ .join(", ");
247
+ return emitError(options, `multiple worktrees match "${options.branch}": ${candidates}`);
248
+ }
249
+ function emitNoRepositoryError(options, staleRegisteredRepos) {
250
+ if (options.json) {
251
+ return emitError(options, "not in a git repository and no accessible worktrees are registered");
252
+ }
253
+ options.stderr("gji go: not in a git repository and no repos registered yet.\n");
254
+ options.stderr("Use any gji command inside a repository to register it.\n");
255
+ if (staleRegisteredRepos) {
256
+ options.stderr("Hint: Run 'gji doctor' to inspect stale repository entries.\n");
257
+ }
258
+ return 1;
259
+ }
260
+ function emitNoMatchError(options, staleRegisteredRepos = false) {
261
+ if (options.json) {
262
+ return emitError(options, `nothing matched "${options.branch}"`);
263
+ }
264
+ options.stderr(`No worktree found for branch: ${options.branch}\n`);
265
+ options.stderr(`gji go: nothing matched "${options.branch}"\n`);
266
+ options.stderr("Hint: Use 'gji ls' to see available worktrees\n");
267
+ options.stderr(" · no worktree or branch named that query in this repository\n" +
268
+ " · no matching worktree in registered repositories\n" +
269
+ ` · create it: gji new ${options.branch}\n`);
270
+ if (staleRegisteredRepos) {
271
+ options.stderr("Hint: Run 'gji doctor' to inspect stale repository entries.\n");
272
+ }
273
+ return 1;
274
+ }
275
+ function emitError(options, message) {
276
+ if (options.json) {
277
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
278
+ }
279
+ else {
280
+ options.stderr(`gji go: ${message}\n`);
281
+ }
282
+ return 1;
283
+ }
284
+ async function defaultConfirmBranchCreation(branch) {
285
+ const choice = await confirm({
286
+ message: `branch "${branch}" exists but has no worktree. Create one?`,
287
+ initialValue: true,
288
+ });
289
+ return !isCancel(choice) && choice;
290
+ }
291
+ async function promptForWorktree(worktrees, scope) {
292
+ return promptForSingleWorktree("Choose a worktree", worktrees, { scope });
85
293
  }
package/dist/hooks.d.ts CHANGED
@@ -8,7 +8,8 @@ export interface HookContext {
8
8
  branch?: string;
9
9
  path: string;
10
10
  repo: string;
11
+ slot?: number | null;
11
12
  }
12
- export declare function runHook(hookCmd: GjiHookCommand | undefined, cwd: string, context: HookContext, stderr: (chunk: string) => void): Promise<void>;
13
+ export declare function runHook(hookCmd: GjiHookCommand | undefined, cwd: string, context: HookContext, stderr: (chunk: string) => void, stdout?: (chunk: string) => void): Promise<void>;
13
14
  export declare function interpolate(template: string, context: HookContext): string;
14
15
  export declare function extractHooks(config: Record<string, unknown>): GjiHooks;