@solaqua/gji 0.6.2 → 0.7.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.
Files changed (84) 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 +266 -165
  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 +1688 -865
  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.d.ts +3 -3
  25. package/dist/hooks.js +19 -19
  26. package/dist/index.js +13 -9
  27. package/dist/init.d.ts +5 -5
  28. package/dist/init.js +115 -103
  29. package/dist/install-prompt.d.ts +3 -3
  30. package/dist/install-prompt.js +57 -38
  31. package/dist/ls.d.ts +2 -2
  32. package/dist/ls.js +29 -29
  33. package/dist/new.d.ts +2 -2
  34. package/dist/new.js +97 -82
  35. package/dist/open.d.ts +2 -2
  36. package/dist/open.js +24 -21
  37. package/dist/package-manager.js +96 -45
  38. package/dist/pr.d.ts +2 -2
  39. package/dist/pr.js +47 -34
  40. package/dist/remove.d.ts +1 -1
  41. package/dist/remove.js +39 -27
  42. package/dist/repo-registry.js +14 -14
  43. package/dist/repo.js +29 -28
  44. package/dist/root.js +3 -3
  45. package/dist/run-hook.d.ts +6 -0
  46. package/dist/{trigger-hook.js → run-hook.js} +18 -8
  47. package/dist/shell-completion.d.ts +1 -1
  48. package/dist/shell-completion.js +67 -39
  49. package/dist/shell-handoff.js +2 -2
  50. package/dist/shell.d.ts +1 -1
  51. package/dist/shell.js +4 -4
  52. package/dist/status.d.ts +5 -5
  53. package/dist/status.js +23 -23
  54. package/dist/sync-files-command.d.ts +10 -0
  55. package/dist/sync-files-command.js +137 -0
  56. package/dist/sync.js +23 -15
  57. package/dist/warp.js +37 -33
  58. package/dist/worktree-info.d.ts +9 -9
  59. package/dist/worktree-info.js +31 -29
  60. package/dist/worktree-management.d.ts +1 -1
  61. package/dist/worktree-management.js +26 -11
  62. package/dist/worktree-prompts.js +5 -5
  63. package/man/man1/gji-back.1 +1 -1
  64. package/man/man1/gji-clean.1 +1 -1
  65. package/man/man1/gji-completion.1 +1 -1
  66. package/man/man1/gji-config.1 +1 -1
  67. package/man/man1/gji-go.1 +1 -1
  68. package/man/man1/gji-history.1 +1 -1
  69. package/man/man1/gji-init.1 +1 -1
  70. package/man/man1/gji-ls.1 +1 -1
  71. package/man/man1/gji-new.1 +1 -1
  72. package/man/man1/gji-open.1 +1 -1
  73. package/man/man1/gji-pr.1 +1 -1
  74. package/man/man1/gji-remove.1 +1 -1
  75. package/man/man1/gji-root.1 +1 -1
  76. package/man/man1/gji-run-hook.1 +9 -0
  77. package/man/man1/gji-status.1 +1 -1
  78. package/man/man1/gji-sync-files.1 +23 -0
  79. package/man/man1/gji-sync.1 +1 -1
  80. package/man/man1/gji-warp.1 +1 -1
  81. package/man/man1/gji.1 +10 -4
  82. package/package.json +8 -2
  83. package/dist/trigger-hook.d.ts +0 -6
  84. package/man/man1/gji-trigger-hook.1 +0 -9
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]) {
@@ -0,0 +1,6 @@
1
+ export interface RunHookCommandOptions {
2
+ cwd: string;
3
+ hook: string;
4
+ stderr: (chunk: string) => void;
5
+ }
6
+ export declare function runHookCommand(options: RunHookCommandOptions): Promise<number>;
@@ -1,16 +1,26 @@
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
+ "after-create",
6
+ "after-enter",
7
+ "before-remove",
8
+ ];
9
+ const CAMEL_ALIASES = {
10
+ afterCreate: "after-create",
11
+ afterEnter: "after-enter",
12
+ beforeRemove: "before-remove",
13
+ };
5
14
  function isValidHook(hook) {
6
15
  return VALID_HOOKS.includes(hook);
7
16
  }
8
- export async function runTriggerHookCommand(options) {
9
- if (!isValidHook(options.hook)) {
10
- options.stderr(`gji trigger-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(', ')}\n`);
17
+ export async function runHookCommand(options) {
18
+ const normalized = CAMEL_ALIASES[options.hook] ?? options.hook;
19
+ if (!isValidHook(normalized)) {
20
+ options.stderr(`gji run-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(", ")}\n`);
11
21
  return 1;
12
22
  }
13
- const hookName = options.hook;
23
+ const hookName = normalized;
14
24
  const repository = await detectRepository(options.cwd);
15
25
  const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
16
26
  const hooks = extractHooks(config);
@@ -1 +1 @@
1
- export declare function renderShellCompletion(shell: 'bash' | 'fish' | 'zsh'): string;
1
+ export declare function renderShellCompletion(shell: "bash" | "fish" | "zsh"): string;
@@ -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: "run-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 = ["after-create", "after-enter", "before-remove"];
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
  ;;
@@ -97,7 +116,7 @@ _gji_completion() {
97
116
  remove|rm)
98
117
  COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) -f --force --dry-run --json --help" -- "$cur") )
99
118
  ;;
100
- trigger-hook)
119
+ run-hook)
101
120
  COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
102
121
  ;;
103
122
  warp)
@@ -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 run-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
  ;;
@@ -281,7 +309,7 @@ case "\${words[2]}" in
281
309
  remove|rm)
282
310
  _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
283
311
  ;;
284
- trigger-hook)
312
+ run-hook)
285
313
  _arguments "2:hook:(${hooks})"
286
314
  ;;
287
315
  warp)
@@ -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
+ }