@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/go.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type WorktreeHealth } from './git.js';
2
- import { type WorktreeEntry } from './repo.js';
1
+ import { type WorktreeHealth } from "./git.js";
2
+ import { type WorktreeEntry } from "./repo.js";
3
3
  export interface GoCommandOptions {
4
4
  branch?: string;
5
5
  cwd: string;
package/dist/go.js CHANGED
@@ -1,14 +1,14 @@
1
- import { basename } from 'node:path';
2
- import { isCancel, select } from '@clack/prompts';
3
- import { loadEffectiveConfig } from './config.js';
4
- import { readWorktreeHealth } from './git.js';
5
- import { isHeadless } from './headless.js';
6
- import { extractHooks, runHook } from './hooks.js';
7
- import { appendHistory } from './history.js';
8
- import { detectRepository, listWorktrees, sortByCurrentFirst } from './repo.js';
9
- import { writeShellOutput } from './shell-handoff.js';
10
- import { resolveWarpTarget } from './warp.js';
11
- const GO_OUTPUT_FILE_ENV = 'GJI_GO_OUTPUT_FILE';
1
+ import { basename } from "node:path";
2
+ import { isCancel, select } from "@clack/prompts";
3
+ import { loadEffectiveConfig } from "./config.js";
4
+ import { readWorktreeHealth } from "./git.js";
5
+ import { isHeadless } from "./headless.js";
6
+ import { appendHistory } from "./history.js";
7
+ import { extractHooks, runHook } from "./hooks.js";
8
+ import { detectRepository, listWorktrees, sortByCurrentFirst, } from "./repo.js";
9
+ import { writeShellOutput } from "./shell-handoff.js";
10
+ import { resolveWarpTarget } from "./warp.js";
11
+ const GO_OUTPUT_FILE_ENV = "GJI_GO_OUTPUT_FILE";
12
12
  export function createGoCommand(dependencies = {}) {
13
13
  const prompt = dependencies.promptForWorktree ?? promptForWorktree;
14
14
  return async function runGoCommand(options) {
@@ -23,10 +23,13 @@ export function createGoCommand(dependencies = {}) {
23
23
  catch {
24
24
  // Not inside a git repo — fall back to cross-repo navigation.
25
25
  if (isHeadless() && !options.branch) {
26
- options.stderr('gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n');
26
+ options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
27
27
  return 1;
28
28
  }
29
- const target = await resolveWarpTarget({ ...options, commandName: 'gji go' });
29
+ const target = await resolveWarpTarget({
30
+ ...options,
31
+ commandName: "gji go",
32
+ });
30
33
  if (!target)
31
34
  return 1;
32
35
  appendHistory(target.path, target.branch).catch(() => undefined);
@@ -34,27 +37,33 @@ export function createGoCommand(dependencies = {}) {
34
37
  return 0;
35
38
  }
36
39
  if (!options.branch && isHeadless()) {
37
- options.stderr('gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n');
40
+ options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
38
41
  return 1;
39
42
  }
40
- const prompted = options.branch ? null : await prompt(sortByCurrentFirst(worktrees));
43
+ const prompted = options.branch
44
+ ? null
45
+ : await prompt(sortByCurrentFirst(worktrees));
41
46
  const resolvedPath = options.branch
42
47
  ? worktrees.find((entry) => entry.branch === options.branch)?.path
43
- : prompted ?? undefined;
48
+ : (prompted ?? undefined);
44
49
  if (!resolvedPath) {
45
50
  if (options.branch) {
46
51
  options.stderr(`No worktree found for branch: ${options.branch}\n`);
47
52
  options.stderr(`Hint: Use 'gji ls' to see available worktrees\n`);
48
53
  }
49
54
  else {
50
- options.stderr('Aborted\n');
55
+ options.stderr("Aborted\n");
51
56
  }
52
57
  return 1;
53
58
  }
54
59
  const chosenWorktree = worktrees.find((w) => w.path === resolvedPath);
55
60
  const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
56
61
  const hooks = extractHooks(config);
57
- await runHook(hooks.afterEnter, resolvedPath, { branch: chosenWorktree?.branch ?? undefined, path: resolvedPath, repo: basename(repository.repoRoot) }, options.stderr);
62
+ await runHook(hooks.afterEnter, resolvedPath, {
63
+ branch: chosenWorktree?.branch ?? undefined,
64
+ path: resolvedPath,
65
+ repo: basename(repository.repoRoot),
66
+ }, options.stderr);
58
67
  appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => undefined);
59
68
  await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
60
69
  return 0;
@@ -64,14 +73,18 @@ export const runGoCommand = createGoCommand();
64
73
  async function promptForWorktree(worktrees) {
65
74
  const healthResults = await Promise.allSettled(worktrees.map((w) => readWorktreeHealth(w.path)));
66
75
  const choice = await select({
67
- message: 'Choose a worktree',
76
+ message: "Choose a worktree",
68
77
  options: worktrees.map((worktree, i) => {
69
- const health = healthResults[i].status === 'fulfilled' ? healthResults[i].value : null;
70
- const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
71
- const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
78
+ const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
79
+ const pathHint = worktree.isCurrent
80
+ ? `${worktree.path} (current)`
81
+ : worktree.path;
82
+ const upstream = health
83
+ ? formatUpstreamHint(worktree.branch, health)
84
+ : null;
72
85
  return {
73
86
  value: worktree.path,
74
- label: worktree.branch ?? '(detached)',
87
+ label: worktree.branch ?? "(detached)",
75
88
  hint: upstream ? `${upstream} · ${pathHint}` : pathHint,
76
89
  };
77
90
  }),
@@ -85,11 +98,11 @@ export function formatUpstreamHint(branch, health) {
85
98
  if (branch === null)
86
99
  return null;
87
100
  if (!health.hasUpstream)
88
- return 'no upstream';
101
+ return "no upstream";
89
102
  if (health.upstreamGone)
90
- return 'upstream gone';
103
+ return "upstream gone";
91
104
  if (health.ahead === 0 && health.behind === 0)
92
- return 'up to date';
105
+ return "up to date";
93
106
  if (health.ahead === 0)
94
107
  return `behind ${health.behind}`;
95
108
  if (health.behind === 0)
package/dist/headless.js CHANGED
@@ -4,5 +4,5 @@
4
4
  * Commands that would otherwise hang waiting for input must fail fast instead.
5
5
  */
6
6
  export function isHeadless() {
7
- return process.env.GJI_NO_TUI === '1';
7
+ return process.env.GJI_NO_TUI === "1";
8
8
  }
@@ -1,5 +1,5 @@
1
- import { loadHistory } from './history.js';
2
- import { formatHistoryList } from './back.js';
1
+ import { formatHistoryList } from "./back.js";
2
+ import { loadHistory } from "./history.js";
3
3
  export async function runHistoryCommand(options) {
4
4
  const history = await loadHistory(options.home);
5
5
  if (options.json) {
@@ -7,7 +7,7 @@ export async function runHistoryCommand(options) {
7
7
  return 0;
8
8
  }
9
9
  if (history.length === 0) {
10
- options.stdout('No navigation history.\n');
10
+ options.stdout("No navigation history.\n");
11
11
  return 0;
12
12
  }
13
13
  options.stdout(formatHistoryList(history, options.cwd));
package/dist/history.js CHANGED
@@ -1,8 +1,8 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import { homedir } from 'node:os';
3
- import { dirname, join, resolve } from 'node:path';
4
- import { GLOBAL_CONFIG_DIRECTORY } from './config.js';
5
- export const HISTORY_FILE_NAME = 'history.json';
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { GLOBAL_CONFIG_DIRECTORY } from "./config.js";
5
+ export const HISTORY_FILE_NAME = "history.json";
6
6
  const MAX_HISTORY_ENTRIES = 50;
7
7
  export function HISTORY_FILE_PATH(home = homedir()) {
8
8
  const configDir = process.env.GJI_CONFIG_DIR;
@@ -14,7 +14,7 @@ export function HISTORY_FILE_PATH(home = homedir()) {
14
14
  export async function loadHistory(home = homedir()) {
15
15
  const path = HISTORY_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 [];
@@ -34,13 +34,13 @@ export async function appendHistory(path, branch, home = homedir()) {
34
34
  const entry = { branch, path, timestamp: Date.now() };
35
35
  const next = [entry, ...existing].slice(0, MAX_HISTORY_ENTRIES);
36
36
  await mkdir(dirname(historyPath), { recursive: true });
37
- await writeFile(historyPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
37
+ await writeFile(historyPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
38
38
  }
39
39
  function isHistoryEntry(value) {
40
- return (typeof value === 'object' &&
40
+ return (typeof value === "object" &&
41
41
  value !== null &&
42
- 'path' in value &&
43
- typeof value.path === 'string' &&
44
- 'timestamp' in value &&
45
- typeof value.timestamp === 'number');
42
+ "path" in value &&
43
+ typeof value.path === "string" &&
44
+ "timestamp" in value &&
45
+ typeof value.timestamp === "number");
46
46
  }
package/dist/hooks.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn } from 'node:child_process';
1
+ import { spawn } from "node:child_process";
2
2
  export async function runHook(hookCmd, cwd, context, stderr) {
3
3
  if (!hookCmd)
4
4
  return;
@@ -11,26 +11,26 @@ export async function runHook(hookCmd, cwd, context, stderr) {
11
11
  async function runArgvHook(hookCmd, cwd, context, stderr) {
12
12
  const [command, ...args] = hookCmd.map((arg) => interpolate(arg, context));
13
13
  if (!command) {
14
- stderr('gji: hook argv command must include a non-empty command\n');
14
+ stderr("gji: hook argv command must include a non-empty command\n");
15
15
  return;
16
16
  }
17
17
  await new Promise((resolve) => {
18
18
  const child = spawn(command, args, {
19
19
  cwd,
20
20
  shell: false,
21
- stdio: ['ignore', 'inherit', 'pipe'],
21
+ stdio: ["ignore", "inherit", "pipe"],
22
22
  env: hookEnvironment(context),
23
23
  });
24
- child.stderr.on('data', (chunk) => {
24
+ child.stderr.on("data", (chunk) => {
25
25
  stderr(chunk.toString());
26
26
  });
27
- child.on('close', (code) => {
27
+ child.on("close", (code) => {
28
28
  if (code !== 0) {
29
29
  stderr(`gji: hook exited with code ${code}: ${formatArgvHook(command, args)}\n`);
30
30
  }
31
31
  resolve();
32
32
  });
33
- child.on('error', (err) => {
33
+ child.on("error", (err) => {
34
34
  stderr(`gji: hook failed to start: ${err.message}\n`);
35
35
  resolve();
36
36
  });
@@ -42,19 +42,19 @@ async function runShellHook(hookCmd, cwd, context, stderr) {
42
42
  const child = spawn(interpolated, {
43
43
  cwd,
44
44
  shell: true,
45
- stdio: ['ignore', 'inherit', 'pipe'],
45
+ stdio: ["ignore", "inherit", "pipe"],
46
46
  env: hookEnvironment(context),
47
47
  });
48
- child.stderr.on('data', (chunk) => {
48
+ child.stderr.on("data", (chunk) => {
49
49
  stderr(chunk.toString());
50
50
  });
51
- child.on('close', (code) => {
51
+ child.on("close", (code) => {
52
52
  if (code !== 0) {
53
53
  stderr(`gji: hook exited with code ${code}: ${interpolated}\n`);
54
54
  }
55
55
  resolve();
56
56
  });
57
- child.on('error', (err) => {
57
+ child.on("error", (err) => {
58
58
  stderr(`gji: hook failed to start: ${err.message}\n`);
59
59
  resolve();
60
60
  });
@@ -63,7 +63,7 @@ async function runShellHook(hookCmd, cwd, context, stderr) {
63
63
  function hookEnvironment(context) {
64
64
  return {
65
65
  ...process.env,
66
- GJI_BRANCH: context.branch ?? '',
66
+ GJI_BRANCH: context.branch ?? "",
67
67
  GJI_PATH: context.path,
68
68
  GJI_REPO: context.repo,
69
69
  };
@@ -73,13 +73,13 @@ function formatArgvHook(command, args) {
73
73
  }
74
74
  export function interpolate(template, context) {
75
75
  return template
76
- .replace(/\{\{branch\}\}/g, context.branch ?? '')
76
+ .replace(/\{\{branch\}\}/g, context.branch ?? "")
77
77
  .replace(/\{\{path\}\}/g, context.path)
78
78
  .replace(/\{\{repo\}\}/g, context.repo);
79
79
  }
80
80
  export function extractHooks(config) {
81
81
  const raw = config.hooks;
82
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
82
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
83
83
  return {};
84
84
  }
85
85
  const hooks = raw;
@@ -90,12 +90,12 @@ export function extractHooks(config) {
90
90
  };
91
91
  }
92
92
  function parseHookCommand(value) {
93
- if (typeof value === 'string')
93
+ if (typeof value === "string")
94
94
  return value;
95
95
  if (Array.isArray(value) &&
96
96
  value.length > 0 &&
97
- value[0] !== '' &&
98
- value.every((item) => typeof item === 'string')) {
97
+ value[0] !== "" &&
98
+ value.every((item) => typeof item === "string")) {
99
99
  return value;
100
100
  }
101
101
  return undefined;
package/dist/index.js CHANGED
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { homedir } from 'node:os';
3
- import { loadGlobalConfig } from './config.js';
4
- import { runCli } from './cli.js';
2
+ import { homedir } from "node:os";
3
+ import { runCli } from "./cli.js";
4
+ import { loadGlobalConfig } from "./config.js";
5
5
  async function main() {
6
6
  try {
7
7
  const argv = process.argv.slice(2);
8
8
  // Warn once (until fixed) when shell integration hasn't been set up.
9
9
  // Only shown in interactive terminals — suppressed in pipes and after gji init --write.
10
- const isMetaArg = argv[0] === 'init'
11
- || argv[0] === '--version' || argv[0] === '-V'
12
- || argv[0] === '--help' || argv[0] === '-h';
10
+ const isMetaArg = argv[0] === "init" ||
11
+ argv[0] === "--version" ||
12
+ argv[0] === "-V" ||
13
+ argv[0] === "--help" ||
14
+ argv[0] === "-h";
13
15
  if (process.stderr.isTTY === true && !isMetaArg) {
14
16
  await warnIfMissingShellIntegration();
15
17
  }
@@ -20,7 +22,7 @@ async function main() {
20
22
  process.exitCode = result.exitCode;
21
23
  }
22
24
  catch (error) {
23
- const message = error instanceof Error ? error.message : 'An unknown error occurred.';
25
+ const message = error instanceof Error ? error.message : "An unknown error occurred.";
24
26
  process.stderr.write(`${message}\n`);
25
27
  process.exitCode = 1;
26
28
  }
@@ -29,8 +31,10 @@ async function warnIfMissingShellIntegration() {
29
31
  try {
30
32
  const { config } = await loadGlobalConfig(homedir());
31
33
  if (!config.shellIntegration) {
32
- const shellBin = (process.env.SHELL ?? '').split('/').at(-1);
33
- const shellArg = shellBin && ['bash', 'zsh', 'fish'].includes(shellBin) ? ` ${shellBin}` : '';
34
+ const shellBin = (process.env.SHELL ?? "").split("/").at(-1);
35
+ const shellArg = shellBin && ["bash", "zsh", "fish"].includes(shellBin)
36
+ ? ` ${shellBin}`
37
+ : "";
34
38
  process.stderr.write(`gji: shell integration not set up — run \`gji init${shellArg} --write\` to enable automatic cd.\n`);
35
39
  }
36
40
  }
package/dist/init.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type SupportedShell } from './shell.js';
2
- export type InstallSaveTarget = 'local' | 'global';
1
+ import { type SupportedShell } from "./shell.js";
2
+ export type InstallSaveTarget = "local" | "global";
3
3
  export interface SetupWizardResult {
4
4
  branchPrefix?: string;
5
5
  hooks?: {