@solaqua/gji 0.4.0 → 0.5.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 (47) hide show
  1. package/README.md +124 -11
  2. package/dist/back.d.ts +13 -0
  3. package/dist/back.js +74 -0
  4. package/dist/clean.d.ts +1 -0
  5. package/dist/clean.js +103 -13
  6. package/dist/cli.js +104 -4
  7. package/dist/completion.d.ts +6 -0
  8. package/dist/completion.js +11 -0
  9. package/dist/git.d.ts +3 -0
  10. package/dist/git.js +38 -0
  11. package/dist/gji +5 -0
  12. package/dist/gji-bundle.mjs +16705 -0
  13. package/dist/go.js +2 -0
  14. package/dist/history-command.d.ts +7 -0
  15. package/dist/history-command.js +15 -0
  16. package/dist/history.d.ts +9 -0
  17. package/dist/history.js +46 -0
  18. package/dist/init.d.ts +1 -1
  19. package/dist/init.js +9 -22
  20. package/dist/ls.d.ts +3 -0
  21. package/dist/ls.js +46 -2
  22. package/dist/new.js +3 -0
  23. package/dist/pr.js +46 -1
  24. package/dist/shell-completion.d.ts +1 -0
  25. package/dist/shell-completion.js +284 -0
  26. package/dist/shell.d.ts +2 -0
  27. package/dist/shell.js +21 -0
  28. package/dist/sync.js +2 -13
  29. package/dist/worktree-info.d.ts +33 -0
  30. package/dist/worktree-info.js +105 -0
  31. package/man/man1/gji-back.1 +13 -0
  32. package/man/man1/gji-clean.1 +4 -1
  33. package/man/man1/gji-completion.1 +9 -0
  34. package/man/man1/gji-config.1 +1 -1
  35. package/man/man1/gji-go.1 +1 -1
  36. package/man/man1/gji-history.1 +13 -0
  37. package/man/man1/gji-init.1 +1 -1
  38. package/man/man1/gji-ls.1 +4 -1
  39. package/man/man1/gji-new.1 +1 -1
  40. package/man/man1/gji-pr.1 +1 -1
  41. package/man/man1/gji-remove.1 +1 -1
  42. package/man/man1/gji-root.1 +1 -1
  43. package/man/man1/gji-status.1 +1 -1
  44. package/man/man1/gji-sync.1 +1 -1
  45. package/man/man1/gji-trigger-hook.1 +1 -1
  46. package/man/man1/gji.1 +13 -1
  47. package/package.json +11 -2
package/dist/sync.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { loadEffectiveConfig } from './config.js';
2
- import { isDirtyWorktree, runGit } from './git.js';
2
+ import { isDirtyWorktree, resolveRemoteDefaultBranch, runGit } from './git.js';
3
3
  import { comparePaths } from './paths.js';
4
4
  import { detectRepository, listWorktrees } from './repo.js';
