@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
package/dist/pr.js CHANGED
@@ -1,18 +1,18 @@
1
- import { mkdir } from 'node:fs/promises';
2
- import { basename, dirname } from 'node:path';
3
- import { execFile } from 'node:child_process';
4
- import { promisify } from 'node:util';
5
- import { loadEffectiveConfig, resolveConfigString } from './config.js';
6
- import { syncFiles } from './file-sync.js';
7
- import { pathExists, promptForPathConflict } from './conflict.js';
8
- import { extractHooks, runHook } from './hooks.js';
9
- import { appendHistory } from './history.js';
10
- import { isHeadless } from './headless.js';
11
- import { maybeRunInstallPrompt } from './install-prompt.js';
12
- import { detectRepository, resolveWorktreePath } from './repo.js';
13
- import { writeShellOutput } from './shell-handoff.js';
1
+ import { execFile } from "node:child_process";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { basename, dirname } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { loadEffectiveConfig, resolveConfigString } from "./config.js";
6
+ import { pathExists, promptForPathConflict, } from "./conflict.js";
7
+ import { syncFiles } from "./file-sync.js";
8
+ import { isHeadless } from "./headless.js";
9
+ import { appendHistory } from "./history.js";
10
+ import { extractHooks, runHook } from "./hooks.js";
11
+ import { maybeRunInstallPrompt, } from "./install-prompt.js";
12
+ import { detectRepository, resolveWorktreePath } from "./repo.js";
13
+ import { writeShellOutput } from "./shell-handoff.js";
14
14
  const execFileAsync = promisify(execFile);
