@solaqua/gji 0.6.2 → 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 +1624 -819
  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 +14 -14
  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 +37 -33
  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,43 +1,56 @@
1
- import { KNOWN_GLOBAL_CONFIG_KEYS } from './config.js';
1
+ import { KNOWN_GLOBAL_CONFIG_KEYS } from "./config.js";
2
2
  const TOP_LEVEL_COMMANDS = [
3
- { name: 'new', description: 'create a new branch or detached linked worktree' },
4
- { name: 'init', description: 'print or install shell integration' },
5
- { name: 'completion', description: 'print shell completion definitions' },
6
- { name: 'pr', description: 'fetch a pull request into a linked worktree' },
7
- { name: 'back', description: 'navigate to the previously visited worktree' },
8
- { name: 'history', description: 'show navigation history' },
9
- { name: 'open', description: 'open the worktree in an editor' },
10
- { name: 'go', description: 'print or select a worktree path' },
11
- { name: 'jump', description: 'alias of go' },
12
- { name: 'root', description: 'print the main repository root path' },
13
- { name: 'status', description: 'summarize repository and worktree health' },
14
- { name: 'sync', description: 'fetch and update one or all worktrees' },
15
- { name: 'ls', description: 'list active worktrees' },
16
- { name: 'clean', description: 'interactively prune linked worktrees' },
17
- { name: 'remove', description: 'remove a linked worktree and delete its branch when present' },
18
- { name: 'rm', description: 'alias of remove' },
19
- { name: 'trigger-hook', description: 'run a named hook in the current worktree' },
20
- { name: 'warp', description: 'jump to any worktree across all known repos' },
21
- { name: 'config', description: 'manage global config defaults' },
3
+ {
4
+ name: "new",
5
+ description: "create a new branch or detached linked worktree",
6
+ },
7
+ { name: "init", description: "print or install shell integration" },
8
+ { name: "completion", description: "print shell completion definitions" },
9
+ { name: "pr", description: "fetch a pull request into a linked worktree" },
10
+ { name: "back", description: "navigate to the previously visited worktree" },
11
+ { name: "history", description: "show navigation history" },
12
+ { name: "open", description: "open the worktree in an editor" },
13
+ { name: "go", description: "print or select a worktree path" },
14
+ { name: "jump", description: "alias of go" },
15
+ { name: "root", description: "print the main repository root path" },
16
+ { name: "status", description: "summarize repository and worktree health" },
17
+ { name: "sync", description: "fetch and update one or all worktrees" },
18
+ {
19
+ name: "sync-files",
20
+ description: "manage local files copied into new worktrees",
21
+ },
22
+ { name: "ls", description: "list active worktrees" },
23
+ { name: "clean", description: "interactively prune linked worktrees" },
24
+ {
25
+ name: "remove",
26
+ description: "remove a linked worktree and delete its branch when present",
27
+ },
28
+ { name: "rm", description: "alias of remove" },
29
+ {
30
+ name: "trigger-hook",
31
+ description: "run a named hook in the current worktree",
32
+ },
33
+ { name: "warp", description: "jump to any worktree across all known repos" },
34
+ { name: "config", description: "manage global config defaults" },
22
35
  ];
23
- const SHELL_NAMES = ['bash', 'fish', 'zsh'];
24
- const HOOK_NAMES = ['afterCreate', 'afterEnter', 'beforeRemove'];
36
+ const SHELL_NAMES = ["bash", "fish", "zsh"];
37
+ const HOOK_NAMES = ["afterCreate", "afterEnter", "beforeRemove"];
25
38
  const CONFIG_KEYS = Array.from(KNOWN_GLOBAL_CONFIG_KEYS);
26
39
  export function renderShellCompletion(shell) {
27
40
  switch (shell) {
28
- case 'bash':
41
+ case "bash":
29
42
  return renderBashCompletion();
30
- case 'fish':
43
+ case "fish":
31
44
  return renderFishCompletion();
32
- case 'zsh':
45
+ case "zsh":
33
46
  return renderZshCompletion();
34
47
  }
35
48
  }
