@solaqua/gji 0.5.0 → 0.6.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.
package/dist/go.js CHANGED
@@ -7,14 +7,32 @@ import { extractHooks, runHook } from './hooks.js';
7
7
  import { appendHistory } from './history.js';
8
8
  import { detectRepository, listWorktrees, sortByCurrentFirst } from './repo.js';
9
9
  import { writeShellOutput } from './shell-handoff.js';
10
+ import { resolveWarpTarget } from './warp.js';
10
11
  const GO_OUTPUT_FILE_ENV = 'GJI_GO_OUTPUT_FILE';
11
12
  export function createGoCommand(dependencies = {}) {
12
13
  const prompt = dependencies.promptForWorktree ?? promptForWorktree;
13
14
  return async function runGoCommand(options) {
14
- const [worktrees, repository] = await Promise.all([
15
- listWorktrees(options.cwd),
16
- detectRepository(options.cwd),
17
- ]);
15
+ let worktrees;
16
+ let repository;
17
+ try {
18
+ [worktrees, repository] = await Promise.all([
19
+ listWorktrees(options.cwd),
20
+ detectRepository(options.cwd),
21
+ ]);
22
+ }
23
+ catch {
24
+ // Not inside a git repo — fall back to cross-repo navigation.
25
+ if (isHeadless() && !options.branch) {
26
+ options.stderr('gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n');
27
+ return 1;
28
+ }
29
+ const target = await resolveWarpTarget({ ...options, commandName: 'gji go' });
30
+ if (!target)
31
+ return 1;
32
+ appendHistory(target.path, target.branch).catch(() => undefined);
33
+ await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
34
+ return 0;
35
+ }
18
36
  if (!options.branch && isHeadless()) {
19
37
  options.stderr('gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n');
20
38
  return 1;
package/dist/hooks.d.ts CHANGED
@@ -1,13 +1,14 @@
1
+ export type GjiHookCommand = string | string[];
1
2
  export interface GjiHooks {
2
- afterCreate?: string;
3
- afterEnter?: string;
4
- beforeRemove?: string;
3
+ afterCreate?: GjiHookCommand;
4
+ afterEnter?: GjiHookCommand;
5
+ beforeRemove?: GjiHookCommand;
5
6
  }
6
7
  export interface HookContext {
7
8
  branch?: string;
8
9
  path: string;
9
10
  repo: string;
10
11
  }
11
- export declare function runHook(hookCmd: string | undefined, cwd: string, context: HookContext, stderr: (chunk: string) => void): Promise<void>;
12
+ export declare function runHook(hookCmd: GjiHookCommand | undefined, cwd: string, context: HookContext, stderr: (chunk: string) => void): Promise<void>;
12
13
  export declare function interpolate(template: string, context: HookContext): string;
13
14
  export declare function extractHooks(config: Record<string, unknown>): GjiHooks;
package/dist/hooks.js CHANGED
@@ -2,18 +2,48 @@ import { spawn } from 'node:child_process';
2
2
  export async function runHook(hookCmd, cwd, context, stderr) {
3
3
  if (!hookCmd)
4
4
  return;
5
+ if (Array.isArray(hookCmd)) {
6
+ await runArgvHook(hookCmd, cwd, context, stderr);
7
+ return;
8
+ }
9
+ await runShellHook(hookCmd, cwd, context, stderr);
10
+ }
11
+ async function runArgvHook(hookCmd, cwd, context, stderr) {
12
+ const [command, ...args] = hookCmd.map((arg) => interpolate(arg, context));
13
+ if (!command) {
14
+ stderr('gji: hook argv command must include a non-empty command\n');
15
+ return;
16
+ }
17
+ await new Promise((resolve) => {
18
+ const child = spawn(command, args, {
19
+ cwd,
20
+ shell: false,
21
+ stdio: ['ignore', 'inherit', 'pipe'],
22
+ env: hookEnvironment(context),
23
+ });
24
+ child.stderr.on('data', (chunk) => {
25
+ stderr(chunk.toString());
26
+ });
27
+ child.on('close', (code) => {
28
+ if (code !== 0) {
29
+ stderr(`gji: hook exited with code ${code}: ${formatArgvHook(command, args)}\n`);
30
+ }
31
+ resolve();
32
+ });
33
+ child.on('error', (err) => {
34
+ stderr(`gji: hook failed to start: ${err.message}\n`);
35
+ resolve();
36
+ });
37
+ });
38
+ }
39
+ async function runShellHook(hookCmd, cwd, context, stderr) {
5
40
  const interpolated = interpolate(hookCmd, context);
6
41
  await new Promise((resolve) => {
7
42
  const child = spawn(interpolated, {
8
43
  cwd,
9
44
  shell: true,
10
45
  stdio: ['ignore', 'inherit', 'pipe'],
11
- env: {
12
- ...process.env,
13
- GJI_BRANCH: context.branch ?? '',
14
- GJI_PATH: context.path,
15
- GJI_REPO: context.repo,
16
- },
46
+ env: hookEnvironment(context),
17
47
  });
18
48
  child.stderr.on('data', (chunk) => {
19
49
  stderr(chunk.toString());
@@ -30,6 +60,17 @@ export async function runHook(hookCmd, cwd, context, stderr) {
30
60
  });
31
61
  });
32
62
  }
63
+ function hookEnvironment(context) {
64
+ return {
65
+ ...process.env,
66
+ GJI_BRANCH: context.branch ?? '',
67
+ GJI_PATH: context.path,
68
+ GJI_REPO: context.repo,
69
+ };
70
+ }
71
+ function formatArgvHook(command, args) {
72
+ return JSON.stringify([command, ...args]);
73
+ }
33
74
  export function interpolate(template, context) {
34
75
  return template
35
76
  .replace(/\{\{branch\}\}/g, context.branch ?? '')
@@ -43,8 +84,19 @@ export function extractHooks(config) {
43
84
  }
44
85
  const hooks = raw;
45
86
  return {
46
- afterCreate: typeof hooks.afterCreate === 'string' ? hooks.afterCreate : undefined,
47
- afterEnter: typeof hooks.afterEnter === 'string' ? hooks.afterEnter : undefined,
48
- beforeRemove: typeof hooks.beforeRemove === 'string' ? hooks.beforeRemove : undefined,
87
+ afterCreate: parseHookCommand(hooks.afterCreate),
88
+ afterEnter: parseHookCommand(hooks.afterEnter),
89
+ beforeRemove: parseHookCommand(hooks.beforeRemove),
49
90
  };
50
91
  }
92
+ function parseHookCommand(value) {
93
+ if (typeof value === 'string')
94
+ return value;
95
+ if (Array.isArray(value) &&
96
+ value.length > 0 &&
97
+ value[0] !== '' &&
98
+ value.every((item) => typeof item === 'string')) {
99
+ return value;
100
+ }
101
+ return undefined;
102
+ }
package/dist/init.js CHANGED
@@ -8,47 +8,54 @@ const START_MARKER = '# >>> gji init >>>';
8
8
  const END_MARKER = '# <<< gji init <<<';
9
9
  const SHELL_WRAPPED_COMMANDS = [
10
10
  {
11
- bypassOption: '--help',
11
+ bypassOptions: ['--help'],
12
12
  commandName: 'new',
13
13
  envVar: 'GJI_NEW_OUTPUT_FILE',
14
14
  names: ['new'],
15
15
  tempPrefix: 'gji-new',
16
16
  },
17
17
  {
18
- bypassOption: '--help',
18
+ bypassOptions: ['--help'],
19
19
  commandName: 'pr',
20
20
  envVar: 'GJI_PR_OUTPUT_FILE',
21
21
  names: ['pr'],
22
22
  tempPrefix: 'gji-pr',
23
23
  },
24
24
  {
25
- bypassOption: '--print',
25
+ bypassOptions: ['--print'],
26
26
  commandName: 'back',
27
27
  envVar: 'GJI_BACK_OUTPUT_FILE',
28
28
  names: ['back'],
29
29
  tempPrefix: 'gji-back',
30
30
  },
31
31
  {
32
- bypassOption: '--print',
32
+ bypassOptions: ['--print'],
33
33
  commandName: 'go',
34
34
  envVar: 'GJI_GO_OUTPUT_FILE',
35
- names: ['go'],
35
+ names: ['go', 'jump'],
36
36
  tempPrefix: 'gji-go',
37
37
  },
38
38
  {
39
- bypassOption: '--print',
39
+ bypassOptions: ['--print'],
40
40
  commandName: 'root',
41
41
  envVar: 'GJI_ROOT_OUTPUT_FILE',
42
42
  names: ['root'],
43
43
  tempPrefix: 'gji-root',
44
44
  },
45
45
  {
46
- bypassOption: '--help',
46
+ bypassOptions: ['--help'],
47
47
  commandName: 'remove',
48
48
  envVar: 'GJI_REMOVE_OUTPUT_FILE',
49
49
  names: ['remove', 'rm'],
50
50
  tempPrefix: 'gji-remove',
51
51
  },
52
+ {
53
+ bypassOptions: ['--print', '--json'],
54
+ commandName: 'warp',
55
+ envVar: 'GJI_WARP_OUTPUT_FILE',
56
+ names: ['warp'],
57
+ tempPrefix: 'gji-warp',
58
+ },
52
59
  ];
53
60
  export async function runInitCommand(options) {
54
61
  const shell = resolveSupportedShell(options.shell, process.env.SHELL);
@@ -178,10 +185,17 @@ function isMissingFileError(error) {
178
185
  return error instanceof Error && 'code' in error && error.code === 'ENOENT';
179
186
  }
180
187
  function renderFishWrapper(command) {
181
- const tests = command.names.map((name) => `test $argv[1] = ${name}`).join('; or ');
182
- return `if test (count $argv) -gt 0; and ${tests}
188
+ const nameTests = command.names.map((name) => `test $argv[1] = ${name}`);
189
+ const nameCondition = nameTests.length === 1
190
+ ? nameTests[0]
191
+ : `begin; ${nameTests.join('; or ')}; end`;
192
+ const bypassTests = command.bypassOptions.map((opt) => `test $argv[1] = ${opt}`);
193
+ const bypassCondition = bypassTests.length === 1
194
+ ? bypassTests[0]
195
+ : `begin; ${bypassTests.join('; or ')}; end`;
196
+ return `if test (count $argv) -gt 0; and ${nameCondition}
183
197
  set -e argv[1]
184
- if test (count $argv) -gt 0; and test $argv[1] = ${command.bypassOption}
198
+ if test (count $argv) -gt 0; and ${bypassCondition}
185
199
  command gji ${command.commandName} $argv
186
200
  return $status
187
201
  end
@@ -202,9 +216,10 @@ end`;
202
216
  }
203
217
  function renderPosixWrapper(command) {
204
218
  const tests = command.names.map((name) => `[ "$1" = "${name}" ]`).join(' || ');
219
+ const bypassTests = command.bypassOptions.map((opt) => `[ "\${1:-}" = "${opt}" ]`).join(' || ');
205
220
  return `if ${tests}; then
206
221
  shift
207
- if [ "\${1:-}" = "${command.bypassOption}" ]; then
222
+ if ${bypassTests}; then
208
223
  command gji ${command.commandName} "$@"
209
224
  return $?
210
225
  fi
@@ -10,7 +10,7 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
10
10
  }
11
11
  // Skip if afterCreate hook is already configured in effective config.
12
12
  const hooks = isPlainObject(config.hooks) ? config.hooks : null;
13
- if (typeof hooks?.afterCreate === 'string' && hooks.afterCreate.length > 0) {
13
+ if (isConfiguredHookCommand(hooks?.afterCreate)) {
14
14
  return;
15
15
  }
16
16
  // Skip if user has permanently opted out of install prompts.
@@ -120,3 +120,11 @@ async function defaultPromptForInstallChoice(pm) {
120
120
  function isPlainObject(value) {
121
121
  return typeof value === 'object' && value !== null && !Array.isArray(value);
122
122
  }
123
+ function isConfiguredHookCommand(value) {
124
+ if (typeof value === 'string')
125
+ return value.length > 0;
126
+ return (Array.isArray(value) &&
127
+ value.length > 0 &&
128
+ value[0] !== '' &&
129
+ value.every((item) => typeof item === 'string'));
130
+ }
package/dist/new.d.ts CHANGED
@@ -6,8 +6,10 @@ export interface NewCommandOptions {
6
6
  cwd: string;
7
7
  detached?: boolean;
8
8
  dryRun?: boolean;
9
+ editor?: string;
9
10
  force?: boolean;
10
11
  json?: boolean;
12
+ open?: boolean;
11
13
  stderr: (chunk: string) => void;
12
14
  stdout: (chunk: string) => void;
13
15
  }
@@ -15,6 +17,7 @@ export interface NewCommandDependencies extends InstallPromptDependencies {
15
17
  createBranchPlaceholder: () => string;
16
18
  promptForBranch: (placeholder: string) => Promise<string | null>;
17
19
  promptForPathConflict: (path: string) => Promise<PathConflictChoice>;
20
+ spawnEditor: (cli: string, args: string[]) => Promise<void>;
18
21
  }
19
22
  export declare function createNewCommand(dependencies?: Partial<NewCommandDependencies>): (options: NewCommandOptions) => Promise<number>;
20
23
  export declare const runNewCommand: (options: NewCommandOptions) => Promise<number>;
package/dist/new.js CHANGED
@@ -4,6 +4,7 @@ import { execFile } from 'node:child_process';
4
4
  import { promisify } from 'node:util';
5
5
  import { isCancel, text } from '@clack/prompts';
6
6
  import { loadEffectiveConfig, resolveConfigString } from './config.js';
7
+ import { defaultSpawnEditor, EDITORS } from './editor.js';
7
8
  import { syncFiles } from './file-sync.js';
8
9
  import { extractHooks, runHook } from './hooks.js';
9
10
  import { appendHistory } from './history.js';
@@ -18,10 +19,14 @@ export function createNewCommand(dependencies = {}) {
18
19
  const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
19
20
  const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
20
21
  const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
22
+ const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
21
23
  return async function runNewCommand(options) {
22
24
  const repository = await detectRepository(options.cwd);
23
25
  const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
24
26
  const usesGeneratedDetachedName = options.detached && options.branch === undefined;
27
+ if (options.editor && !options.open) {
28
+ options.stderr('gji new: --editor has no effect without --open\n');
29
+ }
25
30
  if (!options.detached && !options.branch && (options.json || isHeadless())) {
26
31
  const message = 'branch argument is required';
27
32
  if (options.json) {
@@ -119,7 +124,11 @@ export function createNewCommand(dependencies = {}) {
119
124
  options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}\n`);
120
125
  }
121
126
  else {
122
- options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName})\n`);
127
+ const resolvedEditor = options.open
128
+ ? (options.editor ?? resolveConfigString(config, 'editor'))
129
+ : undefined;
130
+ const openNote = resolvedEditor ? `, then open in ${resolvedEditor}` : '';
131
+ options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})\n`);
123
132
  }
124
133
  return 0;
125
134
  }
@@ -152,6 +161,10 @@ export function createNewCommand(dependencies = {}) {
152
161
  await appendHistory(worktreePath, worktreeName);
153
162
  await writeOutput(worktreePath, options.stdout);
154
163
  }
164
+ if (options.open) {
165
+ const resolvedEditor = options.editor ?? resolveConfigString(config, 'editor');
166
+ await openWorktree(worktreePath, resolvedEditor, spawnEditor, options.stderr);
167
+ }
155
168
  return 0;
156
169
  };
157
170
  }
@@ -259,3 +272,22 @@ function hasExecStderr(error) {
259
272
  function toExecMessage(error) {
260
273
  return hasExecStderr(error) ? error.stderr.trim() : String(error);
261
274
  }
275
+ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
276
+ if (!editorCli) {
277
+ stderr('gji new: --open requires --editor <cli> or a saved editor in config\n');
278
+ return;
279
+ }
280
+ const editorDef = EDITORS.find((e) => e.cli === editorCli);
281
+ const args = [];
282
+ if (editorDef?.newWindowFlag) {
283
+ args.push(editorDef.newWindowFlag);
284
+ }
285
+ args.push(worktreePath);
286
+ try {
287
+ await spawnFn(editorCli, args);
288
+ }
289
+ catch (error) {
290
+ const message = error instanceof Error ? error.message : String(error);
291
+ stderr(`gji new: failed to open editor: ${message}\n`);
292
+ }
293
+ }
package/dist/open.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { type EditorDefinition } from './editor.js';
2
+ import { type WorktreeEntry } from './repo.js';
3
+ export type { EditorDefinition };
4
+ export interface OpenCommandOptions {
5
+ branch?: string;
6
+ cwd: string;
7
+ editor?: string;
8
+ save?: boolean;
9
+ stderr: (chunk: string) => void;
10
+ stdout: (chunk: string) => void;
11
+ workspace?: boolean;
12
+ }
13
+ export interface OpenCommandDependencies {
14
+ detectEditors: () => Promise<EditorDefinition[]>;
15
+ promptForEditor: (editors: EditorDefinition[]) => Promise<string | null>;
16
+ promptForWorktree: (worktrees: WorktreeEntry[]) => Promise<string | null>;
17
+ spawnEditor: (cli: string, args: string[]) => Promise<void>;
18
+ }
19
+ export declare function createOpenCommand(dependencies?: Partial<OpenCommandDependencies>): (options: OpenCommandOptions) => Promise<number>;
20
+ export declare const runOpenCommand: (options: OpenCommandOptions) => Promise<number>;
package/dist/open.js ADDED
@@ -0,0 +1,155 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { access, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { isCancel, select } from '@clack/prompts';
6
+ import { loadEffectiveConfig, resolveConfigString, updateGlobalConfigKey } from './config.js';
7
+ import { defaultSpawnEditor, EDITORS } from './editor.js';
8
+ import { isHeadless } from './headless.js';
9
+ import { detectRepository, listWorktrees, sortByCurrentFirst } from './repo.js';
10
+ const execFileAsync = promisify(execFile);
11
+ export function createOpenCommand(dependencies = {}) {
12
+ const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
13
+ const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
14
+ const promptForWorktree = dependencies.promptForWorktree ?? defaultPromptForWorktree;
15
+ const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
16
+ return async function runOpenCommand(options) {
17
+ const [worktrees, repository] = await Promise.all([
18
+ listWorktrees(options.cwd),
19
+ detectRepository(options.cwd),
20
+ ]);
21
+ // Resolve target worktree path.
22
+ let targetPath;
23
+ if (options.branch) {
24
+ const entry = worktrees.find((w) => w.branch === options.branch);
25
+ if (!entry) {
26
+ options.stderr(`gji open: no worktree found for branch: ${options.branch}\n`);
27
+ options.stderr(`Hint: Use 'gji ls' to see available worktrees\n`);
28
+ return 1;
29
+ }
30
+ targetPath = entry.path;
31
+ }
32
+ else if (isHeadless()) {
33
+ targetPath = worktrees.find((w) => w.isCurrent)?.path ?? options.cwd;
34
+ }
35
+ else {
36
+ const chosen = await promptForWorktree(sortByCurrentFirst(worktrees));
37
+ if (!chosen) {
38
+ options.stderr('Aborted\n');
39
+ return 1;
40
+ }
41
+ targetPath = chosen;
42
+ }
43
+ // Resolve which editor to use.
44
+ const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
45
+ const savedEditor = resolveConfigString(config, 'editor');
46
+ let editorCli;
47
+ if (options.editor) {
48
+ editorCli = options.editor;
49
+ }
50
+ else if (savedEditor) {
51
+ editorCli = savedEditor;
52
+ }
53
+ else {
54
+ const installed = await detectEditors();
55
+ if (installed.length === 0) {
56
+ options.stderr('gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n');
57
+ return 1;
58
+ }
59
+ if (installed.length === 1 || isHeadless()) {
60
+ editorCli = installed[0].cli;
61
+ }
62
+ else {
63
+ const chosen = await promptForEditor(installed);
64
+ if (!chosen) {
65
+ options.stderr('Aborted\n');
66
+ return 1;
67
+ }
68
+ editorCli = chosen;
69
+ }
70
+ }
71
+ // Persist editor choice when requested.
72
+ if (options.save && editorCli !== savedEditor) {
73
+ await updateGlobalConfigKey('editor', editorCli);
74
+ const displayName = EDITORS.find((e) => e.cli === editorCli)?.name ?? editorCli;
75
+ options.stdout(`Saved editor "${displayName}" to global config\n`);
76
+ }
77
+ // Build open args.
78
+ const editorDef = EDITORS.find((e) => e.cli === editorCli);
79
+ let openTarget = targetPath;
80
+ if (options.workspace) {
81
+ if (editorDef?.supportsWorkspace) {
82
+ openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
83
+ }
84
+ else {
85
+ const displayName = editorDef?.name ?? editorCli;
86
+ options.stderr(`gji open: --workspace is not supported for ${displayName}, ignoring\n`);
87
+ }
88
+ }
89
+ const args = [];
90
+ if (editorDef?.newWindowFlag) {
91
+ args.push(editorDef.newWindowFlag);
92
+ }
93
+ args.push(openTarget);
94
+ try {
95
+ await spawnEditor(editorCli, args);
96
+ }
97
+ catch (error) {
98
+ const message = error instanceof Error ? error.message : String(error);
99
+ options.stderr(`gji open: failed to launch editor: ${message}\n`);
100
+ return 1;
101
+ }
102
+ const displayName = editorDef?.name ?? editorCli;
103
+ options.stdout(`Opened ${targetPath} in ${displayName}\n`);
104
+ return 0;
105
+ };
106
+ }
107
+ export const runOpenCommand = createOpenCommand();
108
+ async function detectInstalledEditors() {
109
+ const results = await Promise.all(EDITORS.map(async (editor) => ({ editor, available: await isCommandAvailable(editor.cli) })));
110
+ return results.filter((r) => r.available).map((r) => r.editor);
111
+ }
112
+ async function isCommandAvailable(command) {
113
+ try {
114
+ await execFileAsync('which', [command]);
115
+ return true;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ async function defaultPromptForWorktree(worktrees) {
122
+ const choice = await select({
123
+ message: 'Choose a worktree to open',
124
+ options: worktrees.map((w) => ({
125
+ value: w.path,
126
+ label: w.branch ?? '(detached)',
127
+ hint: w.isCurrent ? `${w.path} (current)` : w.path,
128
+ })),
129
+ });
130
+ if (isCancel(choice))
131
+ return null;
132
+ return choice;
133
+ }
134
+ async function defaultPromptForEditor(editors) {
135
+ const choice = await select({
136
+ message: 'Choose an editor',
137
+ options: editors.map((e) => ({ value: e.cli, label: e.name })),
138
+ });
139
+ if (isCancel(choice))
140
+ return null;
141
+ return choice;
142
+ }
143
+ async function ensureWorkspaceFile(worktreePath, repoName) {
144
+ const workspacePath = join(worktreePath, `${repoName}.code-workspace`);
145
+ try {
146
+ await access(workspacePath);
147
+ return workspacePath;
148
+ }
149
+ catch {
150
+ // File doesn't exist yet — create it.
151
+ }
152
+ const workspace = { folders: [{ path: '.' }], settings: {} };
153
+ await writeFile(workspacePath, `${JSON.stringify(workspace, null, 2)}\n`, 'utf8');
154
+ return workspacePath;
155
+ }
@@ -0,0 +1,8 @@
1
+ export interface RepoRegistryEntry {
2
+ lastUsed: number;
3
+ name: string;
4
+ path: string;
5
+ }
6
+ export declare function REGISTRY_FILE_PATH(home?: string): string;
7
+ export declare function loadRegistry(home?: string): Promise<RepoRegistryEntry[]>;
8
+ export declare function registerRepo(repoPath: string, home?: string): Promise<void>;
@@ -0,0 +1,52 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { GLOBAL_CONFIG_DIRECTORY } from './config.js';
5
+ const REGISTRY_FILE_NAME = 'repos.json';
6
+ const MAX_REGISTRY_ENTRIES = 100;
7
+ export function REGISTRY_FILE_PATH(home = homedir()) {
8
+ const configDir = process.env.GJI_CONFIG_DIR;
9
+ if (configDir) {
10
+ return join(resolve(configDir), REGISTRY_FILE_NAME);
11
+ }
12
+ return join(home, GLOBAL_CONFIG_DIRECTORY, REGISTRY_FILE_NAME);
13
+ }
14
+ export async function loadRegistry(home = homedir()) {
15
+ const path = REGISTRY_FILE_PATH(home);
16
+ try {
17
+ const raw = await readFile(path, 'utf8');
18
+ const parsed = JSON.parse(raw);
19
+ if (!Array.isArray(parsed))
20
+ return [];
21
+ return parsed.filter(isRegistryEntry);
22
+ }
23
+ catch {
24
+ return [];
25
+ }
26
+ }
27
+ export async function registerRepo(repoPath, home = homedir()) {
28
+ const registryPath = REGISTRY_FILE_PATH(home);
29
+ const existing = await loadRegistry(home);
30
+ // Skip write if this repo is already the most-recently-used entry (common case).
31
+ if (existing.length > 0 && existing[0].path === repoPath)
32
+ return;
33
+ const entry = {
34
+ lastUsed: Date.now(),
35
+ name: basename(repoPath),
36
+ path: repoPath,
37
+ };
38
+ const filtered = existing.filter((e) => e.path !== repoPath);
39
+ const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
40
+ await mkdir(dirname(registryPath), { recursive: true });
41
+ await writeFile(registryPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
42
+ }
43
+ function isRegistryEntry(value) {
44
+ return (typeof value === 'object' &&
45
+ value !== null &&
46
+ 'path' in value &&
47
+ typeof value.path === 'string' &&
48
+ 'name' in value &&
49
+ typeof value.name === 'string' &&
50
+ 'lastUsed' in value &&
51
+ typeof value.lastUsed === 'number');
52
+ }
package/dist/warp.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export interface WarpCommandOptions {
2
+ branch?: string;
3
+ cwd: string;
4
+ json?: boolean;
5
+ newWorktree?: boolean;
6
+ stderr: (chunk: string) => void;
7
+ stdout: (chunk: string) => void;
8
+ }
9
+ export declare function runWarpCommand(options: WarpCommandOptions): Promise<number>;
10
+ export interface WarpTarget {
11
+ branch: string | null;
12
+ path: string;
13
+ }
14
+ export declare function resolveWarpTarget(options: {
15
+ branch?: string;
16
+ commandName?: string;
17
+ cwd: string;
18
+ json?: boolean;
19
+ stderr: (chunk: string) => void;
20
+ }): Promise<WarpTarget | null>;