@solaqua/gji 0.8.0 → 0.9.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 (53) hide show
  1. package/README.md +24 -30
  2. package/dist/browser.d.ts +7 -0
  3. package/dist/browser.js +37 -0
  4. package/dist/clean.js +34 -19
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +59 -4
  7. package/dist/doctor.d.ts +28 -0
  8. package/dist/doctor.js +454 -0
  9. package/dist/gji-bundle.mjs +2514 -378
  10. package/dist/go.d.ts +2 -1
  11. package/dist/go.js +4 -1
  12. package/dist/index.js +12 -6
  13. package/dist/init.d.ts +15 -0
  14. package/dist/init.js +232 -16
  15. package/dist/open.d.ts +2 -1
  16. package/dist/open.js +2 -1
  17. package/dist/pr-open.d.ts +19 -0
  18. package/dist/pr-open.js +290 -0
  19. package/dist/pull-requests.d.ts +33 -0
  20. package/dist/pull-requests.js +384 -0
  21. package/dist/remove.js +2 -2
  22. package/dist/repo-registry.d.ts +5 -0
  23. package/dist/repo-registry.js +87 -14
  24. package/dist/shell-completion.js +278 -10
  25. package/dist/shell-setup.d.ts +9 -0
  26. package/dist/shell-setup.js +56 -0
  27. package/dist/warp.d.ts +3 -0
  28. package/dist/warp.js +8 -3
  29. package/dist/worktree-management.d.ts +2 -1
  30. package/dist/worktree-management.js +23 -7
  31. package/dist/worktree-picker.d.ts +12 -1
  32. package/dist/worktree-picker.js +84 -6
  33. package/man/man1/gji-back.1 +1 -1
  34. package/man/man1/gji-clean.1 +1 -1
  35. package/man/man1/gji-completion.1 +1 -1
  36. package/man/man1/gji-config.1 +1 -1
  37. package/man/man1/gji-doctor.1 +19 -0
  38. package/man/man1/gji-go.1 +1 -1
  39. package/man/man1/gji-history.1 +1 -1
  40. package/man/man1/gji-init.1 +6 -3
  41. package/man/man1/gji-ls.1 +1 -1
  42. package/man/man1/gji-new.1 +1 -1
  43. package/man/man1/gji-open.1 +1 -1
  44. package/man/man1/gji-pr.1 +5 -1
  45. package/man/man1/gji-remove.1 +1 -1
  46. package/man/man1/gji-root.1 +1 -1
  47. package/man/man1/gji-run-hook.1 +1 -1
  48. package/man/man1/gji-status.1 +1 -1
  49. package/man/man1/gji-sync-files.1 +1 -1
  50. package/man/man1/gji-sync.1 +1 -1
  51. package/man/man1/gji-warp.1 +1 -1
  52. package/man/man1/gji.1 +6 -2
  53. package/package.json +1 -1
