@solaqua/gji 0.6.1 → 0.7.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 (80) hide show
  1. package/README.md +26 -1
  2. package/dist/back.d.ts +1 -1
  3. package/dist/back.js +23 -17
  4. package/dist/clean.d.ts +1 -1
  5. package/dist/clean.js +44 -35
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.js +264 -164
  8. package/dist/completion.js +3 -3
  9. package/dist/config-command.js +5 -5
  10. package/dist/config.js +41 -35
  11. package/dist/conflict.d.ts +1 -1
  12. package/dist/conflict.js +14 -6
  13. package/dist/editor.js +29 -9
  14. package/dist/file-sync.d.ts +1 -0
  15. package/dist/file-sync.js +15 -11
  16. package/dist/git.d.ts +1 -1
  17. package/dist/git.js +21 -19
  18. package/dist/gji-bundle.mjs +1709 -850
  19. package/dist/go.d.ts +2 -2
  20. package/dist/go.js +39 -26
  21. package/dist/headless.js +1 -1
  22. package/dist/history-command.js +3 -3
  23. package/dist/history.js +12 -12
  24. package/dist/hooks.js +16 -16
  25. package/dist/index.js +13 -9
  26. package/dist/init.d.ts +2 -2
  27. package/dist/init.js +106 -94
  28. package/dist/install-prompt.d.ts +3 -3
  29. package/dist/install-prompt.js +46 -28
  30. package/dist/ls.d.ts +2 -2
  31. package/dist/ls.js +29 -29
  32. package/dist/new.d.ts +2 -2
  33. package/dist/new.js +96 -81
  34. package/dist/open.d.ts +2 -2
  35. package/dist/open.js +24 -21
  36. package/dist/package-manager.js +96 -45
  37. package/dist/pr.d.ts +2 -2
  38. package/dist/pr.js +47 -34
  39. package/dist/remove.d.ts +1 -1
  40. package/dist/remove.js +39 -27
  41. package/dist/repo-registry.js +45 -19
  42. package/dist/repo.js +29 -28
  43. package/dist/root.js +3 -3
  44. package/dist/shell-completion.d.ts +1 -1
  45. package/dist/shell-completion.js +65 -37
  46. package/dist/shell-handoff.js +2 -2
  47. package/dist/shell.d.ts +1 -1
  48. package/dist/shell.js +4 -4
  49. package/dist/status.d.ts +5 -5
  50. package/dist/status.js +23 -23
  51. package/dist/sync-files-command.d.ts +10 -0
  52. package/dist/sync-files-command.js +137 -0
  53. package/dist/sync.js +23 -15
  54. package/dist/trigger-hook.js +9 -5
  55. package/dist/warp.js +66 -34
  56. package/dist/worktree-info.d.ts +9 -9
  57. package/dist/worktree-info.js +31 -29
  58. package/dist/worktree-management.d.ts +1 -1
  59. package/dist/worktree-management.js +26 -11
  60. package/dist/worktree-prompts.js +5 -5
  61. package/man/man1/gji-back.1 +1 -1
  62. package/man/man1/gji-clean.1 +1 -1
  63. package/man/man1/gji-completion.1 +1 -1
  64. package/man/man1/gji-config.1 +1 -1
  65. package/man/man1/gji-go.1 +1 -1
  66. package/man/man1/gji-history.1 +1 -1
  67. package/man/man1/gji-init.1 +1 -1
  68. package/man/man1/gji-ls.1 +1 -1
  69. package/man/man1/gji-new.1 +1 -1
  70. package/man/man1/gji-open.1 +1 -1
  71. package/man/man1/gji-pr.1 +1 -1
  72. package/man/man1/gji-remove.1 +1 -1
  73. package/man/man1/gji-root.1 +1 -1
  74. package/man/man1/gji-status.1 +1 -1
  75. package/man/man1/gji-sync-files.1 +23 -0
  76. package/man/man1/gji-sync.1 +1 -1
  77. package/man/man1/gji-trigger-hook.1 +1 -1
  78. package/man/man1/gji-warp.1 +1 -1
  79. package/man/man1/gji.1 +5 -1
  80. package/package.json +8 -2