15
- const PR_OUTPUT_FILE_ENV = 'GJI_PR_OUTPUT_FILE';
15
+ const PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
16
16
  export function parsePrInput(input) {
17
17
  if (/^\d+$/.test(input))
18
18
  return input;
@@ -42,8 +42,10 @@ export function createPrCommand(dependencies = {}) {
42
42
  const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
43
43
  const branchName = `pr/${prNumber}`;
44
44
  const remoteRef = `refs/remotes/origin/pull/${prNumber}/head`;
45
- const rawBasePath = resolveConfigString(config, 'worktreePath');
46
- const configuredBasePath = rawBasePath?.startsWith('/') || rawBasePath?.startsWith('~') ? rawBasePath : undefined;
45
+ const rawBasePath = resolveConfigString(config, "worktreePath");
46
+ const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~")
47
+ ? rawBasePath
48
+ : undefined;
47
49
  const worktreePath = resolveWorktreePath(repository.repoRoot, branchName, configuredBasePath);
48
50
  if (await pathExists(worktreePath)) {
49
51
  if (options.json || isHeadless()) {
@@ -58,7 +60,7 @@ export function createPrCommand(dependencies = {}) {
58
60
  return 1;
59
61
  }
60
62
  const choice = await prompt(worktreePath);
61
- if (choice === 'reuse') {
63
+ if (choice === "reuse") {
62
64
  appendHistory(worktreePath, branchName).catch(() => undefined);
63
65
  await writeOutput(worktreePath, options.stdout);
64
66
  return 0;
@@ -92,12 +94,12 @@ export function createPrCommand(dependencies = {}) {
92
94
  await mkdir(dirname(worktreePath), { recursive: true });
93
95
  const branchAlreadyExists = await localBranchExists(repository.repoRoot, branchName);
94
96
  const worktreeArgs = branchAlreadyExists
95
- ? ['worktree', 'add', worktreePath, branchName]
96
- : ['worktree', 'add', '-b', branchName, worktreePath, remoteRef];
97
- await execFileAsync('git', worktreeArgs, { cwd: repository.repoRoot });
97
+ ? ["worktree", "add", worktreePath, branchName]
98
+ : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
99
+ await execFileAsync("git", worktreeArgs, { cwd: repository.repoRoot });
98
100
  // Sync files from main worktree before afterCreate so synced files are available to install scripts.
99
101
  const syncPatterns = Array.isArray(config.syncFiles)
100
- ? config.syncFiles.filter((p) => typeof p === 'string')
102
+ ? config.syncFiles.filter((p) => typeof p === "string")
101
103
  : [];
102
104
  for (const pattern of syncPatterns) {
103
105
  try {
@@ -109,7 +111,11 @@ export function createPrCommand(dependencies = {}) {
109
111
  }
110
112
  await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
111
113
  const hooks = extractHooks(config);
112
- await runHook(hooks.afterCreate, worktreePath, { branch: branchName, path: worktreePath, repo: basename(repository.repoRoot) }, options.stderr);
114
+ await runHook(hooks.afterCreate, worktreePath, {
115
+ branch: branchName,
116
+ path: worktreePath,
117
+ repo: basename(repository.repoRoot),
118
+ }, options.stderr);
113
119
  if (options.json) {
114
120
  options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}\n`);
115
121
  }
@@ -122,7 +128,7 @@ export function createPrCommand(dependencies = {}) {
122
128
  }
123
129
  async function localBranchExists(repoRoot, branchName) {
124
130
  try {
125
- await execFileAsync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`], { cwd: repoRoot });
131
+ await execFileAsync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
126
132
  return true;
127
133
  }
128
134
  catch {
@@ -133,7 +139,7 @@ export const runPrCommand = createPrCommand();
133
139
  async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
134
140
  for (const sourceRef of listPullRequestSourceRefs(input, prNumber)) {
135
141
  try {
136
- await execFileAsync('git', ['fetch', 'origin', `${sourceRef}:${remoteRef}`], { cwd: repoRoot });
142
+ await execFileAsync("git", ["fetch", "origin", `${sourceRef}:${remoteRef}`], { cwd: repoRoot });
137
143
  return;
138
144
  }
139
145
  catch {
@@ -143,32 +149,39 @@ async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
143
149
  throw new Error(`No pull request ref found for #${prNumber}`);
144
150
  }
145
151
  function listPullRequestSourceRefs(input, prNumber) {
146
- const allForges = ['github', 'gitlab', 'bitbucket'];
152
+ const allForges = [
153
+ "github",
154
+ "gitlab",
155
+ "bitbucket",
156
+ ];
147
157
  const preferredForge = detectPullRequestForge(input);
148
- const orderedForges = preferredForge === 'unknown'
158
+ const orderedForges = preferredForge === "unknown"
149
159
  ? allForges
150
- : [preferredForge, ...allForges.filter((forge) => forge !== preferredForge)];
160
+ : [
161
+ preferredForge,
162
+ ...allForges.filter((forge) => forge !== preferredForge),
163
+ ];
151
164
  return orderedForges.map((forge) => sourceRefForForge(forge, prNumber));
152
165
  }
153
166
  function detectPullRequestForge(input) {
154
167
  if (/\/pull-requests\/\d+/.test(input)) {
155
- return 'bitbucket';
168
+ return "bitbucket";
156
169
  }
157
170
  if (/\/merge_requests\/\d+/.test(input)) {
158
- return 'gitlab';
171
+ return "gitlab";
159
172
  }
160
173
  if (/\/pull\/\d+/.test(input)) {
161
- return 'github';
174
+ return "github";
162
175
  }
163
- return 'unknown';
176
+ return "unknown";
164
177
  }
165
178
  function sourceRefForForge(forge, prNumber) {
166
179
  switch (forge) {
167
- case 'bitbucket':
180
+ case "bitbucket":
168
181
  return `refs/pull-requests/${prNumber}/from`;
169
- case 'github':
182
+ case "github":
170
183
  return `refs/pull/${prNumber}/head`;
171
- case 'gitlab':
184
+ case "gitlab":
172
185
  return `refs/merge-requests/${prNumber}/head`;
173
186
  }
174
187
  }
package/dist/remove.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type WorktreeEntry } from './repo.js';
1
+ import { type WorktreeEntry } from "./repo.js";
2
2
  export interface RemoveCommandOptions {
3
3
  branch?: string;
4
4
  cwd: string;
package/dist/remove.js CHANGED
@@ -1,26 +1,27 @@
1
- import { basename } from 'node:path';
2
- import { confirm, isCancel, select } from '@clack/prompts';
3
- import { loadEffectiveConfig } from './config.js';
4
- import { extractHooks, runHook } from './hooks.js';
5
- import { isHeadless } from './headless.js';
6
- import { sortByCurrentFirst } from './repo.js';
7
- import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeDirtyError, loadLinkedWorktrees, removeWorktree, } from './worktree-management.js';
8
- import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree } from './worktree-prompts.js';
9
- import { writeShellOutput } from './shell-handoff.js';
10
- const REMOVE_OUTPUT_FILE_ENV = 'GJI_REMOVE_OUTPUT_FILE';
1
+ import { basename } from "node:path";
2
+ import { confirm, isCancel, select } from "@clack/prompts";
3
+ import { loadEffectiveConfig } from "./config.js";
4
+ import { isHeadless } from "./headless.js";
5
+ import { extractHooks, runHook } from "./hooks.js";
6
+ import { sortByCurrentFirst } from "./repo.js";
7
+ import { writeShellOutput } from "./shell-handoff.js";
8
+ import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeDirtyError, loadLinkedWorktrees, removeWorktree, } from "./worktree-management.js";
9
+ import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree, } from "./worktree-prompts.js";
10
+ const REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
11
11
  export function createRemoveCommand(dependencies = {}) {
12
12
  const promptForWorktree = dependencies.promptForWorktree ?? defaultPromptForWorktree;
13
13
  const confirmRemoval = dependencies.confirmRemoval ?? defaultConfirmRemoval;
14
- const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
14
+ const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ??
15
+ defaultConfirmForceRemoveWorktree;
15
16
  const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
16
17
  return async function runRemoveCommand(options) {
17
18
  const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
18
19
  if (linkedWorktrees.length === 0) {
19
- emitError(options, 'No linked worktrees to finish');
20
+ emitError(options, "No linked worktrees to finish");
20
21
  return 1;
21
22
  }
22
23
  if (!options.branch && (options.json || isHeadless())) {
23
- const message = 'branch argument is required';
24
+ const message = "branch argument is required";
24
25
  if (options.json) {
25
26
  emitError(options, message);
26
27
  }
@@ -29,9 +30,10 @@ export function createRemoveCommand(dependencies = {}) {
29
30
  }
30
31
  return 1;
31
32
  }
32
- const selection = options.branch ?? (await promptForWorktree(sortByCurrentFirst(linkedWorktrees)));
33
+ const selection = options.branch ??
34
+ (await promptForWorktree(sortByCurrentFirst(linkedWorktrees)));
33
35
  if (!selection) {
34
- options.stderr('Aborted\n');
36
+ options.stderr("Aborted\n");
35
37
  return 1;
36
38
  }
37
39
  const worktree = linkedWorktrees.find((entry) => entry.branch === selection || entry.path === selection);
@@ -40,7 +42,7 @@ export function createRemoveCommand(dependencies = {}) {
40
42
  return 1;
41
43
  }
42
44
  if (!options.dryRun && !options.force && (options.json || isHeadless())) {
43
- const message = '--force is required';
45
+ const message = "--force is required";
44
46
  if (options.json) {
45
47
  emitError(options, message);
46
48
  }
@@ -49,8 +51,10 @@ export function createRemoveCommand(dependencies = {}) {
49
51
  }
50
52
  return 1;
51
53
  }
52
- if (!options.dryRun && !options.force && !(await confirmRemoval(worktree))) {
53
- options.stderr('Aborted\n');
54
+ if (!options.dryRun &&
55
+ !options.force &&
56
+ !(await confirmRemoval(worktree))) {
57
+ options.stderr("Aborted\n");
54
58
  return 1;
55
59
  }
56
60
  if (options.dryRun) {
@@ -58,14 +62,20 @@ export function createRemoveCommand(dependencies = {}) {
58
62
  options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}\n`);
59
63
  }
60
64
  else {
61
- const desc = worktree.branch ? `branch: ${worktree.branch}` : 'detached';
65
+ const desc = worktree.branch
66
+ ? `branch: ${worktree.branch}`
67
+ : "detached";
62
68
  options.stdout(`Would remove worktree at ${worktree.path} (${desc})\n`);
63
69
  }
64
70
  return 0;
65
71
  }
66
72
  const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
67
73
  const hooks = extractHooks(config);
68
- await runHook(hooks.beforeRemove, worktree.path, { branch: worktree.branch ?? undefined, path: worktree.path, repo: basename(repository.repoRoot) }, options.stderr);
74
+ await runHook(hooks.beforeRemove, worktree.path, {
75
+ branch: worktree.branch ?? undefined,
76
+ path: worktree.path,
77
+ repo: basename(repository.repoRoot),
78
+ }, options.stderr);
69
79
  try {
70
80
  await removeWorktree(repository.repoRoot, worktree.path);
71
81
  }
@@ -73,8 +83,9 @@ export function createRemoveCommand(dependencies = {}) {
73
83
  if (!isWorktreeDirtyError(error)) {
74
84
  throw error;
75
85
  }
76
- if (!options.force && !(await confirmForceRemoveWorktree(worktree.path))) {
77
- options.stderr('Aborted\n');
86
+ if (!options.force &&
87
+ !(await confirmForceRemoveWorktree(worktree.path))) {
88
+ options.stderr("Aborted\n");
78
89
  return 1;
79
90
  }
80
91
  try {
@@ -93,7 +104,8 @@ export function createRemoveCommand(dependencies = {}) {
93
104
  if (!isBranchUnmergedError(error)) {
94
105
  throw error;
95
106
  }
96
- if (options.force || (await confirmForceDeleteBranch(worktree.branch))) {
107
+ if (options.force ||
108
+ (await confirmForceDeleteBranch(worktree.branch))) {
97
109
  try {
98
110
  await forceDeleteBranch(repository.repoRoot, worktree.branch);
99
111
  }
@@ -118,10 +130,10 @@ export function createRemoveCommand(dependencies = {}) {
118
130
  export const runRemoveCommand = createRemoveCommand();
119
131
  async function defaultPromptForWorktree(worktrees) {
120
132
  const choice = await select({
121
- message: 'Choose a worktree to finish',
133
+ message: "Choose a worktree to finish",
122
134
  options: worktrees.map((worktree) => ({
123
135
  hint: worktree.isCurrent ? `${worktree.path} (current)` : worktree.path,
124
- label: worktree.branch ?? '(detached)',
136
+ label: worktree.branch ?? "(detached)",
125
137
  value: worktree.path,
126
138
  })),
127
139
  });
@@ -132,8 +144,8 @@ async function defaultConfirmRemoval(worktree) {
132
144
  message: worktree.branch
133
145
  ? `Remove worktree and delete branch ${worktree.branch}?`
134
146
  : `Remove detached worktree ${worktree.path}?`,
135
- active: 'Yes',
136
- inactive: 'No',
147
+ active: "Yes",
148
+ inactive: "No",
137
149
  initialValue: true,
138
150
  });
139
151
  return !isCancel(choice) && choice;
@@ -1,8 +1,8 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import { homedir } from 'node:os';
3
- import { basename, dirname, join, resolve } from 'node:path';
4
- import { GLOBAL_CONFIG_DIRECTORY } from './config.js';
5
- const REGISTRY_FILE_NAME = 'repos.json';
1
+ import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { GLOBAL_CONFIG_DIRECTORY } from "./config.js";
5
+ const REGISTRY_FILE_NAME = "repos.json";
6
6
  const MAX_REGISTRY_ENTRIES = 100;
7
7
  export function REGISTRY_FILE_PATH(home = homedir()) {
8
8
  const configDir = process.env.GJI_CONFIG_DIR;
@@ -14,7 +14,7 @@ export function REGISTRY_FILE_PATH(home = homedir()) {
14
14
  export async function loadRegistry(home = homedir()) {
15
15
  const path = REGISTRY_FILE_PATH(home);
16
16
  try {
17
- const raw = await readFile(path, 'utf8');
17
+ const raw = await readFile(path, "utf8");
18
18
  const parsed = JSON.parse(raw);
19
19
  if (!Array.isArray(parsed))
20
20
  return [];
@@ -24,29 +24,55 @@ export async function loadRegistry(home = homedir()) {
24
24
  return [];
25
25
  }
26
26
  }
27
+ async function canonicalizeRepoPath(repoPath) {
28
+ try {
29
+ return await realpath(repoPath);
30
+ }
31
+ catch {
32
+ return resolve(repoPath);
33
+ }
34
+ }
27
35
  export async function registerRepo(repoPath, home = homedir()) {
28
36
  const registryPath = REGISTRY_FILE_PATH(home);
29
- const existing = await loadRegistry(home);
37
+ const existing = await normalizeRegistryForWrite(await loadRegistry(home));
38
+ const canonicalRepoPath = await canonicalizeRepoPath(repoPath);
30
39
  // Skip write if this repo is already the most-recently-used entry (common case).
31
- if (existing.length > 0 && existing[0].path === repoPath)
40
+ if (existing.length > 0 && existing[0].path === canonicalRepoPath)
32
41
  return;
33
42
  const entry = {
34
43
  lastUsed: Date.now(),
35
- name: basename(repoPath),
36
- path: repoPath,
44
+ name: basename(canonicalRepoPath),
45
+ path: canonicalRepoPath,
37
46
  };
38
- const filtered = existing.filter((e) => e.path !== repoPath);
47
+ const filtered = existing.filter((e) => e.path !== canonicalRepoPath);
39
48
  const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
40
49
  await mkdir(dirname(registryPath), { recursive: true });
41
- await writeFile(registryPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
50
+ await writeFile(registryPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
51
+ }
52
+ async function normalizeRegistryForWrite(entries) {
53
+ const normalized = [];
54
+ const seenPaths = new Set();
55
+ for (const entry of entries) {
56
+ const canonicalPath = await canonicalizeRepoPath(entry.path);
57
+ if (seenPaths.has(canonicalPath)) {
58
+ continue;
59
+ }
60
+ seenPaths.add(canonicalPath);
61
+ normalized.push({
62
+ ...entry,
63
+ name: basename(canonicalPath),
64
+ path: canonicalPath,
65
+ });
66
+ }
67
+ return normalized;
42
68
  }
43
69
  function isRegistryEntry(value) {
44
- return (typeof value === 'object' &&
70
+ return (typeof value === "object" &&
45
71
  value !== null &&
46
- 'path' in value &&
47
- typeof value.path === 'string' &&
48
- 'name' in value &&
49
- typeof value.name === 'string' &&
50
- 'lastUsed' in value &&
51
- typeof value.lastUsed === 'number');
72
+ "path" in value &&
73
+ typeof value.path === "string" &&
74
+ "name" in value &&
75
+ typeof value.name === "string" &&
76
+ "lastUsed" in value &&
77
+ typeof value.lastUsed === "number");
52
78
  }
package/dist/repo.js CHANGED
@@ -1,9 +1,9 @@
1
- import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
2
- import { homedir } from 'node:os';
3
- import { runGit } from './git.js';
1
+ import { homedir } from "node:os";
2
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { runGit } from "./git.js";
4
4
  export async function detectRepository(cwd) {
5
- const currentRoot = await runGit(cwd, ['rev-parse', '--show-toplevel']);
6
- const rawCommonDir = await runGit(cwd, ['rev-parse', '--git-common-dir']);
5
+ const currentRoot = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
6
+ const rawCommonDir = await runGit(cwd, ["rev-parse", "--git-common-dir"]);
7
7
  const gitCommonDir = isAbsolute(rawCommonDir)
8
8
  ? rawCommonDir
9
9
  : resolve(currentRoot, rawCommonDir);
@@ -17,71 +17,72 @@ export async function detectRepository(cwd) {
17
17
  };
18
18
  }
19
19
  export function resolveWorktreePath(repoRoot, branch, basePath) {
20
- const segments = branch.split('/').filter(Boolean);
20
+ const segments = branch.split("/").filter(Boolean);
21
21
  if (segments.length === 0) {
22
- throw new Error('Branch name must not be empty.');
22
+ throw new Error("Branch name must not be empty.");
23
23
  }
24
- if (segments.some((segment) => segment === '.' || segment === '..')) {
24
+ if (segments.some((segment) => segment === "." || segment === "..")) {
25
25
  throw new Error(`Branch name '${branch}' contains an invalid path segment.`);
26
26
  }
27
27
  const base = basePath
28
28
  ? expandTildeInPath(basePath)
29
- : join(dirname(repoRoot), 'worktrees', basename(repoRoot));
29
+ : join(dirname(repoRoot), "worktrees", basename(repoRoot));
30
30
  return join(base, ...segments);
31
31
  }
32
32
  export function validateBranchName(name) {
33
33
  if (name.length === 0) {
34
- return 'Branch name must not be empty.';
34
+ return "Branch name must not be empty.";
35
35
  }
36
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional control chars to reject in git branch names
36
37
  if (/[\x00-\x1f\x7f ~^:?*[\\\s]/.test(name)) {
37
38
  return `Branch name '${name}' contains an invalid character.`;
38
39
  }
39
- if (name.startsWith('-')) {
40
+ if (name.startsWith("-")) {
40
41
  return `Branch name '${name}' must not start with a dash.`;
41
42
  }
42
- if (name.startsWith('/') || name.endsWith('/') || name.includes('//')) {
43
+ if (name.startsWith("/") || name.endsWith("/") || name.includes("//")) {
43
44
  return `Branch name '${name}' has invalid slash placement.`;
44
45
  }
45
- if (name.includes('..')) {
46
+ if (name.includes("..")) {
46
47
  return `Branch name '${name}' must not contain '..'.`;
47
48
  }
48
- if (name.endsWith('.')) {
49
+ if (name.endsWith(".")) {
49
50
  return `Branch name '${name}' must not end with '.'.`;
50
51
  }
51
- if (name.includes('@{')) {
52
+ if (name.includes("@{")) {
52
53
  return `Branch name '${name}' must not contain '@{'.`;
53
54
  }
54
- if (name === '@') {
55
+ if (name === "@") {
55
56
  return "Branch name cannot be '@'.";
56
57
  }
57
- for (const segment of name.split('/')) {
58
- if (segment.startsWith('.')) {
58
+ for (const segment of name.split("/")) {
59
+ if (segment.startsWith(".")) {
59
60
  return `Branch name '${name}' contains a path component starting with '.'.`;
60
61
  }
61
- if (segment.endsWith('.lock')) {
62
+ if (segment.endsWith(".lock")) {
62
63
  return `Branch name '${name}' contains a path component ending with '.lock'.`;
63
64
  }
64
65
  }
65
66
  return null;
66
67
  }
67
68
  function expandTildeInPath(p) {
68
- if (p === '~')
69
+ if (p === "~")
69
70
  return homedir();
70
- if (p.startsWith('~/'))
71
+ if (p.startsWith("~/"))
71
72
  return join(homedir(), p.slice(2));
72
73
  return p;
73
74
  }
74
75
  export async function listWorktrees(cwd) {
75
76
  const [output, currentRoot] = await Promise.all([
76
- runGit(cwd, ['worktree', 'list', '--porcelain']),
77
- runGit(cwd, ['rev-parse', '--show-toplevel']),
77
+ runGit(cwd, ["worktree", "list", "--porcelain"]),
78
+ runGit(cwd, ["rev-parse", "--show-toplevel"]),
78
79
  ]);
79
- const entries = output.split('\n\n').filter(Boolean);
80
+ const entries = output.split("\n\n").filter(Boolean);
80
81
  return entries.map((entry) => {
81
- const path = findPorcelainValue(entry, 'worktree');
82
- const branchRef = findOptionalPorcelainValue(entry, 'branch');
82
+ const path = findPorcelainValue(entry, "worktree");
83
+ const branchRef = findOptionalPorcelainValue(entry, "branch");
83
84
  return {
84
- branch: branchRef ? branchRef.replace('refs/heads/', '') : null,
85
+ branch: branchRef ? branchRef.replace("refs/heads/", "") : null,
85
86
  isCurrent: path === currentRoot,
86
87
  path,
87
88
  };
@@ -105,7 +106,7 @@ function findPorcelainValue(block, key) {
105
106
  }
106
107
  function findOptionalPorcelainValue(block, key) {
107
108
  const line = block
108
- .split('\n')
109
+ .split("\n")
109
110
  .find((candidate) => candidate.startsWith(`${key} `));
110
111
  if (!line) {
111
112
  return null;
package/dist/root.js CHANGED
@@ -1,6 +1,6 @@
1
- import { detectRepository } from './repo.js';
2
- import { writeShellOutput } from './shell-handoff.js';
3
- const ROOT_OUTPUT_FILE_ENV = 'GJI_ROOT_OUTPUT_FILE';
1
+ import { detectRepository } from "./repo.js";
2
+ import { writeShellOutput } from "./shell-handoff.js";
3
+ const ROOT_OUTPUT_FILE_ENV = "GJI_ROOT_OUTPUT_FILE";
4
4
  export async function runRootCommand(options) {
5
5
  const repository = await detectRepository(options.cwd);
6
6
  if (!options.print && process.env[ROOT_OUTPUT_FILE_ENV]) {
@@ -1 +1 @@
1
- export declare function renderShellCompletion(shell: 'bash' | 'fish' | 'zsh'): string;
1
+ export declare function renderShellCompletion(shell: "bash" | "fish" | "zsh"): string;