@solaqua/gji 0.2.0 → 0.2.2

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.
package/dist/clean.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { WorktreeEntry } from './repo.js';
2
2
  export interface CleanCommandOptions {
3
3
  cwd: string;
4
+ dryRun?: boolean;
4
5
  force?: boolean;
5
6
  json?: boolean;
6
7
  stderr: (chunk: string) => void;
package/dist/clean.js CHANGED
@@ -14,7 +14,7 @@ export function createCleanCommand(dependencies = {}) {
14
14
  emitError(options, 'No linked worktrees to clean');
15
15
  return 1;
16
16
  }
17
- if (!options.force && (options.json || isHeadless())) {
17
+ if (!options.dryRun && !options.force && (options.json || isHeadless())) {
18
18
  const message = '--force is required';
19
19
  if (options.json) {
20
20
  emitError(options, message);
@@ -24,8 +24,9 @@ export function createCleanCommand(dependencies = {}) {
24
24
  }
25
25
  return 1;
26
26
  }
27
- // With --force, skip selection prompt and target all candidates.
28
- const selections = options.force
27
+ // With --force, or dry-run in headless/json mode, skip selection prompt and target all candidates.
28
+ const shouldSelectAll = options.force || (options.dryRun && (options.json || isHeadless()));
29
+ const selections = shouldSelectAll
29
30
  ? cleanupCandidates.map((w) => w.path)
30
31
  : await promptForWorktrees(cleanupCandidates);
31
32
  if (!selections || selections.length === 0) {
@@ -37,10 +38,23 @@ export function createCleanCommand(dependencies = {}) {
37
38
  options.stderr('Selected worktree no longer exists\n');
38
39
  return 1;
39
40
  }
40
- if (!options.force && !(await confirmRemoval(selectedWorktrees))) {
41
+ if (!options.dryRun && !options.force && !(await confirmRemoval(selectedWorktrees))) {
41
42
  options.stderr('Aborted\n');
42
43
  return 1;
43
44
  }
45
+ if (options.dryRun) {
46
+ if (options.json) {
47
+ const removed = selectedWorktrees.map((w) => ({ branch: w.branch, path: w.path }));
48
+ options.stdout(`${JSON.stringify({ removed, dryRun: true }, null, 2)}\n`);
49
+ }
50
+ else {
51
+ for (const w of selectedWorktrees) {
52
+ const desc = w.branch ? `branch: ${w.branch}` : 'detached';
53
+ options.stdout(`Would remove worktree at ${w.path} (${desc})\n`);
54
+ }
55
+ }
56
+ return 0;
57
+ }
44
58
  const removedPaths = [];
45
59
  const removedWorktrees = [];
46
60
  for (const worktree of selectedWorktrees) {
package/dist/cli.js CHANGED
@@ -59,6 +59,7 @@ function registerCommands(program) {
59
59
  .command('new [branch]')
60
60
  .description('create a new branch or detached linked worktree')
61
61
  .option('--detached', 'create a detached worktree without a branch')
62
+ .option('--dry-run', 'show what would be created without executing any git commands or writing files')
62
63
  .option('--json', 'emit JSON on success or error instead of human-readable output')
63
64
  .action(notImplemented('new'));
64
65
  program
@@ -67,8 +68,9 @@ function registerCommands(program) {
67
68
  .option('--write', 'write the integration to the shell config file')
68
69
  .action(notImplemented('init'));
69
70
  program
70
- .command('pr <number>')
71
- .description('fetch a pull request ref and create a linked worktree')
71
+ .command('pr <ref>')
72
+ .description('fetch a pull request by number, #number, or URL into a linked worktree')
73
+ .option('--dry-run', 'show what would be created without executing any git commands or writing files')
72
74
  .option('--json', 'emit JSON on success or error instead of human-readable output')
73
75
  .action(notImplemented('pr'));
74
76
  program
@@ -90,6 +92,7 @@ function registerCommands(program) {
90
92
  .command('sync')
91
93
  .description('fetch and update one or all worktrees')
92
94
  .option('--all', 'sync every worktree in the repository')
95
+ .option('--json', 'emit JSON on success or error instead of human-readable output')
93
96
  .action(notImplemented('sync'));
94
97
  program
95
98
  .command('ls')
@@ -100,6 +103,7 @@ function registerCommands(program) {
100
103
  .command('clean')
101
104
  .description('interactively prune linked worktrees')
102
105
  .option('-f, --force', 'bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches')
106
+ .option('--dry-run', 'show what would be deleted without removing anything')
103
107
  .option('--json', 'emit JSON on success or error instead of human-readable output')
104
108
  .action(notImplemented('clean'));
105
109
  program
@@ -107,6 +111,7 @@ function registerCommands(program) {
107
111
  .alias('rm')
108
112
  .description('remove a linked worktree and delete its branch when present')
109
113
  .option('-f, --force', 'bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch')
114
+ .option('--dry-run', 'show what would be deleted without removing anything')
110
115
  .option('--json', 'emit JSON on success or error instead of human-readable output')
111
116
  .action(notImplemented('remove'));
112
117
  const configCommand = program
@@ -130,7 +135,7 @@ function attachCommandActions(program, options) {
130
135
  program.commands
131
136
  .find((command) => command.name() === 'new')
132
137
  ?.action(async (branch, commandOptions) => {
133
- const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached, json: commandOptions.json });
138
+ const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached, dryRun: commandOptions.dryRun, json: commandOptions.json });
134
139
  if (exitCode !== 0) {
135
140
  throw commanderExit(exitCode);
136
141
  }
@@ -151,7 +156,7 @@ function attachCommandActions(program, options) {
151
156
  program.commands
152
157
  .find((command) => command.name() === 'pr')
153
158
  ?.action(async (number, commandOptions) => {
154
- const exitCode = await runPrCommand({ cwd: options.cwd, json: commandOptions.json, number, stderr: options.stderr, stdout: options.stdout });
159
+ const exitCode = await runPrCommand({ cwd: options.cwd, dryRun: commandOptions.dryRun, json: commandOptions.json, number, stderr: options.stderr, stdout: options.stdout });
155
160
  if (exitCode !== 0) {
156
161
  throw commanderExit(exitCode);
157
162
  }
@@ -200,6 +205,7 @@ function attachCommandActions(program, options) {
200
205
  const exitCode = await runSyncCommand({
201
206
  all: commandOptions.all,
202
207
  cwd: options.cwd,
208
+ json: commandOptions.json,
203
209
  stderr: options.stderr,
204
210
  stdout: options.stdout,
205
211
  });
@@ -224,6 +230,7 @@ function attachCommandActions(program, options) {
224
230
  ?.action(async (commandOptions) => {
225
231
  const exitCode = await runCleanCommand({
226
232
  cwd: options.cwd,
233
+ dryRun: commandOptions.dryRun,
227
234
  force: commandOptions.force,
228
235
  json: commandOptions.json,
229
236
  stderr: options.stderr,
@@ -237,6 +244,7 @@ function attachCommandActions(program, options) {
237
244
  const exitCode = await runRemoveCommand({
238
245
  branch,
239
246
  cwd: options.cwd,
247
+ dryRun: commandOptions.dryRun,
240
248
  force: commandOptions.force,
241
249
  json: commandOptions.json,
242
250
  stderr: options.stderr,
package/dist/go.js CHANGED
@@ -22,9 +22,13 @@ export function createGoCommand(dependencies = {}) {
22
22
  ? worktrees.find((entry) => entry.branch === options.branch)?.path
23
23
  : prompted ?? undefined;
24
24
  if (!resolvedPath) {
25
- options.stderr(options.branch
26
- ? `No worktree found for branch: ${options.branch}\n`
27
- : 'Aborted\n');
25
+ if (options.branch) {
26
+ options.stderr(`No worktree found for branch: ${options.branch}\n`);
27
+ options.stderr(`Hint: Use 'gji ls' to see available worktrees\n`);
28
+ }
29
+ else {
30
+ options.stderr('Aborted\n');
31
+ }
28
32
  return 1;
29
33
  }
30
34
  const chosenWorktree = worktrees.find((w) => w.path === resolvedPath);
package/dist/new.d.ts CHANGED
@@ -5,6 +5,7 @@ export interface NewCommandOptions {
5
5
  branch?: string;
6
6
  cwd: string;
7
7
  detached?: boolean;
8
+ dryRun?: boolean;
8
9
  json?: boolean;
9
10
  stderr: (chunk: string) => void;
10
11
  stdout: (chunk: string) => void;
package/dist/new.js CHANGED
@@ -57,6 +57,7 @@ export function createNewCommand(dependencies = {}) {
57
57
  }
58
58
  else {
59
59
  options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
60
+ options.stderr(`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree\n`);
60
61
  }
61
62
  return 1;
62
63
  }
@@ -68,6 +69,15 @@ export function createNewCommand(dependencies = {}) {
68
69
  options.stderr(`Aborted because target worktree path already exists: ${worktreePath}\n`);
69
70
  return 1;
70
71
  }
72
+ if (options.dryRun) {
73
+ if (options.json) {
74
+ options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}\n`);
75
+ }
76
+ else {
77
+ options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName})\n`);
78
+ }
79
+ return 0;
80
+ }
71
81
  await mkdir(dirname(worktreePath), { recursive: true });
72
82
  const gitArgs = options.detached
73
83
  ? ['worktree', 'add', '--detach', worktreePath]
package/dist/pr.d.ts CHANGED
@@ -3,6 +3,7 @@ import { type InstallPromptDependencies } from './install-prompt.js';
3
3
  export type { PathConflictChoice };
4
4
  export interface PrCommandOptions {
5
5
  cwd: string;
6
+ dryRun?: boolean;
6
7
  json?: boolean;
7
8
  number: string;
8
9
  stderr: (chunk: string) => void;
package/dist/pr.js CHANGED
@@ -50,6 +50,7 @@ export function createPrCommand(dependencies = {}) {
50
50
  }
51
51
  else {
52
52
  options.stderr(`gji pr: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
53
+ options.stderr(`Hint: Use 'gji remove pr/${prNumber}' or 'gji clean' to remove the existing worktree\n`);
53
54
  }
54
55
  return 1;
55
56
  }
@@ -61,6 +62,15 @@ export function createPrCommand(dependencies = {}) {
61
62
  options.stderr(`Aborted because target worktree path already exists: ${worktreePath}\n`);
62
63
  return 1;
63
64
  }
65
+ if (options.dryRun) {
66
+ if (options.json) {
67
+ options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath, dryRun: true }, null, 2)}\n`);
68
+ }
69
+ else {
70
+ options.stdout(`Would create worktree at ${worktreePath} (branch: ${branchName})\n`);
71
+ }
72
+ return 0;
73
+ }
64
74
  try {
65
75
  await execFileAsync('git', ['fetch', 'origin', `refs/pull/${prNumber}/head:${remoteRef}`], { cwd: repository.repoRoot });
66
76
  }
@@ -71,6 +81,7 @@ export function createPrCommand(dependencies = {}) {
71
81
  }
72
82
  else {
73
83
  options.stderr(`${message}\n`);
84
+ options.stderr(`Hint: Verify the remote is reachable: git fetch origin\n`);
74
85
  }
75
86
  return 1;
76
87
  }
package/dist/remove.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { WorktreeEntry } from './repo.js';
2
2
  export interface RemoveCommandOptions {
3
3
  branch?: string;
4
4
  cwd: string;
5
+ dryRun?: boolean;
5
6
  force?: boolean;
6
7
  json?: boolean;
7
8
  stderr: (chunk: string) => void;
package/dist/remove.js CHANGED
@@ -38,7 +38,7 @@ export function createRemoveCommand(dependencies = {}) {
38
38
  emitError(options, `No linked worktree found for branch: ${selection}`);
39
39
  return 1;
40
40
  }
41
- if (!options.force && (options.json || isHeadless())) {
41
+ if (!options.dryRun && !options.force && (options.json || isHeadless())) {
42
42
  const message = '--force is required';
43
43
  if (options.json) {
44
44
  emitError(options, message);
@@ -48,10 +48,20 @@ export function createRemoveCommand(dependencies = {}) {
48
48
  }
49
49
  return 1;
50
50
  }
51
- if (!options.force && !(await confirmRemoval(worktree))) {
51
+ if (!options.dryRun && !options.force && !(await confirmRemoval(worktree))) {
52
52
  options.stderr('Aborted\n');
53
53
  return 1;
54
54
  }
55
+ if (options.dryRun) {
56
+ if (options.json) {
57
+ options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}\n`);
58
+ }
59
+ else {
60
+ const desc = worktree.branch ? `branch: ${worktree.branch}` : 'detached';
61
+ options.stdout(`Would remove worktree at ${worktree.path} (${desc})\n`);
62
+ }
63
+ return 0;
64
+ }
55
65
  const config = await loadEffectiveConfig(repository.repoRoot);
56
66
  const hooks = extractHooks(config);
57
67
  await runHook(hooks.beforeRemove, worktree.path, { branch: worktree.branch ?? undefined, path: worktree.path, repo: basename(repository.repoRoot) }, options.stderr);
package/dist/sync.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export interface SyncCommandOptions {
2
2
  all?: boolean;
3
3
  cwd: string;
4
+ json?: boolean;
4
5
  stderr: (chunk: string) => void;
5
6
  stdout: (chunk: string) => void;
6
7
  }
package/dist/sync.js CHANGED
@@ -7,24 +7,47 @@ export async function runSyncCommand(options) {
7
7
  const config = await loadEffectiveConfig(repository.repoRoot);
8
8
  const worktrees = await listWorktrees(options.cwd);
9
9
  const remote = resolveConfiguredString(config.syncRemote) ?? 'origin';
10
- const defaultBranch = resolveConfiguredString(config.syncDefaultBranch)
11
- ?? await resolveDefaultBranch(repository.repoRoot, remote);
10
+ let defaultBranch;
11
+ try {
12
+ defaultBranch = resolveConfiguredString(config.syncDefaultBranch)
13
+ ?? await resolveDefaultBranch(repository.repoRoot, remote);
14
+ }
15
+ catch {
16
+ emitError(options, `Unable to reach remote '${remote}'`);
17
+ if (!options.json) {
18
+ options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>\n`);
19
+ }
20
+ return 1;
21
+ }
12
22
  if (!defaultBranch) {
13
- options.stderr('Unable to determine the default branch for sync.\n');
23
+ emitError(options, 'Unable to determine the default branch for sync.');
24
+ if (!options.json) {
25
+ options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>\n`);
26
+ }
14
27
  return 1;
15
28
  }
16
29
  const targetWorktrees = selectTargetWorktrees(worktrees, repository.currentRoot, options.all);
17
30
  if (targetWorktrees === 'detached') {
18
- options.stderr(`Cannot sync detached worktree: ${repository.currentRoot}\n`);
31
+ emitError(options, `Cannot sync detached worktree: ${repository.currentRoot}`);
19
32
  return 1;
20
33
  }
21
34
  for (const worktree of targetWorktrees) {
22
35
  if (await isDirtyWorktree(worktree.path)) {
23
- options.stderr(`Cannot sync dirty worktree: ${worktree.path}\n`);
36
+ emitError(options, `Cannot sync dirty worktree: ${worktree.path}`);
24
37
  return 1;
25
38
  }
26
39
  }
27
- await runGit(repository.repoRoot, ['fetch', '--prune', remote]);
40
+ try {
41
+ await runGit(repository.repoRoot, ['fetch', '--prune', remote]);
42
+ }
43
+ catch {
44
+ emitError(options, `Failed to fetch from remote '${remote}'`);
45
+ if (!options.json) {
46
+ options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>\n`);
47
+ }
48
+ return 1;
49
+ }
50
+ const updatedWorktrees = [];
28
51
  for (const worktree of targetWorktrees) {
29
52
  if (worktree.branch === defaultBranch) {
30
53
  await runGit(worktree.path, ['merge', '--ff-only', `${remote}/${defaultBranch}`]);
@@ -32,10 +55,25 @@ export async function runSyncCommand(options) {
32
55
  else {
33
56
  await runGit(worktree.path, ['rebase', `${remote}/${defaultBranch}`]);
34
57
  }
35
- options.stdout(`${worktree.path}\n`);
58
+ updatedWorktrees.push(worktree);
59
+ if (!options.json) {
60
+ options.stdout(`${worktree.path}\n`);
61
+ }
62
+ }
63
+ if (options.json) {
64
+ const updated = updatedWorktrees.map((w) => ({ branch: w.branch, path: w.path }));
65
+ options.stdout(`${JSON.stringify({ updated }, null, 2)}\n`);
36
66
  }
37
67
  return 0;
38
68
  }
69
+ function emitError(options, message) {
70
+ if (options.json) {
71
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
72
+ }
73
+ else {
74
+ options.stderr(`${message}\n`);
75
+ }
76
+ }
39
77
  function selectTargetWorktrees(worktrees, currentRoot, all) {
40
78
  if (all) {
41
79
  return worktrees
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solaqua/gji",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Git worktree CLI for fast context switching.",
5
5
  "license": "MIT",
6
6
  "author": "sjquant",