@@ -1,9 +1,9 @@
1
- import { renderShellCompletion } from './shell-completion.js';
2
- import { resolveSupportedShell } from './shell.js';
1
+ import { resolveSupportedShell } from "./shell.js";
2
+ import { renderShellCompletion } from "./shell-completion.js";
3
3
  export async function runCompletionCommand(options) {
4
4
  const shell = resolveSupportedShell(options.shell, process.env.SHELL);
5
5
  if (!shell) {
6
- options.stderr?.('Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n');
6
+ options.stderr?.("Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n");
7
7
  return 1;
8
8
  }
9
9
  options.stdout(renderShellCompletion(shell));
@@ -1,4 +1,4 @@
1
- import { loadGlobalConfig, parseConfigValue, unsetGlobalConfigKey, updateGlobalConfigKey, } from './config.js';
1
+ import { loadGlobalConfig, parseConfigValue, unsetGlobalConfigKey, updateGlobalConfigKey, } from "./config.js";
2
2
  export async function runConfigCommand(options) {
3
3
  switch (options.action) {
4
4
  case undefined: {
@@ -6,25 +6,25 @@ export async function runConfigCommand(options) {
6
6
  writeJson(options.stdout, loaded.config);
7
7
  return 0;
8
8
  }
9
- case 'get': {
9
+ case "get": {
10
10
  const loaded = await loadGlobalConfig();
11
11
  writeJson(options.stdout, options.key ? loaded.config[options.key] : loaded.config);
12
12
  return 0;
13
13
  }
14
- case 'set':
14
+ case "set":
15
15
  if (options.key && options.value !== undefined) {
16
16
  await updateGlobalConfigKey(options.key, parseConfigValue(options.value));
17
17
  return 0;
18
18
  }
19
19
  break;
20
- case 'unset':
20
+ case "unset":
21
21
  if (options.key) {
22
22
  await unsetGlobalConfigKey(options.key);
23
23
  return 0;
24
24
  }
25
25
  break;
26
26
  }
27
- throw new Error(`Invalid config arguments: ${[options.action, options.key, options.value].filter(Boolean).join(' ')}`);
27
+ throw new Error(`Invalid config arguments: ${[options.action, options.key, options.value].filter(Boolean).join(" ")}`);
28
28
  }
29
29
  function writeJson(stdout, value) {
30
30
  stdout(`${JSON.stringify(value, null, 2)}\n`);
package/dist/config.js CHANGED
@@ -1,24 +1,24 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import { homedir } from 'node:os';
3
- import { dirname, join, resolve } from 'node:path';
4
- export const CONFIG_FILE_NAME = '.gji.json';
5
- export const GLOBAL_CONFIG_DIRECTORY = '.config/gji';
6
- export const GLOBAL_CONFIG_NAME = 'config.json';
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ export const CONFIG_FILE_NAME = ".gji.json";
5
+ export const GLOBAL_CONFIG_DIRECTORY = ".config/gji";
6
+ export const GLOBAL_CONFIG_NAME = "config.json";
7
7
  export const KNOWN_CONFIG_KEYS = new Set([
8
- 'branchPrefix',
9
- 'editor',
10
- 'hooks',
11
- 'installSaveTarget',
12
- 'shellIntegration',
13
- 'skipInstallPrompt',
14
- 'syncDefaultBranch',
15
- 'syncFiles',
16
- 'syncRemote',
17
- 'worktreePath',
8
+ "branchPrefix",
9
+ "editor",
10
+ "hooks",
11
+ "installSaveTarget",
12
+ "shellIntegration",
13
+ "skipInstallPrompt",
14
+ "syncDefaultBranch",
15
+ "syncFiles",
16
+ "syncRemote",
17
+ "worktreePath",
18
18
  ]);
19
19
  export const KNOWN_GLOBAL_CONFIG_KEYS = new Set([
20
20
  ...KNOWN_CONFIG_KEYS,
21
- 'repos',
21
+ "repos",
22
22
  ]);
23
23
  export const DEFAULT_CONFIG = Object.freeze({});
24
24
  export async function loadConfig(root) {
@@ -55,18 +55,22 @@ export async function loadEffectiveConfig(root, home = homedir(), onWarning) {
55
55
  // Warn about relative worktreePath: it must be absolute or tilde-prefixed.
56
56
  const worktreePathValue = merged.worktreePath;
57
57
  if (onWarning &&
58
- typeof worktreePathValue === 'string' &&
58
+ typeof worktreePathValue === "string" &&
59
59
  worktreePathValue.length > 0 &&
60
- !worktreePathValue.startsWith('/') &&
61
- !worktreePathValue.startsWith('~')) {
60
+ !worktreePathValue.startsWith("/") &&
61
+ !worktreePathValue.startsWith("~")) {
62
62
  onWarning(`gji: "worktreePath" must be an absolute path or start with ~, got "${worktreePathValue}" — using default\n`);
63
63
  }
64
64
  // Hooks are spread across all three layers so that different hook keys from
65
65
  // different layers both apply (e.g. global afterEnter + local afterCreate).
66
66
  // Within each key the higher-precedence layer wins (same spread order).
67
67
  const globalHooks = isPlainObject(globalBase.hooks) ? globalBase.hooks : {};
68
- const perRepoHooks = isPlainObject(perRepoConfig.hooks) ? perRepoConfig.hooks : {};
69
- const localHooks = isPlainObject(localConfig.config.hooks) ? localConfig.config.hooks : {};
68
+ const perRepoHooks = isPlainObject(perRepoConfig.hooks)
69
+ ? perRepoConfig.hooks
70
+ : {};
71
+ const localHooks = isPlainObject(localConfig.config.hooks)
72
+ ? localConfig.config.hooks
73
+ : {};
70
74
  if (Object.keys(globalHooks).length > 0 ||
71
75
  Object.keys(perRepoHooks).length > 0 ||
72
76
  Object.keys(localHooks).length > 0) {
@@ -79,7 +83,7 @@ export async function loadGlobalConfig(home = homedir()) {
79
83
  }
80
84
  export async function saveLocalConfig(root, config) {
81
85
  const path = join(root, CONFIG_FILE_NAME);
82
- await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
86
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
83
87
  return path;
84
88
  }
85
89
  export async function updateLocalConfigKey(root, key, value) {
@@ -94,7 +98,7 @@ export async function updateLocalConfigKey(root, key, value) {
94
98
  export async function saveGlobalConfig(config, home = homedir()) {
95
99
  const path = GLOBAL_CONFIG_FILE_PATH(home);
96
100
  await mkdir(dirname(path), { recursive: true });
97
- await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
101
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
98
102
  return path;
99
103
  }
100
104
  export async function unsetGlobalConfigKey(key, home = homedir()) {
@@ -115,8 +119,12 @@ export async function updateGlobalConfigKey(key, value, home = homedir()) {
115
119
  }
116
120
  export async function updateGlobalRepoConfigKey(repoRoot, key, value, home = homedir()) {
117
121
  const loaded = await loadGlobalConfig(home);
118
- const repos = isPlainObject(loaded.config.repos) ? { ...loaded.config.repos } : {};
119
- const existing = isPlainObject(repos[repoRoot]) ? repos[repoRoot] : {};
122
+ const repos = isPlainObject(loaded.config.repos)
123
+ ? { ...loaded.config.repos }
124
+ : {};
125
+ const existing = isPlainObject(repos[repoRoot])
126
+ ? repos[repoRoot]
127
+ : {};
120
128
  repos[repoRoot] = { ...existing, [key]: value };
121
129
  const nextConfig = { ...loaded.config, repos };
122
130
  await saveGlobalConfig(nextConfig, home);
@@ -139,11 +147,11 @@ export function parseConfigValue(value) {
139
147
  }
140
148
  export function resolveConfigString(config, key) {
141
149
  const value = config[key];
142
- return typeof value === 'string' && value.length > 0 ? value : undefined;
150
+ return typeof value === "string" && value.length > 0 ? value : undefined;
143
151
  }
144
152
  async function loadConfigFile(path) {
145
153
  try {
146
- const rawConfig = await readFile(path, 'utf8');
154
+ const rawConfig = await readFile(path, "utf8");
147
155
  const parsedConfig = JSON.parse(rawConfig);
148
156
  return {
149
157
  config: mergeConfig(parsedConfig),
@@ -178,25 +186,23 @@ function findPerRepoConfig(repos, repoRoot, home) {
178
186
  return {};
179
187
  }
180
188
  function expandTilde(value, home) {
181
- if (value === '~')
189
+ if (value === "~")
182
190
  return home;
183
- if (value.startsWith('~/'))
191
+ if (value.startsWith("~/"))
184
192
  return join(home, value.slice(2));
185
193
  return value;
186
194
  }
187
195
  function isPlainObject(value) {
188
- return typeof value === 'object' && value !== null && !Array.isArray(value);
196
+ return typeof value === "object" && value !== null && !Array.isArray(value);
189
197
  }
190
198
  function isMissingFileError(error) {
191
- return (error instanceof Error &&
192
- 'code' in error &&
193
- error.code === 'ENOENT');
199
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
194
200
  }
195
201
  function warnUnknownKeys(config, filePath, knownKeys, onWarning) {
196
202
  for (const key of Object.keys(config)) {
197
203
  if (!knownKeys.has(key)) {
198
204
  const suggestion = closestKey(key, knownKeys);
199
- const hint = suggestion ? ` (did you mean "${suggestion}"?)` : '';
205
+ const hint = suggestion ? ` (did you mean "${suggestion}"?)` : "";
200
206
  onWarning(`gji: unknown config key "${key}" in ${filePath}${hint}\n`);
201
207
  }
202
208
  }
@@ -1,3 +1,3 @@
1
- export type PathConflictChoice = 'abort' | 'reuse';
1
+ export type PathConflictChoice = "abort" | "reuse";
2
2
  export declare function pathExists(path: string): Promise<boolean>;
3
3
  export declare function promptForPathConflict(path: string): Promise<PathConflictChoice>;
package/dist/conflict.js CHANGED
@@ -1,6 +1,6 @@
1
- import { access } from 'node:fs/promises';
2
- import { constants } from 'node:fs';
3
- import { isCancel, select } from '@clack/prompts';
1
+ import { constants } from "node:fs";
2
+ import { access } from "node:fs/promises";
3
+ import { isCancel, select } from "@clack/prompts";
4
4
  export async function pathExists(path) {
5
5
  try {
6
6
  await access(path, constants.F_OK);
@@ -14,12 +14,20 @@ export async function promptForPathConflict(path) {
14
14
  const choice = await select({
15
15
  message: `Target path already exists: ${path}`,
16
16
  options: [
17
- { value: 'abort', label: 'Abort', hint: 'Keep the existing directory untouched' },
18
- { value: 'reuse', label: 'Reuse path', hint: 'Print the existing path and stop' },
17
+ {
18
+ value: "abort",
19
+ label: "Abort",
20
+ hint: "Keep the existing directory untouched",
21
+ },
22
+ {
23
+ value: "reuse",
24
+ label: "Reuse path",
25
+ hint: "Print the existing path and stop",
26
+ },
19
27
  ],
20
28
  });
21
29
  if (isCancel(choice)) {
22
- return 'abort';
30
+ return "abort";
23
31
  }
24
32
  return choice;
25
33
  }
package/dist/editor.js CHANGED
@@ -1,17 +1,37 @@
1
- import { spawn } from 'node:child_process';
1
+ import { spawn } from "node:child_process";
2
2
  // Ordered by likely popularity among the target audience.
3
3
  export const EDITORS = [
4
- { cli: 'cursor', name: 'Cursor', newWindowFlag: '--new-window', supportsWorkspace: true },
5
- { cli: 'code', name: 'VS Code', newWindowFlag: '--new-window', supportsWorkspace: true },
6
- { cli: 'windsurf', name: 'Windsurf', newWindowFlag: '--new-window', supportsWorkspace: true },
7
- { cli: 'zed', name: 'Zed', supportsWorkspace: false },
8
- { cli: 'subl', name: 'Sublime Text', newWindowFlag: '--new-window', supportsWorkspace: false },
4
+ {
5
+ cli: "cursor",
6
+ name: "Cursor",
7
+ newWindowFlag: "--new-window",
8
+ supportsWorkspace: true,
9
+ },
10
+ {
11
+ cli: "code",
12
+ name: "VS Code",
13
+ newWindowFlag: "--new-window",
14
+ supportsWorkspace: true,
15
+ },
16
+ {
17
+ cli: "windsurf",
18
+ name: "Windsurf",
19
+ newWindowFlag: "--new-window",
20
+ supportsWorkspace: true,
21
+ },
22
+ { cli: "zed", name: "Zed", supportsWorkspace: false },
23
+ {
24
+ cli: "subl",
25
+ name: "Sublime Text",
26
+ newWindowFlag: "--new-window",
27
+ supportsWorkspace: false,
28
+ },
9
29
  ];
10
30
  export async function defaultSpawnEditor(cli, args) {
11
- const child = spawn(cli, args, { detached: true, stdio: 'ignore' });
31
+ const child = spawn(cli, args, { detached: true, stdio: "ignore" });
12
32
  await new Promise((resolve, reject) => {
13
- child.once('error', reject);
14
- child.once('spawn', resolve);
33
+ child.once("error", reject);
34
+ child.once("spawn", resolve);
15
35
  });
16
36
  child.unref();
17
37
  }
@@ -7,3 +7,4 @@
7
7
  * - Rejects patterns that are absolute paths or contain `..` segments.
8
8
  */
9
9
  export declare function syncFiles(mainRoot: string, targetPath: string, patterns: string[]): Promise<void>;
10
+ export declare function validateSyncFilePattern(pattern: string): string;
package/dist/file-sync.js CHANGED
@@ -1,5 +1,5 @@
1
- import { copyFile, mkdir, stat } from 'node:fs/promises';
2
- import { dirname, isAbsolute, join, normalize } from 'node:path';
1
+ import { copyFile, mkdir, stat } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, normalize } from "node:path";
3
3
  /**
4
4
  * Copies files matching each pattern (relative to mainRoot) into the equivalent
5
5
  * relative path under targetPath, creating parent directories as needed.
@@ -10,13 +10,7 @@ import { dirname, isAbsolute, join, normalize } from 'node:path';
10
10
  */
11
11
  export async function syncFiles(mainRoot, targetPath, patterns) {
12
12
  for (const pattern of patterns) {
13
- if (isAbsolute(pattern)) {
14
- throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
15
- }
16
- const normalized = normalize(pattern);
17
- if (normalized.startsWith('..')) {
18
- throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
19
- }
13
+ const normalized = validateSyncFilePattern(pattern);
20
14
  const sourcePath = join(mainRoot, normalized);
21
15
  const destPath = join(targetPath, normalized);
22
16
  // Skip silently if source does not exist
@@ -33,6 +27,16 @@ export async function syncFiles(mainRoot, targetPath, patterns) {
33
27
  await copyFile(sourcePath, destPath);
34
28
  }
35
29
  }
30
+ export function validateSyncFilePattern(pattern) {
31
+ if (isAbsolute(pattern)) {
32
+ throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
33
+ }
34
+ const normalized = normalize(pattern);
35
+ if (normalized.startsWith("..")) {
36
+ throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
37
+ }
38
+ return normalized;
39
+ }
36
40
  async function fileExists(path) {
37
41
  try {
38
42
  await stat(path);
@@ -47,6 +51,6 @@ async function fileExists(path) {
47
51
  }
48
52
  function isNotFoundError(error) {
49
53
  return (error instanceof Error &&
50
- 'code' in error &&
51
- error.code === 'ENOENT');
54
+ "code" in error &&
55
+ error.code === "ENOENT");
52
56
  }
package/dist/git.d.ts CHANGED
@@ -3,7 +3,7 @@ export interface WorktreeHealth {
3
3
  behind: number;
4
4
  hasUpstream: boolean;
5
5
  upstreamGone: boolean;
6
- status: 'clean' | 'dirty';
6
+ status: "clean" | "dirty";
7
7
  }
8
8
  export declare function runGit(cwd: string, args: string[]): Promise<string>;
9
9
  export declare function readWorktreeHealth(cwd: string): Promise<WorktreeHealth>;
package/dist/git.js CHANGED
@@ -1,9 +1,9 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
3
  const execFileAsync = promisify(execFile);
4
4
  export async function runGit(cwd, args) {
5
5
  try {
6
- const { stdout } = await execFileAsync('git', args, { cwd });
6
+ const { stdout } = await execFileAsync("git", args, { cwd });
7
7
  return stdout.trim();
8
8
  }
9
9
  catch (error) {
@@ -12,16 +12,18 @@ export async function runGit(cwd, args) {
12
12
  }
13
13
  }
14
14
  export async function readWorktreeHealth(cwd) {
15
- const { stdout } = await execFileAsync('git', ['status', '--porcelain=v2', '--branch'], { cwd });
15
+ const { stdout } = await execFileAsync("git", ["status", "--porcelain=v2", "--branch"], { cwd });
16
16
  return parseWorktreeHealth(stdout);
17
17
  }
18
18
  export async function isDirtyWorktree(cwd) {
19
19
  const health = await readWorktreeHealth(cwd);
20
- return health.status === 'dirty';
20
+ return health.status === "dirty";
21
21
  }
22
- export async function isBranchMergedInto(cwd, branch, base = 'HEAD') {
22
+ export async function isBranchMergedInto(cwd, branch, base = "HEAD") {
23
23
  try {
24
- await execFileAsync('git', ['merge-base', '--is-ancestor', branch, base], { cwd });
24
+ await execFileAsync("git", ["merge-base", "--is-ancestor", branch, base], {
25
+ cwd,
26
+ });
25
27
  return true;
26
28
  }
27
29
  catch (error) {
@@ -32,10 +34,10 @@ export async function isBranchMergedInto(cwd, branch, base = 'HEAD') {
32
34
  }
33
35
  }
34
36
  export async function resolveRemoteDefaultBranch(cwd, remote) {
35
- const { stdout } = await execFileAsync('git', ['ls-remote', '--symref', remote, 'HEAD'], { cwd });
37
+ const { stdout } = await execFileAsync("git", ["ls-remote", "--symref", remote, "HEAD"], { cwd });
36
38
  const refLine = stdout
37
- .split('\n')
38
- .find((line) => line.startsWith('ref: refs/heads/'));
39
+ .split("\n")
40
+ .find((line) => line.startsWith("ref: refs/heads/"));
39
41
  if (!refLine) {
40
42
  return null;
41
43
  }
@@ -44,7 +46,7 @@ export async function resolveRemoteDefaultBranch(cwd, remote) {
44
46
  }
45
47
  export async function readBranchLastCommitTimestamp(cwd, branch) {
46
48
  try {
47
- const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%ct', branch], { cwd });
49
+ const { stdout } = await execFileAsync("git", ["log", "-1", "--format=%ct", branch], { cwd });
48
50
  const timestamp = Number(stdout.trim());
49
51
  return Number.isFinite(timestamp) ? timestamp : null;
50
52
  }
@@ -58,12 +60,12 @@ function parseWorktreeHealth(output) {
58
60
  let hasUpstream = false;
59
61
  let hasAb = false;
60
62
  let dirty = false;
61
- for (const line of output.split('\n').filter(Boolean)) {
62
- if (line.startsWith('# branch.upstream ')) {
63
+ for (const line of output.split("\n").filter(Boolean)) {
64
+ if (line.startsWith("# branch.upstream ")) {
63
65
  hasUpstream = true;
64
66
  continue;
65
67
  }
66
- if (line.startsWith('# branch.ab ')) {
68
+ if (line.startsWith("# branch.ab ")) {
67
69
  const match = /^# branch\.ab \+(\d+) -(\d+)$/.exec(line);
68
70
  if (!match) {
69
71
  throw new Error(`Unexpected branch.ab output: '${line}'`);
@@ -73,7 +75,7 @@ function parseWorktreeHealth(output) {
73
75
  behind = Number(match[2]);
74
76
  continue;
75
77
  }
76
- if (!line.startsWith('# ')) {
78
+ if (!line.startsWith("# ")) {
77
79
  dirty = true;
78
80
  }
79
81
  }
@@ -82,11 +84,11 @@ function parseWorktreeHealth(output) {
82
84
  behind,
83
85
  hasUpstream,
84
86
  upstreamGone: hasUpstream && !hasAb,
85
- status: dirty ? 'dirty' : 'clean',
87
+ status: dirty ? "dirty" : "clean",
86
88
  };
87
89
  }
88
90
  function hasExitCode(error, code) {
89
- return error instanceof Error
90
- && 'code' in error
91
- && error.code === code;
91
+ return (error instanceof Error &&
92
+ "code" in error &&
93
+ error.code === code);
92
94
  }