36
49
  function renderBashCompletion() {
37
- const topLevelCommands = TOP_LEVEL_COMMANDS.map((command) => command.name).join(' ');
38
- const shells = SHELL_NAMES.join(' ');
39
- const hooks = HOOK_NAMES.join(' ');
40
- const configKeys = CONFIG_KEYS.join(' ');
50
+ const topLevelCommands = TOP_LEVEL_COMMANDS.map((command) => command.name).join(" ");
51
+ const shells = SHELL_NAMES.join(" ");
52
+ const hooks = HOOK_NAMES.join(" ");
53
+ const configKeys = CONFIG_KEYS.join(" ");
41
54
  return `__gji_worktree_branches() {
42
55
  command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
43
56
  }
@@ -88,6 +101,12 @@ _gji_completion() {
88
101
  sync)
89
102
  COMPREPLY=( $(compgen -W "--all --json --help" -- "$cur") )
90
103
  ;;
104
+ sync-files)
105
+ if [ "$COMP_CWORD" -eq 2 ]; then
106
+ COMPREPLY=( $(compgen -W "list add remove rm --json --help" -- "$cur") )
107
+ return 0
108
+ fi
109
+ ;;
91
110
  ls)
92
111
  COMPREPLY=( $(compgen -W "--compact --json --help" -- "$cur") )
93
112
  ;;
@@ -128,10 +147,10 @@ _gji_completion() {
128
147
  complete -F _gji_completion gji`;
129
148
  }
130
149
  function renderFishCompletion() {
131
- const commandLines = TOP_LEVEL_COMMANDS.map((command) => `complete -c gji -n '__fish_use_subcommand' -a '${command.name}' -d '${escapeSingleQuotes(command.description)}'`).join('\n');
132
- const shellLines = SHELL_NAMES.map((shell) => `complete -c gji -n '__fish_seen_subcommand_from init' -a '${shell}' -d 'shell'`).join('\n');
133
- const hookLines = HOOK_NAMES.map((hook) => `complete -c gji -n '__fish_seen_subcommand_from trigger-hook' -a '${hook}' -d 'hook'`).join('\n');
134
- const configKeyLines = CONFIG_KEYS.map((key) => `complete -c gji -n '__gji_should_complete_config_key' -a '${key}' -d 'config key'`).join('\n');
150
+ const commandLines = TOP_LEVEL_COMMANDS.map((command) => `complete -c gji -n '__fish_use_subcommand' -a '${command.name}' -d '${escapeSingleQuotes(command.description)}'`).join("\n");
151
+ const shellLines = SHELL_NAMES.map((shell) => `complete -c gji -n '__fish_seen_subcommand_from init' -a '${shell}' -d 'shell'`).join("\n");
152
+ const hookLines = HOOK_NAMES.map((hook) => `complete -c gji -n '__fish_seen_subcommand_from trigger-hook' -a '${hook}' -d 'hook'`).join("\n");
153
+ const configKeyLines = CONFIG_KEYS.map((key) => `complete -c gji -n '__gji_should_complete_config_key' -a '${key}' -d 'config key'`).join("\n");
135
154
  return `function __gji_worktree_branches
136
155
  command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
137
156
  end
@@ -193,6 +212,12 @@ complete -c gji -n '__fish_seen_subcommand_from status' -l json -d 'print reposi
193
212
  complete -c gji -n '__fish_seen_subcommand_from sync' -l all -d 'sync every worktree in the repository'
194
213
  complete -c gji -n '__fish_seen_subcommand_from sync' -l json -d 'emit JSON on success or error instead of human-readable output'
195
214
 
215
+ complete -c gji -n '__fish_seen_subcommand_from sync-files' -a 'list add remove rm' -d 'sync-files action'
216
+ complete -c gji -n '__fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
217
+ complete -c gji -n '__fish_seen_subcommand_from list; and __fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
218
+ complete -c gji -n '__fish_seen_subcommand_from add; and __fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
219
+ complete -c gji -n '__fish_seen_subcommand_from remove rm; and __fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
220
+
196
221
  complete -c gji -n '__fish_seen_subcommand_from ls' -l compact -d 'show only branch and path columns'
197
222
  complete -c gji -n '__fish_seen_subcommand_from ls' -l json -d 'print active worktrees as JSON'
198
223
 
@@ -216,10 +241,10 @@ complete -c gji -n '__fish_seen_subcommand_from config; and __gji_should_complet
216
241
  ${configKeyLines}`;
217
242
  }
