@solaqua/gji 0.1.0 → 0.2.1

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/remove.js CHANGED
@@ -2,6 +2,7 @@ import { basename } from 'node:path';
2
2
  import { confirm, isCancel, select } from '@clack/prompts';
3
3
  import { loadEffectiveConfig } from './config.js';
4
4
  import { extractHooks, runHook } from './hooks.js';
5
+ import { isHeadless } from './headless.js';
5
6
  import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeDirtyError, loadLinkedWorktrees, removeWorktree, } from './worktree-management.js';
6
7
  import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree } from './worktree-prompts.js';
7
8
  import { writeShellOutput } from './shell-handoff.js';
@@ -14,7 +15,17 @@ export function createRemoveCommand(dependencies = {}) {
14
15
  return async function runRemoveCommand(options) {
15
16
  const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
16
17
  if (linkedWorktrees.length === 0) {
17
- options.stderr('No linked worktrees to finish\n');
18
+ emitError(options, 'No linked worktrees to finish');
19
+ return 1;
20
+ }
21
+ if (!options.branch && (options.json || isHeadless())) {
22
+ const message = 'branch argument is required';
23
+ if (options.json) {
24
+ emitError(options, message);
25
+ }
26
+ else {
27
+ options.stderr(`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
28
+ }
18
29
  return 1;
19
30
  }
20
31
  const selection = options.branch ?? (await promptForWorktree(linkedWorktrees));
@@ -24,13 +35,33 @@ export function createRemoveCommand(dependencies = {}) {
24
35
  }
25
36
  const worktree = linkedWorktrees.find((entry) => entry.branch === selection || entry.path === selection);
26
37
  if (!worktree) {
27
- options.stderr(`No linked worktree found for branch: ${selection}\n`);
38
+ emitError(options, `No linked worktree found for branch: ${selection}`);
39
+ return 1;
40
+ }
41
+ if (!options.dryRun && !options.force && (options.json || isHeadless())) {
42
+ const message = '--force is required';
43
+ if (options.json) {
44
+ emitError(options, message);
45
+ }
46
+ else {
47
+ options.stderr(`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
48
+ }
28
49
  return 1;
29
50
  }
30
- if (!options.force && !(await confirmRemoval(worktree))) {
51
+ if (!options.dryRun && !options.force && !(await confirmRemoval(worktree))) {
31
52
  options.stderr('Aborted\n');
32
53
  return 1;
33
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
+ }
34
65
  const config = await loadEffectiveConfig(repository.repoRoot);
35
66
  const hooks = extractHooks(config);
36
67
  await runHook(hooks.beforeRemove, worktree.path, { branch: worktree.branch ?? undefined, path: worktree.path, repo: basename(repository.repoRoot) }, options.stderr);
@@ -49,7 +80,7 @@ export function createRemoveCommand(dependencies = {}) {
49
80
  await forceRemoveWorktree(repository.repoRoot, worktree.path);
50
81
  }
51
82
  catch (forceError) {
52
- options.stderr(`Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}\n`);
83
+ emitError(options, `Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}`);
53
84
  return 1;
54
85
  }
55
86
  }
@@ -74,7 +105,12 @@ export function createRemoveCommand(dependencies = {}) {
74
105
  }
75
106
  }
76
107
  }
77
- await writeOutput(repository.repoRoot, options.stdout);
108
+ if (options.json) {
109
+ options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}\n`);
110
+ }
111
+ else {
112
+ await writeOutput(repository.repoRoot, options.stdout);
113
+ }
78
114
  return 0;
79
115
  };
80
116
  }
@@ -104,6 +140,14 @@ async function defaultConfirmRemoval(worktree) {
104
140
  async function writeOutput(repoRoot, stdout) {
105
141
  await writeShellOutput(REMOVE_OUTPUT_FILE_ENV, repoRoot, stdout);
106
142
  }
143
+ function emitError(options, message) {
144
+ if (options.json) {
145
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
146
+ }
147
+ else {
148
+ options.stderr(`${message}\n`);
149
+ }
150
+ }
107
151
  function toMessage(error) {
108
152
  return error instanceof Error ? error.message : String(error);
109
153
  }
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.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Git worktree CLI for fast context switching.",
5
5
  "license": "MIT",
6
6
  "author": "sjquant",