package/dist/go.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type WorktreePromptEntry } from "./worktree-picker.js";
1
+ import { type QueryWorktreePullRequests, type WorktreePromptEntry } from "./worktree-picker.js";
2
2
  export interface GoCommandOptions {
3
3
  branch?: string;
4
4
  cwd: string;
@@ -8,6 +8,7 @@ export interface GoCommandOptions {
8
8
  }
9
9
  export interface GoCommandDependencies {
10
10
  promptForWorktree: (worktrees: WorktreePromptEntry[]) => Promise<string | null>;
11
+ queryPullRequests: QueryWorktreePullRequests;
11
12
  }
12
13
  export declare function createGoCommand(dependencies?: Partial<GoCommandDependencies>): (options: GoCommandOptions) => Promise<number>;
13
14
  export declare const runGoCommand: (options: GoCommandOptions) => Promise<number>;
package/dist/go.js CHANGED
@@ -40,12 +40,15 @@ export function createGoCommand(dependencies = {}) {
40
40
  return 1;
41
41
  }
42
42
  const promptSources = worktrees.map((worktree) => ({
43
+ repoRoot: repository.repoRoot,
43
44
  repoName: repository.repoName,
44
45
  worktree,
45
46
  }));
46
47
  const promptEntries = options.branch
47
48
  ? []
48
- : await buildWorktreePromptEntries(promptSources);
49
+ : await buildWorktreePromptEntries(promptSources, {
50
+ queryPullRequests: dependencies.queryPullRequests,
51
+ });
49
52
  const queried = options.branch
50
53
  ? resolveWorktreeQuery(promptSources, options.branch)
51
54
  : null;
package/dist/index.js CHANGED
@@ -2,11 +2,12 @@
2
2
  import { homedir } from "node:os";
3
3
  import { runCli } from "./cli.js";
4
4
  import { loadGlobalConfig } from "./config.js";
5
+ import { isHeadless } from "./headless.js";
5
6
  async function main() {
6
7
  try {
7
8
  const argv = process.argv.slice(2);
8
9
  // Warn once (until fixed) when shell integration hasn't been set up.
9
- // Only shown in interactive terminals — suppressed in pipes and after gji init --write.
10
+ // Only shown in interactive terminals — suppressed in pipes and after gji init.
10
11
  const isMetaArg = argv[0] === "init" ||
11
12
  argv[0] === "--version" ||
12
13
  argv[0] === "-V" ||
@@ -31,15 +32,20 @@ async function warnIfMissingShellIntegration() {
31
32
  try {
32
33
  const { config } = await loadGlobalConfig(homedir());
33
34
  if (!config.shellIntegration) {
34
- const shellBin = (process.env.SHELL ?? "").split("/").at(-1);
35
- const shellArg = shellBin && ["bash", "zsh", "fish"].includes(shellBin)
36
- ? ` ${shellBin}`
37
- : "";
38
- process.stderr.write(`gji: shell integration not set up — run \`gji init${shellArg} --write\` to enable automatic cd.\n`);
35
+ const setupCommand = isHeadless()
36
+ ? legacyShellSetupCommand()
37
+ : "gji init";
38
+ process.stderr.write(`gji: shell integration not set up — run \`${setupCommand}\` to enable automatic cd.\n`);
39
39
  }
40
40
  }
41
41
  catch {
42
42
  // best-effort; never block the command
43
43
  }
44
44
  }
45
+ function legacyShellSetupCommand() {
46
+ const shell = (process.env.SHELL ?? "").split("/").at(-1);
47
+ return shell && ["bash", "fish", "zsh"].includes(shell)
48
+ ? `gji init ${shell} --write`
49
+ : "gji init <shell> --write";
50
+ }
45
51
  void main();
package/dist/init.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type SupportedShell } from "./shell.js";
2
2
  export type InstallSaveTarget = "local" | "global";
3
+ export type ShellIntegrationChoice = "existing" | "install" | "skip";
3
4
  export interface SetupWizardResult {
4
5
  branchPrefix?: string;
5
6
  hooks?: {
@@ -10,9 +11,22 @@ export interface SetupWizardResult {
10
11
  installSaveTarget: InstallSaveTarget;
11
12
  worktreePath?: string;
12
13
  }
14
+ export interface InitOnboardingResult {
15
+ editor?: string;
16
+ installCompletion: boolean;
17
+ shellIntegration: ShellIntegrationChoice;
18
+ shell: SupportedShell;
19
+ }
20
+ interface InitOnboardingContext {
21
+ detectedShell: SupportedShell | null;
22
+ home: string;
23
+ }
13
24
  export interface InitCommandOptions {
14
25
  cwd: string;
15
26
  home?: string;
27
+ interactive?: boolean;
28
+ json?: boolean;
29
+ promptForOnboarding?: (context: InitOnboardingContext) => Promise<InitOnboardingResult | null>;
16
30
  promptForSetup?: () => Promise<SetupWizardResult | null>;
17
31
  shell?: string;
18
32
  stderr?: (chunk: string) => void;
@@ -22,3 +36,4 @@ export interface InitCommandOptions {
22
36
  export declare function runInitCommand(options: InitCommandOptions): Promise<number>;
23
37
  export declare function renderShellIntegration(shell: SupportedShell): string;
24
38
  export declare function upsertShellIntegration(existingConfig: string, script: string): string;
39
+ export {};
package/dist/init.js CHANGED
@@ -1,11 +1,15 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { constants } from "node:fs";
2
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
3
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
- import { intro, isCancel, outro, select, text } from "@clack/prompts";
4
+ import { dirname } from "node:path";
5
+ import { confirm, intro, isCancel, log, outro, select, text, } from "@clack/prompts";
5
6
  import { loadConfig, loadGlobalConfig, saveGlobalConfig, saveLocalConfig, updateGlobalConfigKey, } from "./config.js";
7
+ import { EDITORS } from "./editor.js";
8
+ import { isHeadless } from "./headless.js";
6
9
  import { resolveSupportedShell } from "./shell.js";
7
- const START_MARKER = "# >>> gji init >>>";
8
- const END_MARKER = "# <<< gji init <<<";
10
+ import { renderShellCompletion } from "./shell-completion.js";
11
+ import { SHELL_INTEGRATION_END_MARKER as END_MARKER, executableExists, hasShellIntegration, ONBOARDING_SHELL_INTEGRATION_END_MARKER as ONBOARDING_END_MARKER, ONBOARDING_SHELL_INTEGRATION_START_MARKER as ONBOARDING_START_MARKER, resolveCompletionPath, resolveShellConfigPath, SHELL_INTEGRATION_START_MARKER as START_MARKER, } from "./shell-setup.js";
12
+ const ZSH_COMPLETION_PATH_LINE = "fpath=(~/.zsh/completions $fpath)";
9
13
  const SHELL_WRAPPED_COMMANDS = [
10
14
  {
11
15
  bypassOptions: ["--help"],
@@ -58,6 +62,228 @@ const SHELL_WRAPPED_COMMANDS = [
58
62
  },
59
63
  ];
60
64
  export async function runInitCommand(options) {
65
+ if (options.shell === undefined) {
66
+ return runOnboardingInitCommand(options);
67
+ }
68
+ return runLegacyInitCommand(options);
69
+ }
70
+ async function runOnboardingInitCommand(options) {
71
+ if (options.json || isHeadless() || !canRunOnboarding(options)) {
72
+ return writeNonInteractiveInitError(options);
73
+ }
74
+ const home = options.home ?? homedir();
75
+ const context = {
76
+ detectedShell: resolveSupportedShell(undefined, process.env.SHELL),
77
+ home,
78
+ };
79
+ if (options.promptForOnboarding === undefined) {
80
+ return runDefaultOnboarding(context);
81
+ }
82
+ const result = await options.promptForOnboarding(context);
83
+ if (!result) {
84
+ options.stderr?.("Aborted.\n");
85
+ return 1;
86
+ }
87
+ await applyOnboardingResult(result, home);
88
+ return 0;
89
+ }
90
+ function canRunOnboarding(options) {
91
+ return (options.interactive ??
92
+ (options.promptForOnboarding !== undefined ||
93
+ (process.stdin.isTTY === true && process.stdout.isTTY === true)));
94
+ }
95
+ function writeNonInteractiveInitError(options) {
96
+ const error = "run `gji init <shell> --write` in non-interactive mode";
97
+ options.stderr?.(options.json ? `${JSON.stringify({ error })}\n` : `${error}\n`);
98
+ return 1;
99
+ }
100
+ async function runDefaultOnboarding(context) {
101
+ intro("gji init");
102
+ const shell = await select({
103
+ message: "Which shell should gji configure?",
104
+ initialValue: context.detectedShell ?? "zsh",
105
+ options: [
106
+ { value: "zsh", label: "zsh" },
107
+ { value: "bash", label: "bash" },
108
+ { value: "fish", label: "fish" },
109
+ ],
110
+ });
111
+ if (isCancel(shell))
112
+ return abortDefaultOnboarding();
113
+ const rcPath = resolveShellConfigPath(shell, context.home);
114
+ const currentConfig = await readExistingConfig(rcPath);
115
+ const shellIntegration = await promptForShellIntegration(currentConfig, rcPath, shell);
116
+ if (shellIntegration === null)
117
+ return abortDefaultOnboarding();
118
+ const completionPath = resolveCompletionPath(shell, context.home);
119
+ const completionAlreadyInstalled = await fileExists(completionPath);
120
+ await applyShellIntegration(shell, shellIntegration, context.home);
121
+ const installCompletion = await promptForCompletion(shell, completionPath);
122
+ if (installCompletion === null)
123
+ return abortDefaultOnboarding();
124
+ if (installCompletion) {
125
+ await mkdir(dirname(completionPath), { recursive: true });
126
+ await writeFile(completionPath, renderShellCompletion(shell), "utf8");
127
+ }
128
+ if (shell === "zsh" && (completionAlreadyInstalled || installCompletion)) {
129
+ await ensureZshCompletionPath(rcPath);
130
+ }
131
+ const editor = await promptForEditor();
132
+ if (editor === null)
133
+ return abortDefaultOnboarding();
134
+ if (editor) {
135
+ await updateGlobalConfigKey("editor", editor, context.home);
136
+ }
137
+ outro(`Setup complete. Restart your shell or run: source ${rcPath}\nVerify with: gji doctor`);
138
+ return 0;
139
+ }
140
+ async function applyShellIntegration(shell, choice, home) {
141
+ if (choice === "install") {
142
+ await installOnboardingShellIntegration(shell, home);
143
+ }
144
+ if (choice !== "skip") {
145
+ await updateGlobalConfigKey("shellIntegration", true, home);
146
+ }
147
+ }
148
+ function abortDefaultOnboarding() {
149
+ outro("Aborted.");
150
+ return 1;
151
+ }
152
+ async function promptForShellIntegration(currentConfig, rcPath, shell) {
153
+ if (currentConfig.includes(ONBOARDING_START_MARKER) &&
154
+ currentConfig.includes(ONBOARDING_END_MARKER)) {
155
+ log.success(`Shell integration marker found in ${rcPath}; refreshing it.`);
156
+ return "install";
157
+ }
158
+ if (hasShellIntegration(currentConfig, shell)) {
159
+ log.success(`Shell integration already installed in ${rcPath} ✓`);
160
+ return "existing";
161
+ }
162
+ const confirmed = await confirm({
163
+ message: `Install shell integration in ${rcPath}?`,
164
+ initialValue: true,
165
+ });
166
+ if (isCancel(confirmed))
167
+ return null;
168
+ return confirmed ? "install" : "skip";
169
+ }
170
+ async function promptForCompletion(shell, completionPath) {
171
+ if (await fileExists(completionPath)) {
172
+ log.success(`${shell} completion already installed at ${completionPath} ✓`);
173
+ return false;
174
+ }
175
+ const confirmed = await confirm({
176
+ message: `Install ${shell} completion at ${completionPath}?`,
177
+ initialValue: true,
178
+ });
179
+ if (isCancel(confirmed))
180
+ return null;
181
+ return confirmed;
182
+ }
183
+ async function promptForEditor() {
184
+ const availableEditors = await findAvailableEditors();
185
+ if (availableEditors.length === 0) {
186
+ log.info("No supported editor CLIs found on PATH; skipping editor setup.");
187
+ return undefined;
188
+ }
189
+ const skipValue = "__gji_skip_editor__";
190
+ const selected = await select({
191
+ message: "Which editor should gji use?",
192
+ options: [
193
+ ...availableEditors.map(({ cli, name }) => ({
194
+ value: cli,
195
+ label: name,
196
+ hint: cli,
197
+ })),
198
+ { value: skipValue, label: "Skip" },
199
+ ],
200
+ });
201
+ if (isCancel(selected))
202
+ return null;
203
+ return selected === skipValue ? undefined : selected;
204
+ }
205
+ async function findAvailableEditors() {
206
+ const availableEditors = [];
207
+ for (const editor of EDITORS) {
208
+ if (await executableExists(editor.cli)) {
209
+ availableEditors.push(editor);
210
+ }
211
+ }
212
+ return availableEditors;
213
+ }
214
+ async function applyOnboardingResult(result, home) {
215
+ const rcPath = resolveShellConfigPath(result.shell, home);
216
+ const completionPath = resolveCompletionPath(result.shell, home);
217
+ const completionAlreadyInstalled = await fileExists(completionPath);
218
+ await applyShellIntegration(result.shell, result.shellIntegration, home);
219
+ if (result.installCompletion) {
220
+ await mkdir(dirname(completionPath), { recursive: true });
221
+ await writeFile(completionPath, renderShellCompletion(result.shell), "utf8");
222
+ }
223
+ if (result.shell === "zsh" &&
224
+ (completionAlreadyInstalled || result.installCompletion)) {
225
+ await ensureZshCompletionPath(rcPath);
226
+ }
227
+ if (result.editor) {
228
+ await updateGlobalConfigKey("editor", result.editor, home);
229
+ }
230
+ }
231
+ async function installOnboardingShellIntegration(shell, home) {
232
+ const rcPath = resolveShellConfigPath(shell, home);
233
+ await mkdir(dirname(rcPath), { recursive: true });
234
+ const current = await readExistingConfig(rcPath);
235
+ if (shell === "fish") {
236
+ await writeFile(rcPath, upsertShellIntegration(current, renderShellIntegration(shell)), "utf8");
237
+ return;
238
+ }
239
+ await writeFile(rcPath, upsertOnboardingShellIntegration(current, shell), "utf8");
240
+ }
241
+ function upsertOnboardingShellIntegration(existingConfig, shell) {
242
+ const script = renderOnboardingShellIntegration(shell).trimEnd();
243
+ const blockPattern = new RegExp(`${escapeForRegExp(ONBOARDING_START_MARKER)}[\\s\\S]*?${escapeForRegExp(ONBOARDING_END_MARKER)}\\n?`, "m");
244
+ if (blockPattern.test(existingConfig)) {
245
+ return ensureTrailingNewline(existingConfig.replace(blockPattern, `${script}\n`));
246
+ }
247
+ const prefix = existingConfig.trimEnd();
248
+ if (prefix.length === 0)
249
+ return ensureTrailingNewline(script);
250
+ return ensureTrailingNewline(`${prefix}\n\n${script}`);
251
+ }
252
+ function renderOnboardingShellIntegration(shell) {
253
+ return `${ONBOARDING_START_MARKER}
254
+ eval "$(gji init ${shell})"
255
+ ${ONBOARDING_END_MARKER}
256
+ `;
257
+ }
258
+ async function ensureZshCompletionPath(rcPath) {
259
+ const current = await readExistingConfig(rcPath);
260
+ const next = upsertZshCompletionPath(current);
261
+ if (next !== current) {
262
+ await writeFile(rcPath, next, "utf8");
263
+ }
264
+ }
265
+ function upsertZshCompletionPath(existingConfig) {
266
+ const configWithoutCompletionPath = existingConfig
267
+ .replace(/# >>> gji zsh completion path >>>[\s\S]*?# <<< gji zsh completion path <<<\n?/m, "")
268
+ .replace(new RegExp(`^[\\t ]*${escapeForRegExp(ZSH_COMPLETION_PATH_LINE)}[\\t ]*\\n?`, "gm"), "");
269
+ const suffix = configWithoutCompletionPath
270
+ .replace(/^(?:\r?\n)+/, "")
271
+ .trimEnd();
272
+ // Frameworks can invoke compinit from a sourced file, so this must come first.
273
+ return ensureTrailingNewline(suffix.length === 0
274
+ ? ZSH_COMPLETION_PATH_LINE
275
+ : `${ZSH_COMPLETION_PATH_LINE}\n\n${suffix}`);
276
+ }
277
+ async function fileExists(path) {
278
+ try {
279
+ await access(path, constants.F_OK);
280
+ return true;
281
+ }
282
+ catch {
283
+ return false;
284
+ }
285
+ }
286
+ async function runLegacyInitCommand(options) {
61
287
  const shell = resolveSupportedShell(options.shell, process.env.SHELL);
62
288
  const home = options.home ?? homedir();
63
289
  if (!shell) {
@@ -79,7 +305,7 @@ export async function runInitCommand(options) {
79
305
  const { config: globalConfig } = await loadGlobalConfig(home);
80
306
  const alreadyConfigured = "shellIntegration" in globalConfig || "installSaveTarget" in globalConfig;
81
307
  const hasCustomPrompt = options.promptForSetup !== undefined;
82
- const canPrompt = hasCustomPrompt || process.stdout.isTTY === true;
308
+ const canPrompt = !isHeadless() && (hasCustomPrompt || process.stdout.isTTY === true);
83
309
  if (!alreadyConfigured && canPrompt) {
84
310
  const prompt = options.promptForSetup ?? defaultPromptForSetup;
85
311
  const result = await prompt();
@@ -154,16 +380,6 @@ async function saveWizardConfig(result, cwd, home) {
154
380
  await saveGlobalConfig({ ...existing, ...values }, home);
155
381
  }
156
382
  }
157
- function resolveShellConfigPath(shell, home) {
158
- switch (shell) {
159
- case "bash":
160
- return join(home, ".bashrc");
161
- case "fish":
162
- return join(home, ".config", "fish", "config.fish");
163
- case "zsh":
164
- return join(home, ".zshrc");
165
- }
166
- }
167
383
  async function readExistingConfig(path) {
168
384
  try {
169
385
  return await readFile(path, "utf8");
package/dist/open.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type EditorDefinition } from "./editor.js";
2
- import { type WorktreePromptEntry } from "./worktree-picker.js";
2
+ import { type QueryWorktreePullRequests, type WorktreePromptEntry } from "./worktree-picker.js";
3
3
  export type { EditorDefinition };
4
4
  export interface OpenCommandOptions {
5
5
  branch?: string;
@@ -14,6 +14,7 @@ export interface OpenCommandDependencies {
14
14
  detectEditors: () => Promise<EditorDefinition[]>;
15
15
  promptForEditor: (editors: EditorDefinition[]) => Promise<string | null>;
16
16
  promptForWorktree: (worktrees: WorktreePromptEntry[]) => Promise<string | null>;
17
+ queryPullRequests: QueryWorktreePullRequests;
17
18
  spawnEditor: (cli: string, args: string[]) => Promise<void>;
18
19
  }
19
20
  export declare function createOpenCommand(dependencies?: Partial<OpenCommandDependencies>): (options: OpenCommandOptions) => Promise<number>;
package/dist/open.js CHANGED
@@ -42,9 +42,10 @@ export function createOpenCommand(dependencies = {}) {
42
42
  }
43
43
  else {
44
44
  const entries = await buildWorktreePromptEntries(worktrees.map((worktree) => ({
45
+ repoRoot: repository.repoRoot,
45
46
  repoName: repository.repoName,
46
47
  worktree,
47
- })));
48
+ })), { queryPullRequests: dependencies.queryPullRequests });
48
49
  const chosen = await promptForWorktree(entries);
49
50
  if (!chosen) {
50
51
  options.stderr("Aborted\n");
@@ -0,0 +1,19 @@
1
+ import { type PullRequestInfo } from "./pull-requests.js";
2
+ import { type QueryRepositoryPullRequests, type QueryWorktreePullRequests, type WorktreePromptEntry } from "./worktree-picker.js";
3
+ export interface PrOpenCommandOptions {
4
+ cwd: string;
5
+ stderr: (chunk: string) => void;
6
+ stdout: (chunk: string) => void;
7
+ select?: boolean;
8
+ target?: string;
9
+ }
10
+ export interface PrOpenCommandDependencies {
11
+ findOpenPullRequest: (repoRoot: string, number: number) => Promise<PullRequestInfo | null>;
12
+ openBrowser: (url: string) => Promise<void>;
13
+ promptForPullRequest: (pullRequests: PullRequestInfo[]) => Promise<PullRequestInfo | null>;
14
+ promptForWorktree: (worktrees: WorktreePromptEntry[]) => Promise<string | null>;
15
+ queryPullRequests: QueryWorktreePullRequests;
16
+ queryRepositoryPullRequests?: QueryRepositoryPullRequests;
17
+ }
18
+ export declare function createPrOpenCommand(dependencies?: Partial<PrOpenCommandDependencies>): (options: PrOpenCommandOptions) => Promise<number>;
19
+ export declare const runPrOpenCommand: (options: PrOpenCommandOptions) => Promise<number>;