218
243
  function renderZshCompletion() {
219
- const commandLines = TOP_LEVEL_COMMANDS.map((command) => `'${command.name}:${escapeSingleQuotes(command.description)}'`).join('\n ');
220
- const configKeys = CONFIG_KEYS.join(' ');
221
- const shells = SHELL_NAMES.join(' ');
222
- const hooks = HOOK_NAMES.join(' ');
244
+ const commandLines = TOP_LEVEL_COMMANDS.map((command) => `'${command.name}:${escapeSingleQuotes(command.description)}'`).join("\n ");
245
+ const configKeys = CONFIG_KEYS.join(" ");
246
+ const shells = SHELL_NAMES.join(" ");
247
+ const hooks = HOOK_NAMES.join(" ");
223
248
  return `#compdef gji
224
249
 
225
250
  __gji_worktree_branches() {
@@ -272,6 +297,9 @@ case "\${words[2]}" in
272
297
  sync)
273
298
  _arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
274
299
  ;;
300
+ sync-files)
301
+ _arguments '--json[emit JSON instead of human-readable output]' '2:action:(list add remove rm)' '*:path: '
302
+ ;;
275
303
  ls)
276
304
  _arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
277
305
  ;;
@@ -1,8 +1,8 @@
1
- import { writeFile } from 'node:fs/promises';
1
+ import { writeFile } from "node:fs/promises";
2
2
  export async function writeShellOutput(envVar, value, stdout) {
3
3
  const output = `${value}\n`;
4
4
  if (process.env[envVar]) {
5
- await writeFile(process.env[envVar], output, 'utf8');
5
+ await writeFile(process.env[envVar], output, "utf8");
6
6
  return;
7
7
  }
8
8
  stdout(output);
package/dist/shell.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type SupportedShell = 'bash' | 'fish' | 'zsh';
1
+ export type SupportedShell = "bash" | "fish" | "zsh";
2
2
  export declare function resolveSupportedShell(requestedShell: string | undefined, detectedShell: string | undefined): SupportedShell | null;
package/dist/shell.js CHANGED
@@ -9,11 +9,11 @@ function normalizeShell(value) {
9
9
  if (!value) {
10
10
  return null;
11
11
  }
12
- const candidate = value.split('/').at(-1)?.toLowerCase();
12
+ const candidate = value.split("/").at(-1)?.toLowerCase();
13
13
  switch (candidate) {
14
- case 'bash':
15
- case 'fish':
16
- case 'zsh':
14
+ case "bash":
15
+ case "fish":
16
+ case "zsh":
17
17
  return candidate;
18
18
  default:
19
19
  return null;
package/dist/status.d.ts CHANGED
@@ -7,17 +7,17 @@ interface WorktreeStatusRow {
7
7
  branch: string | null;
8
8
  current: boolean;
9
9
  path: string;
10
- status: 'clean' | 'dirty';
10
+ status: "clean" | "dirty";
11
11
  upstream: UpstreamState;
12
12
  }
13
13
  type UpstreamState = {
14
- kind: 'detached';
14
+ kind: "detached";
15
15
  } | {
16
- kind: 'no-upstream';
16
+ kind: "no-upstream";
17
17
  } | {
18
- kind: 'stale';
18
+ kind: "stale";
19
19
  } | {
20
- kind: 'tracked';
20
+ kind: "tracked";
21
21
  ahead: number;
22
22
  behind: number;
23
23
  };
package/dist/status.js CHANGED
@@ -1,6 +1,6 @@
1
- import { detectRepository, listWorktrees } from './repo.js';
2
- import { readWorktreeHealth } from './git.js';
3
- import { comparePaths } from './paths.js';
1
+ import { readWorktreeHealth } from "./git.js";
2
+ import { comparePaths } from "./paths.js";
3
+ import { detectRepository, listWorktrees } from "./repo.js";
4
4
  export async function runStatusCommand(options) {
5
5
  const repository = await detectRepository(options.cwd);
6
6
  const worktrees = sortWorktreesByPath(await listWorktrees(options.cwd));
@@ -13,20 +13,20 @@ export async function runStatusCommand(options) {
13
13
  return 0;
14
14
  }
15
15
  export function formatStatusOutput(repoRoot, currentRoot, rows) {
16
- const currentWidth = Math.max('CURRENT'.length, ...rows.map((row) => row.current ? 1 : 0));
17
- const branchWidth = Math.max('BRANCH'.length, ...rows.map((row) => formatBranch(row.branch).length));
18
- const statusWidth = Math.max('STATUS'.length, ...rows.map((row) => row.status.length));
19
- const upstreamWidth = Math.max('UPSTREAM'.length, ...rows.map((row) => formatUpstreamState(row.upstream).length));
16
+ const currentWidth = Math.max("CURRENT".length, ...rows.map((row) => (row.current ? 1 : 0)));
17
+ const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => formatBranch(row.branch).length));
18
+ const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
19
+ const upstreamWidth = Math.max("UPSTREAM".length, ...rows.map((row) => formatUpstreamState(row.upstream).length));
20
20
  const lines = [
21
21
  `REPO ${repoRoot}`,
22
22
  `CURRENT ${currentRoot}`,
23
- '',
24
- `${'CURRENT'.padEnd(currentWidth, ' ')} ${'BRANCH'.padEnd(branchWidth, ' ')} ${'STATUS'.padEnd(statusWidth, ' ')} ${'UPSTREAM'.padEnd(upstreamWidth, ' ')} PATH`,
23
+ "",
24
+ `${"CURRENT".padEnd(currentWidth, " ")} ${"BRANCH".padEnd(branchWidth, " ")} ${"STATUS".padEnd(statusWidth, " ")} ${"UPSTREAM".padEnd(upstreamWidth, " ")} PATH`,
25
25
  ];
26
26
  for (const row of rows) {
27
- lines.push(`${(row.current ? '*' : '').padEnd(currentWidth, ' ')} ${formatBranch(row.branch).padEnd(branchWidth, ' ')} ${row.status.padEnd(statusWidth, ' ')} ${formatUpstreamState(row.upstream).padEnd(upstreamWidth, ' ')} ${row.path}`);
27
+ lines.push(`${(row.current ? "*" : "").padEnd(currentWidth, " ")} ${formatBranch(row.branch).padEnd(branchWidth, " ")} ${row.status.padEnd(statusWidth, " ")} ${formatUpstreamState(row.upstream).padEnd(upstreamWidth, " ")} ${row.path}`);
28
28
  }
29
- return lines.join('\n');
29
+ return lines.join("\n");
30
30
  }
31
31
  export function formatStatusJson(repoRoot, currentRoot, rows) {
32
32
  return {
@@ -49,36 +49,36 @@ function sortWorktreesByPath(worktrees) {
49
49
  return [...worktrees].sort((left, right) => comparePaths(left.path, right.path));
50
50
  }
51
51
  function formatBranch(branch) {
52
- return branch ?? '(detached)';
52
+ return branch ?? "(detached)";
53
53
  }
54
54
  function buildUpstreamState(branch, health) {
55
55
  if (branch === null) {
56
- return { kind: 'detached' };
56
+ return { kind: "detached" };
57
57
  }
58
58
  if (!health.hasUpstream) {
59
- return { kind: 'no-upstream' };
59
+ return { kind: "no-upstream" };
60
60
  }
61
61
  if (health.upstreamGone) {
62
- return { kind: 'stale' };
62
+ return { kind: "stale" };
63
63
  }
64
64
  return {
65
65
  ahead: health.ahead,
66
66
  behind: health.behind,
67
- kind: 'tracked',
67
+ kind: "tracked",
68
68
  };
69
69
  }
70
70
  function formatUpstreamState(upstream) {
71
- if (upstream.kind === 'detached') {
72
- return 'n/a';
71
+ if (upstream.kind === "detached") {
72
+ return "n/a";
73
73
  }
74
- if (upstream.kind === 'no-upstream') {
75
- return 'no-upstream';
74
+ if (upstream.kind === "no-upstream") {
75
+ return "no-upstream";
76
76
  }
77
- if (upstream.kind === 'stale') {
78
- return 'gone';
77
+ if (upstream.kind === "stale") {
78
+ return "gone";
79
79
  }
80
80
  if (upstream.ahead === 0 && upstream.behind === 0) {
81
- return 'up to date';
81
+ return "up to date";
82
82
  }
83
83
  if (upstream.ahead === 0) {
84
84
  return `behind ${upstream.behind}`;
@@ -0,0 +1,10 @@
1
+ export interface SyncFilesCommandOptions {
2
+ action?: string;
3
+ cwd: string;
4
+ home?: string;
5
+ json?: boolean;
6
+ paths?: string[];
7
+ stderr: (chunk: string) => void;
8
+ stdout: (chunk: string) => void;
9
+ }
10
+ export declare function runSyncFilesCommand(options: SyncFilesCommandOptions): Promise<number>;
@@ -0,0 +1,137 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { loadGlobalConfig, saveGlobalConfig, } from "./config.js";
4
+ import { validateSyncFilePattern } from "./file-sync.js";
5
+ import { detectRepository } from "./repo.js";
6
+ export async function runSyncFilesCommand(options) {
7
+ const repository = await detectRepository(options.cwd);
8
+ const home = options.home ?? homedir();
9
+ const loaded = await loadGlobalConfig(home);
10
+ const repoEntry = findRepoConfigEntry(loaded.config, repository.repoRoot, home);
11
+ const repoConfig = repoEntry?.config ?? {};
12
+ switch (options.action) {
13
+ case undefined:
14
+ case "list": {
15
+ writeSyncFiles(options.stdout, readSyncFiles(repoConfig), !!options.json);
16
+ return 0;
17
+ }
18
+ case "add": {
19
+ const paths = validatePaths(options.paths ?? [], options);
20
+ if (!paths)
21
+ return 1;
22
+ const nextFiles = mergeSyncFiles(readSyncFiles(repoConfig), paths);
23
+ await saveRepoSyncFiles(loaded.config, repoEntry?.key ?? repository.repoRoot, nextFiles, home);
24
+ writeSyncFiles(options.stdout, nextFiles, !!options.json);
25
+ return 0;
26
+ }
27
+ case "remove": {
28
+ const paths = validatePaths(options.paths ?? [], options);
29
+ if (!paths)
30
+ return 1;
31
+ const existingFiles = readSyncFiles(repoConfig);
32
+ const nextFiles = removeSyncFiles(existingFiles, paths);
33
+ if (repoEntry && nextFiles.length !== existingFiles.length) {
34
+ await saveRepoSyncFiles(loaded.config, repoEntry.key, nextFiles, home);
35
+ }
36
+ writeSyncFiles(options.stdout, nextFiles, !!options.json);
37
+ return 0;
38
+ }
39
+ }
40
+ writeError(options, `unknown action: ${options.action}`);
41
+ return 1;
42
+ }
43
+ function findRepoConfigEntry(config, repoRoot, home) {
44
+ const repos = config.repos;
45
+ if (!isPlainObject(repos))
46
+ return null;
47
+ for (const [key, value] of Object.entries(repos)) {
48
+ if (expandTilde(key, home) === repoRoot && isPlainObject(value)) {
49
+ return { config: value, key };
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+ function readSyncFiles(config) {
55
+ const syncFiles = config.syncFiles;
56
+ if (!Array.isArray(syncFiles))
57
+ return [];
58
+ return syncFiles.filter((item) => typeof item === "string");
59
+ }
60
+ function writeSyncFiles(stdout, files, json) {
61
+ if (json) {
62
+ stdout(`${JSON.stringify(files, null, 2)}\n`);
63
+ return;
64
+ }
65
+ if (files.length === 0) {
66
+ stdout("No sync files configured for this repo.\n");
67
+ return;
68
+ }
69
+ stdout(`${files.join("\n")}\n`);
70
+ }
71
+ function validatePaths(paths, options) {
72
+ if (paths.length === 0) {
73
+ writeError(options, "at least one path is required");
74
+ return null;
75
+ }
76
+ const validatedPaths = [];
77
+ for (const path of paths) {
78
+ try {
79
+ validatedPaths.push(validateSyncFilePattern(path));
80
+ }
81
+ catch (error) {
82
+ writeError(options, error instanceof Error ? error.message : String(error));
83
+ return null;
84
+ }
85
+ }
86
+ return validatedPaths;
87
+ }
88
+ function mergeSyncFiles(existing, additions) {
89
+ const nextFiles = [...existing];
90
+ for (const path of additions) {
91
+ if (!nextFiles.includes(path)) {
92
+ nextFiles.push(path);
93
+ }
94
+ }
95
+ return nextFiles;
96
+ }
97
+ function removeSyncFiles(existing, removals) {
98
+ const removalSet = new Set(removals);
99
+ return existing.filter((path) => !removalSet.has(path));
100
+ }
101
+ async function saveRepoSyncFiles(config, repoKey, syncFiles, home) {
102
+ const repos = isPlainObject(config.repos) ? { ...config.repos } : {};
103
+ const repoConfig = isPlainObject(repos[repoKey])
104
+ ? repos[repoKey]
105
+ : {};
106
+ const nextRepoConfig = { ...repoConfig };
107
+ if (syncFiles.length > 0) {
108
+ nextRepoConfig.syncFiles = syncFiles;
109
+ }
110
+ else {
111
+ delete nextRepoConfig.syncFiles;
112
+ }
113
+ if (Object.keys(nextRepoConfig).length > 0) {
114
+ repos[repoKey] = nextRepoConfig;
115
+ }
116
+ else {
117
+ delete repos[repoKey];
118
+ }
119
+ await saveGlobalConfig({ ...config, repos }, home);
120
+ }
121
+ function isPlainObject(value) {
122
+ return typeof value === "object" && value !== null && !Array.isArray(value);
123
+ }
124
+ function writeError(options, message) {
125
+ if (options.json) {
126
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
127
+ return;
128
+ }
129
+ options.stderr(`gji sync-files: ${message}\n`);
130
+ }
131
+ function expandTilde(value, home) {
132
+ if (value === "~")
133
+ return home;
134
+ if (value.startsWith("~/"))
135
+ return join(home, value.slice(2));
136
+ return value;
137
+ }
package/dist/sync.js CHANGED
@@ -1,16 +1,17 @@
1
- import { loadEffectiveConfig } from './config.js';
2
- import { isDirtyWorktree, resolveRemoteDefaultBranch, runGit } from './git.js';
3
- import { comparePaths } from './paths.js';
4
- import { detectRepository, listWorktrees } from './repo.js';
1
+ import { loadEffectiveConfig } from "./config.js";
2
+ import { isDirtyWorktree, resolveRemoteDefaultBranch, runGit } from "./git.js";
3
+ import { comparePaths } from "./paths.js";
4
+ import { detectRepository, listWorktrees } from "./repo.js";
5
5
  export async function runSyncCommand(options) {
6
6
  const repository = await detectRepository(options.cwd);
7
7
  const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
8
8
  const worktrees = await listWorktrees(options.cwd);
9
- const remote = resolveConfiguredString(config.syncRemote) ?? 'origin';
9
+ const remote = resolveConfiguredString(config.syncRemote) ?? "origin";
10
10
  let defaultBranch;
11
11
  try {
12
- defaultBranch = resolveConfiguredString(config.syncDefaultBranch)
13
- ?? await resolveRemoteDefaultBranch(repository.repoRoot, remote);
12
+ defaultBranch =
13
+ resolveConfiguredString(config.syncDefaultBranch) ??
14
+ (await resolveRemoteDefaultBranch(repository.repoRoot, remote));
14
15
  }
15
16
  catch {
16
17
  emitError(options, `Unable to reach remote '${remote}'`);
@@ -20,14 +21,14 @@ export async function runSyncCommand(options) {
20
21
  return 1;
21
22
  }
22
23
  if (!defaultBranch) {
23
- emitError(options, 'Unable to determine the default branch for sync.');
24
+ emitError(options, "Unable to determine the default branch for sync.");
24
25
  if (!options.json) {
25
26
  options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>\n`);