5
5
  export async function runSyncCommand(options) {
@@ -10,7 +10,7 @@ export async function runSyncCommand(options) {
10
10
  let defaultBranch;
11
11
  try {
12
12
  defaultBranch = resolveConfiguredString(config.syncDefaultBranch)
13
- ?? await resolveDefaultBranch(repository.repoRoot, remote);
13
+ ?? await resolveRemoteDefaultBranch(repository.repoRoot, remote);
14
14
  }
15
15
  catch {
16
16
  emitError(options, `Unable to reach remote '${remote}'`);
@@ -89,17 +89,6 @@ function selectTargetWorktrees(worktrees, currentRoot, all) {
89
89
  }
90
90
  return [currentWorktree];
91
91
  }
92
- async function resolveDefaultBranch(repoRoot, remote) {
93
- const stdout = await runGit(repoRoot, ['ls-remote', '--symref', remote, 'HEAD']);
94
- const refLine = stdout
95
- .split('\n')
96
- .find((line) => line.startsWith('ref: refs/heads/'));
97
- if (!refLine) {
98
- return null;
99
- }
100
- const match = /^ref: refs\/heads\/(.+)\tHEAD$/.exec(refLine);
101
- return match?.[1] ?? null;
102
- }
103
92
  function resolveConfiguredString(value) {
104
93
  return typeof value === 'string' && value.length > 0 ? value : null;
105
94
  }
@@ -0,0 +1,33 @@
1
+ import { type WorktreeHealth } from './git.js';
2
+ import type { WorktreeEntry } from './repo.js';
3
+ export interface WorktreeInfo extends WorktreeEntry {
4
+ lastCommitTimestamp: number | null;
5
+ status: WorktreeHealth['status'] | 'unknown';
6
+ upstream: UpstreamState;
7
+ }
8
+ export interface SerializedWorktreeInfo {
9
+ branch: string | null;
10
+ lastCommitTimestamp: number | null;
11
+ path: string;
12
+ status: WorktreeInfo['status'];
13
+ upstream: UpstreamState;
14
+ }
15
+ export type UpstreamState = {
16
+ kind: 'detached';
17
+ } | {
18
+ kind: 'no-upstream';
19
+ } | {
20
+ kind: 'stale';
21
+ } | {
22
+ kind: 'tracked';
23
+ ahead: number;
24
+ behind: number;
25
+ } | {
26
+ kind: 'unknown';
27
+ };
28
+ export declare function readWorktreeInfos(worktrees: WorktreeEntry[]): Promise<WorktreeInfo[]>;
29
+ export declare function serializeWorktreeInfo(info: WorktreeInfo): SerializedWorktreeInfo;
30
+ export declare function formatWorktreeHint(info: WorktreeInfo): string;
31
+ export declare function formatUpstreamState(upstream: UpstreamState): string;
32
+ export declare function formatLastCommit(timestampSeconds: number | null): string;
33
+ export declare function formatRelativeAge(timestampSeconds: number): string;
@@ -0,0 +1,105 @@
1
+ import { readBranchLastCommitTimestamp, readWorktreeHealth } from './git.js';
2
+ export async function readWorktreeInfos(worktrees) {
3
+ return Promise.all(worktrees.map((worktree) => readWorktreeInfo(worktree)));
4
+ }
5
+ async function readWorktreeInfo(worktree) {
6
+ const [healthResult, lastCommitResult] = await Promise.allSettled([
7
+ readWorktreeHealth(worktree.path),
8
+ worktree.branch === null ? null : readBranchLastCommitTimestamp(worktree.path, worktree.branch),
9
+ ]);
10
+ const health = healthResult.status === 'fulfilled' ? healthResult.value : null;
11
+ const lastCommitTimestamp = lastCommitResult.status === 'fulfilled'
12
+ ? lastCommitResult.value
13
+ : null;
14
+ return {
15
+ ...worktree,
16
+ lastCommitTimestamp,
17
+ status: health?.status ?? 'unknown',
18
+ upstream: buildUpstreamState(worktree.branch, health),
19
+ };
20
+ }
21
+ function buildUpstreamState(branch, health) {
22
+ if (branch === null) {
23
+ return { kind: 'detached' };
24
+ }
25
+ if (health === null) {
26
+ return { kind: 'unknown' };
27
+ }
28
+ if (!health.hasUpstream) {
29
+ return { kind: 'no-upstream' };
30
+ }
31
+ if (health.upstreamGone) {
32
+ return { kind: 'stale' };
33
+ }
34
+ return {
35
+ ahead: health.ahead,
36
+ behind: health.behind,
37
+ kind: 'tracked',
38
+ };
39
+ }
40
+ export function serializeWorktreeInfo(info) {
41
+ return {
42
+ branch: info.branch,
43
+ lastCommitTimestamp: info.lastCommitTimestamp,
44
+ path: info.path,
45
+ status: info.status,
46
+ upstream: info.upstream,
47
+ };
48
+ }
49
+ export function formatWorktreeHint(info) {
50
+ const details = [
51
+ `status: ${info.status}`,
52
+ `upstream: ${formatUpstreamState(info.upstream)}`,
53
+ ];
54
+ if (info.lastCommitTimestamp !== null) {
55
+ details.push(`last: ${formatRelativeAge(info.lastCommitTimestamp)}`);
56
+ }
57
+ return `${info.path} (${details.join(', ')})`;
58
+ }
59
+ export function formatUpstreamState(upstream) {
60
+ if (upstream.kind === 'detached') {
61
+ return 'n/a';
62
+ }
63
+ if (upstream.kind === 'no-upstream') {
64
+ return 'no-upstream';
65
+ }
66
+ if (upstream.kind === 'stale') {
67
+ return 'gone';
68
+ }
69
+ if (upstream.kind === 'unknown') {
70
+ return 'unknown';
71
+ }
72
+ return formatAheadBehind(upstream.ahead, upstream.behind);
73
+ }
74
+ function formatAheadBehind(ahead, behind) {
75
+ if (ahead === 0 && behind === 0) {
76
+ return 'up to date';
77
+ }
78
+ if (ahead === 0) {
79
+ return `behind ${behind}`;
80
+ }
81
+ if (behind === 0) {
82
+ return `ahead ${ahead}`;
83
+ }
84
+ return `ahead ${ahead}, behind ${behind}`;
85
+ }
86
+ export function formatLastCommit(timestampSeconds) {
87
+ return timestampSeconds === null ? 'n/a' : formatRelativeAge(timestampSeconds);
88
+ }
89
+ export function formatRelativeAge(timestampSeconds) {
90
+ const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - timestampSeconds);
91
+ const units = [
92
+ { label: 'y', seconds: 365 * 24 * 60 * 60 },
93
+ { label: 'mo', seconds: 30 * 24 * 60 * 60 },
94
+ { label: 'd', seconds: 24 * 60 * 60 },
95
+ { label: 'h', seconds: 60 * 60 },
96
+ { label: 'm', seconds: 60 },
97
+ ];
98
+ for (const unit of units) {
99
+ const value = Math.floor(ageSeconds / unit.seconds);
100
+ if (value > 0) {
101
+ return `${value}${unit.label} ago`;
102
+ }
103
+ }
104
+ return 'just now';
105
+ }
@@ -0,0 +1,13 @@
1
+ .TH GJI\-BACK 1 "May 2026" "gji 0.5.0" "User Commands"
2
+ .SH NAME
3
+ gji\-back \- navigate to the previously visited worktree, optionally N steps back
4
+ .SH SYNOPSIS
5
+ .B gji back [\fIoptions\fR] [options] [n]
6
+ .SH DESCRIPTION
7
+ navigate to the previously visited worktree, optionally N steps back
8
+ .SH OPTIONS
9
+ .TP
10
+ .B \-\-print
11
+ print the resolved worktree path explicitly
12
+ .SH "SEE ALSO"
13
+ .BR gji (1)
@@ -1,4 +1,4 @@
1
- .TH GJI\-CLEAN 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-CLEAN 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-clean \- interactively prune linked worktrees
4
4
  .SH SYNOPSIS
@@ -10,6 +10,9 @@ interactively prune linked worktrees
10
10
  .B \-f, \-\-force
11
11
  bypass prompts, force\-remove dirty worktrees, and force\-delete unmerged branches
12
12
  .TP
13
+ .B \-\-stale
14
+ only target clean worktrees whose upstream is gone and branch is merged into the default branch
15
+ .TP
13
16
  .B \-\-dry\-run
14
17
  show what would be deleted without removing anything
15
18
  .TP
@@ -0,0 +1,9 @@
1
+ .TH GJI\-COMPLETION 1 "May 2026" "gji 0.5.0" "User Commands"
2
+ .SH NAME
3
+ gji\-completion \- print shell completion definitions
4
+ .SH SYNOPSIS
5
+ .B gji completion [options] [shell]
6
+ .SH DESCRIPTION
7
+ print shell completion definitions
8
+ .SH "SEE ALSO"
9
+ .BR gji (1)
@@ -1,4 +1,4 @@
1
- .TH GJI\-CONFIG 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-CONFIG 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-config \- manage global config defaults
4
4
  .SH SYNOPSIS
package/man/man1/gji-go.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-GO 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-GO 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-go \- print or select a worktree path
4
4
  .SH SYNOPSIS
@@ -0,0 +1,13 @@
1
+ .TH GJI\-HISTORY 1 "May 2026" "gji 0.5.0" "User Commands"
2
+ .SH NAME
3
+ gji\-history \- show navigation history
4
+ .SH SYNOPSIS
5
+ .B gji history [\fIoptions\fR] [options]
6
+ .SH DESCRIPTION
7
+ show navigation history
8
+ .SH OPTIONS
9
+ .TP
10
+ .B \-\-json
11
+ print history as JSON
12
+ .SH "SEE ALSO"
13
+ .BR gji (1)
@@ -1,4 +1,4 @@
1
- .TH GJI\-INIT 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-INIT 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-init \- print or install shell integration
4
4
  .SH SYNOPSIS
package/man/man1/gji-ls.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-LS 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-LS 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-ls \- list active worktrees
4
4
  .SH SYNOPSIS
@@ -7,6 +7,9 @@ gji\-ls \- list active worktrees
7
7
  list active worktrees
8
8
  .SH OPTIONS
9
9
  .TP
10
+ .B \-\-compact
11
+ show only branch and path columns
12
+ .TP
10
13
  .B \-\-json
11
14
  print active worktrees as JSON
12
15
  .SH "SEE ALSO"
@@ -1,4 +1,4 @@
1
- .TH GJI\-NEW 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-NEW 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-new \- create a new branch or detached linked worktree
4
4
  .SH SYNOPSIS
package/man/man1/gji-pr.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-PR 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-PR 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-pr \- fetch a pull request by number, #number, or URL into a linked worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-REMOVE 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-REMOVE 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-remove \- remove a linked worktree and delete its branch when present
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-ROOT 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-ROOT 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-root \- print the main repository root path
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-STATUS 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-STATUS 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-status \- summarize repository and worktree health
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-SYNC 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-SYNC 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-sync \- fetch and update one or all worktrees
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-TRIGGER\-HOOK 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI\-TRIGGER\-HOOK 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-trigger\-hook \- run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree
4
4
  .SH SYNOPSIS
package/man/man1/gji.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI 1 "April 2026" "gji 0.4.0" "User Commands"
1
+ .TH GJI 1 "May 2026" "gji 0.5.0" "User Commands"
2
2
  .SH NAME
3
3
  gji \- Context switching without the mess.
4
4
  .SH SYNOPSIS
@@ -17,9 +17,18 @@ create a new branch or detached linked worktree
17
17
  .B init [options] [shell]
18
18
  print or install shell integration
19
19
  .TP
20
+ .B completion [options] [shell]
21
+ print shell completion definitions
22
+ .TP
20
23
  .B pr [options] <ref>
21
24
  fetch a pull request by number, #number, or URL into a linked worktree
22
25
  .TP
26
+ .B back [options] [n]
27
+ navigate to the previously visited worktree, optionally N steps back
28
+ .TP
29
+ .B history [options]
30
+ show navigation history
31
+ .TP
23
32
  .B go [options] [branch]
24
33
  print or select a worktree path
25
34
  .TP
@@ -55,7 +64,10 @@ output the version number
55
64
  .SH "SEE ALSO"
56
65
  .BR gji\-new (1),
57
66
  .BR gji\-init (1),
67
+ .BR gji\-completion (1),
58
68
  .BR gji\-pr (1),
69
+ .BR gji\-back (1),
70
+ .BR gji\-history (1),
59
71
  .BR gji\-go (1),
60
72
  .BR gji\-root (1),
61
73
  .BR gji\-status (1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solaqua/gji",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Git worktree CLI for fast context switching.",
5
5
  "license": "MIT",
6
6
  "author": "sjquant",
@@ -42,7 +42,11 @@
42
42
  },
43
43
  "scripts": {
44
44
  "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
45
+ "generate:readme-demos": "node scripts/generate-readme-demos.mjs",
46
+ "bundle": "node scripts/bundle.mjs",
45
47
  "generate-man": "node scripts/generate-man.mjs",
48
+ "docs:build": "pnpm --dir website build",
49
+ "docs:start": "pnpm --dir website start",
46
50
  "prepublishOnly": "pnpm test && pnpm build && pnpm generate-man",
47
51
  "test": "vitest run --passWithNoTests",
48
52
  "test:watch": "vitest"
@@ -50,11 +54,16 @@
50
54
  "dependencies": {
51
55
  "@clack/core": "0.5.0",
52
56
  "@clack/prompts": "^0.11.0",
53
- "commander": "^14.0.1"
57
+ "commander": "^14.0.1",
58
+ "update-notifier": "^7.3.1"
54
59
  },
55
60
  "devDependencies": {
56
61
  "@types/node": "^24.6.0",
62
+ "esbuild": "^0.27.0",
57
63
  "typescript": "^5.9.3",
58
64
  "vitest": "^3.2.4"
65
+ },
66
+ "pnpm": {
67
+ "onlyBuiltDependencies": ["esbuild"]
59
68
  }
60
69
  }