@solaqua/gji 0.1.0-beta.1 → 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createRequire } from 'node:module';
1
2
  import { Command } from 'commander';
2
3
  import { runCleanCommand } from './clean.js';
3
4
  import { runConfigCommand } from './config-command.js';
@@ -12,14 +13,21 @@ import { runStatusCommand } from './status.js';
12
13
  import { runSyncCommand } from './sync.js';
13
14
  export function createProgram() {
14
15
  const program = new Command();
16
+ const packageVersion = readPackageVersion();
15
17
  program
16
18
  .name('gji')
17
19
  .description('Context switching without the mess.')
20
+ .version(packageVersion)
18
21
  .showHelpAfterError()
19
22
  .showSuggestionAfterError();
20
23
  registerCommands(program);
21
24
  return program;
22
25
  }
26
+ function readPackageVersion() {
27
+ const require = createRequire(import.meta.url);
28
+ const packageJson = require('../package.json');
29
+ return typeof packageJson.version === 'string' ? packageJson.version : '0.0.0';
30
+ }
23
31
  export async function runCli(argv, options = {}) {
24
32
  const program = createProgram();
25
33
  const cwd = options.cwd ?? process.cwd();
@@ -49,7 +57,8 @@ export async function runCli(argv, options = {}) {
49
57
  function registerCommands(program) {
50
58
  program
51
59
  .command('new [branch]')
52
- .description('create a new branch and linked worktree')
60
+ .description('create a new branch or detached linked worktree')
61
+ .option('--detached', 'create a detached worktree without a branch')
53
62
  .action(notImplemented('new'));
54
63
  program
55
64
  .command('init [shell]')
@@ -90,6 +99,7 @@ function registerCommands(program) {
90
99
  .action(notImplemented('clean'));
91
100
  program
92
101
  .command('remove [branch]')
102
+ .alias('rm')
93
103
  .description('remove a linked worktree and delete its branch when present')
94
104
  .action(notImplemented('remove'));
95
105
  const configCommand = program
@@ -112,8 +122,8 @@ function registerCommands(program) {
112
122
  function attachCommandActions(program, options) {
113
123
  program.commands
114
124
  .find((command) => command.name() === 'new')
115
- ?.action(async (branch) => {
116
- const exitCode = await runNewCommand({ ...options, branch });
125
+ ?.action(async (branch, commandOptions) => {
126
+ const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached });
117
127
  if (exitCode !== 0) {
118
128
  throw commanderExit(exitCode);
119
129
  }
package/dist/go.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface GoCommandOptions {
7
7
  stdout: (chunk: string) => void;
8
8
  }
9
9
  export interface GoCommandDependencies {
10
+ promptForCapturedOutputWorktree: (worktrees: WorktreeEntry[]) => Promise<string | null>;
10
11
  promptForWorktree: (worktrees: WorktreeEntry[]) => Promise<string | null>;
11
12
  }
12
13
  export declare function createGoCommand(dependencies?: Partial<GoCommandDependencies>): (options: GoCommandOptions) => Promise<number>;
package/dist/go.js CHANGED
@@ -1,6 +1,9 @@
1
+ import { SelectPrompt, isCancel as isCoreCancel } from '@clack/core';
1
2
  import { isCancel, select } from '@clack/prompts';
2
3
  import { listWorktrees } from './repo.js';
4
+ const GO_TTY_PROMPT_ENV = 'GJI_GO_TTY_PROMPT';
3
5
  export function createGoCommand(dependencies = {}) {
6
+ const promptForCapturedOutput = dependencies.promptForCapturedOutputWorktree ?? promptForCapturedOutputWorktree;
4
7
  const prompt = dependencies.promptForWorktree ?? promptForWorktree;
5
8
  return async function runGoCommand(options) {
6
9
  const worktrees = await listWorktrees(options.cwd);
@@ -13,7 +16,9 @@ export function createGoCommand(dependencies = {}) {
13
16
  options.stdout(`${worktree.path}\n`);
14
17
  return 0;
15
18
  }
16
- const chosenPath = await prompt(worktrees);
19
+ const chosenPath = shouldUseCapturedOutputPrompt(options)
20
+ ? await promptForCapturedOutput(worktrees)
21
+ : await prompt(worktrees);
17
22
  if (!chosenPath) {
18
23
  options.stderr('Aborted\n');
19
24
  return 1;
@@ -37,3 +42,45 @@ async function promptForWorktree(worktrees) {
37
42
  }
38
43
  return choice;
39
44
  }
45
+ async function promptForCapturedOutputWorktree(worktrees) {
46
+ const options = worktrees.map((worktree) => ({
47
+ value: worktree.path,
48
+ label: worktree.branch ?? '(detached)',
49
+ hint: worktree.path,
50
+ }));
51
+ const prompt = new SelectPrompt({
52
+ input: process.stdin,
53
+ options,
54
+ output: process.stderr,
55
+ render() {
56
+ const lines = ['Choose a worktree'];
57
+ switch (this.state) {
58
+ case 'submit': {
59
+ const selected = this.options[this.cursor];
60
+ lines.push(`> ${selected.label} (${selected.hint})`);
61
+ break;
62
+ }
63
+ case 'cancel':
64
+ lines.push('> canceled');
65
+ break;
66
+ default:
67
+ lines.push(...this.options.map((option, index) => {
68
+ const prefix = index === this.cursor ? '> ' : ' ';
69
+ return `${prefix}${option.label} (${option.hint})`;
70
+ }));
71
+ break;
72
+ }
73
+ return `${lines.join('\n')}\n`;
74
+ },
75
+ });
76
+ const choice = await prompt.prompt();
77
+ if (isCoreCancel(choice)) {
78
+ return null;
79
+ }
80
+ return choice;
81
+ }
82
+ function shouldUseCapturedOutputPrompt(options) {
83
+ return (!options.branch &&
84
+ options.print === true &&
85
+ process.env[GO_TTY_PROMPT_ENV] === '1');
86
+ }
package/dist/init.js CHANGED
@@ -34,7 +34,7 @@ function gji --wraps gji --description 'gji shell integration'
34
34
  return $status
35
35
  end
36
36
 
37
- set -l target (command gji go --print $argv)
37
+ set -l target (env GJI_GO_TTY_PROMPT=1 command gji go --print $argv)
38
38
  or return $status
39
39
  cd $target
40
40
  return $status
@@ -56,7 +56,7 @@ gji() {
56
56
  fi
57
57
 
58
58
  local target
59
- target="$(command gji go --print "$@")" || return $?
59
+ target="$(GJI_GO_TTY_PROMPT=1 command gji go --print "$@")" || return $?
60
60
  cd "$target" || return $?
61
61
  return 0
62
62
  fi
package/dist/new.d.ts CHANGED
@@ -2,6 +2,7 @@ export type PathConflictChoice = 'abort' | 'reuse';
2
2
  export interface NewCommandOptions {
3
3
  branch?: string;
4
4
  cwd: string;
5
+ detached?: boolean;
5
6
  stderr: (chunk: string) => void;
6
7
  stdout: (chunk: string) => void;
7
8
  }
package/dist/new.js CHANGED
@@ -14,14 +14,21 @@ export function createNewCommand(dependencies = {}) {
14
14
  return async function runNewCommand(options) {
15
15
  const repository = await detectRepository(options.cwd);
16
16
  const config = await loadEffectiveConfig(repository.repoRoot);
17
- const rawBranch = options.branch ?? await promptForBranch(createBranchPlaceholder());
17
+ const usesGeneratedDetachedName = options.detached && options.branch === undefined;
18
+ const rawBranch = options.detached
19
+ ? options.branch ?? createBranchPlaceholder()
20
+ : options.branch ?? await promptForBranch(createBranchPlaceholder());
18
21
  if (!rawBranch) {
19
22
  options.stderr('Aborted\n');
20
23
  return 1;
21
24
  }
22
- const branch = applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
23
- const worktreePath = resolveWorktreePath(repository.repoRoot, branch);
24
- if (await pathExists(worktreePath)) {
25
+ const worktreeName = options.detached
26
+ ? rawBranch
27
+ : applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
28
+ const worktreePath = usesGeneratedDetachedName
29
+ ? await resolveUniqueDetachedWorktreePath(repository.repoRoot, worktreeName)
30
+ : resolveWorktreePath(repository.repoRoot, worktreeName);
31
+ if (!usesGeneratedDetachedName && await pathExists(worktreePath)) {
25
32
  const choice = await prompt(worktreePath);
26
33
  if (choice === 'reuse') {
27
34
  options.stdout(`${worktreePath}\n`);
@@ -31,7 +38,10 @@ export function createNewCommand(dependencies = {}) {
31
38
  return 1;
32
39
  }
33
40
  await mkdir(dirname(worktreePath), { recursive: true });
34
- await execFileAsync('git', ['worktree', 'add', '-b', branch, worktreePath], { cwd: repository.repoRoot });
41
+ const gitArgs = options.detached
42
+ ? ['worktree', 'add', '--detach', worktreePath]
43
+ : ['worktree', 'add', '-b', worktreeName, worktreePath];
44
+ await execFileAsync('git', gitArgs, { cwd: repository.repoRoot });
35
45
  options.stdout(`${worktreePath}\n`);
36
46
  return 0;
37
47
  };
@@ -97,6 +107,17 @@ function applyConfiguredBranchPrefix(branch, branchPrefix) {
97
107
  }
98
108
  return `${branchPrefix}${branch}`;
99
109
  }
110
+ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName) {
111
+ let attempt = 1;
112
+ while (true) {
113
+ const candidateName = attempt === 1 ? baseName : `${baseName}-${attempt}`;
114
+ const candidatePath = resolveWorktreePath(repoRoot, candidateName);
115
+ if (!await pathExists(candidatePath)) {
116
+ return candidatePath;
117
+ }
118
+ attempt += 1;
119
+ }
120
+ }
100
121
  async function promptForPathConflict(path) {
101
122
  const choice = await select({
102
123
  message: `Target path already exists: ${path}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solaqua/gji",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Git worktree CLI for fast context switching.",
5
5
  "license": "MIT",
6
6
  "author": "sjquant",
@@ -34,7 +34,7 @@
34
34
  "node": ">=18"
35
35
  },
36
36
  "bin": {
37
- "gji": "./dist/index.js"
37
+ "gji": "dist/index.js"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
@@ -43,6 +43,7 @@
43
43
  "test:watch": "vitest"
44
44
  },
45
45
  "dependencies": {
46
+ "@clack/core": "0.5.0",
46
47
  "@clack/prompts": "^0.11.0",
47
48
  "commander": "^14.0.1"
48
49
  },