26
27
  }
27
28
  return 1;
28
29
  }
29
30
  const targetWorktrees = selectTargetWorktrees(worktrees, repository.currentRoot, options.all);
30
- if (targetWorktrees === 'detached') {
31
+ if (targetWorktrees === "detached") {
31
32
  emitError(options, `Cannot sync detached worktree: ${repository.currentRoot}`);
32
33
  return 1;
33
34
  }
@@ -38,7 +39,7 @@ export async function runSyncCommand(options) {
38
39
  }
39
40
  }
40
41
  try {
41
- await runGit(repository.repoRoot, ['fetch', '--prune', remote]);
42
+ await runGit(repository.repoRoot, ["fetch", "--prune", remote]);
42
43
  }
43
44
  catch {
44
45
  emitError(options, `Failed to fetch from remote '${remote}'`);
@@ -50,10 +51,14 @@ export async function runSyncCommand(options) {
50
51
  const updatedWorktrees = [];
51
52
  for (const worktree of targetWorktrees) {
52
53
  if (worktree.branch === defaultBranch) {
53
- await runGit(worktree.path, ['merge', '--ff-only', `${remote}/${defaultBranch}`]);
54
+ await runGit(worktree.path, [
55
+ "merge",
56
+ "--ff-only",
57
+ `${remote}/${defaultBranch}`,
58
+ ]);
54
59
  }
55
60
  else {
56
- await runGit(worktree.path, ['rebase', `${remote}/${defaultBranch}`]);
61
+ await runGit(worktree.path, ["rebase", `${remote}/${defaultBranch}`]);
57
62
  }
58
63
  updatedWorktrees.push(worktree);
59
64
  if (!options.json) {
@@ -61,7 +66,10 @@ export async function runSyncCommand(options) {
61
66
  }
62
67
  }
63
68
  if (options.json) {
64
- const updated = updatedWorktrees.map((w) => ({ branch: w.branch, path: w.path }));
69
+ const updated = updatedWorktrees.map((w) => ({
70
+ branch: w.branch,
71
+ path: w.path,
72
+ }));
65
73
  options.stdout(`${JSON.stringify({ updated }, null, 2)}\n`);
66
74
  }
67
75
  return 0;
@@ -85,10 +93,10 @@ function selectTargetWorktrees(worktrees, currentRoot, all) {
85
93
  return [];
86
94
  }
87
95
  if (!currentWorktree.branch) {
88
- return 'detached';
96
+ return "detached";
89
97
  }
90
98
  return [currentWorktree];
91
99
  }
92
100
  function resolveConfiguredString(value) {
93
- return typeof value === 'string' && value.length > 0 ? value : null;
101
+ return typeof value === "string" && value.length > 0 ? value : null;
94
102
  }
@@ -1,13 +1,17 @@
1
- import { loadEffectiveConfig } from './config.js';
2
- import { extractHooks, runHook } from './hooks.js';
3
- import { detectRepository, listWorktrees } from './repo.js';
4
- const VALID_HOOKS = ['afterCreate', 'afterEnter', 'beforeRemove'];
1
+ import { loadEffectiveConfig } from "./config.js";
2
+ import { extractHooks, runHook } from "./hooks.js";
3
+ import { detectRepository, listWorktrees } from "./repo.js";
4
+ const VALID_HOOKS = [
5
+ "afterCreate",
6
+ "afterEnter",
7
+ "beforeRemove",
8
+ ];
5
9
  function isValidHook(hook) {
6
10
  return VALID_HOOKS.includes(hook);
7
11
  }
8
12
  export async function runTriggerHookCommand(options) {
9
13
  if (!isValidHook(options.hook)) {
10
- options.stderr(`gji trigger-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(', ')}\n`);
14
+ options.stderr(`gji trigger-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(", ")}\n`);
11
15
  return 1;
12
16
  }
13
17
  const hookName = options.hook;