@solaqua/gji 0.15.0 → 0.16.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 (63) hide show
  1. package/README.md +63 -120
  2. package/dist/cli/commands/back.d.ts +1 -1
  3. package/dist/cli/commands/back.js +1 -0
  4. package/dist/cli/commands/clean.d.ts +1 -1
  5. package/dist/cli/commands/clean.js +11 -2
  6. package/dist/cli/commands/done.js +3 -2
  7. package/dist/cli/commands/go.js +18 -5
  8. package/dist/cli/commands/init.js +0 -14
  9. package/dist/cli/commands/new.d.ts +2 -1
  10. package/dist/cli/commands/new.js +71 -63
  11. package/dist/cli/commands/open.js +1 -1
  12. package/dist/cli/commands/pr-open.js +2 -2
  13. package/dist/cli/commands/pr.js +17 -6
  14. package/dist/cli/commands/run-hook.js +4 -0
  15. package/dist/cli/commands/undo.js +10 -74
  16. package/dist/cli/dependencies.d.ts +5 -1
  17. package/dist/cli/dependencies.js +7 -2
  18. package/dist/cli/program.js +1 -56
  19. package/dist/cli/runtime/headless.d.ts +3 -3
  20. package/dist/cli/runtime/headless.js +6 -4
  21. package/dist/gji-bundle.mjs +500 -842
  22. package/dist/infrastructure/filesystem/locked-file.d.ts +4 -0
  23. package/dist/infrastructure/filesystem/locked-file.js +53 -0
  24. package/dist/infrastructure/git/runner.d.ts +7 -0
  25. package/dist/infrastructure/git/runner.js +35 -1
  26. package/dist/infrastructure/persistence/history.js +12 -10
  27. package/dist/infrastructure/persistence/slots.js +24 -21
  28. package/dist/infrastructure/process/hooks.js +2 -0
  29. package/dist/infrastructure/repository/registry.js +11 -41
  30. package/dist/infrastructure/worktree/lifecycle.d.ts +25 -0
  31. package/dist/infrastructure/worktree/lifecycle.js +120 -19
  32. package/dist/ports/git.d.ts +1 -0
  33. package/dist/ports/hooks.d.ts +7 -0
  34. package/dist/presentation/shell/completion.js +7 -30
  35. package/dist/presentation/terminal/context-card.js +1 -1
  36. package/man/man1/gji-back.1 +1 -1
  37. package/man/man1/gji-clean.1 +1 -1
  38. package/man/man1/gji-completion.1 +1 -1
  39. package/man/man1/gji-config.1 +1 -1
  40. package/man/man1/gji-doctor.1 +1 -1
  41. package/man/man1/gji-done.1 +1 -1
  42. package/man/man1/gji-go.1 +1 -1
  43. package/man/man1/gji-history.1 +1 -1
  44. package/man/man1/gji-init.1 +1 -1
  45. package/man/man1/gji-ls.1 +1 -1
  46. package/man/man1/gji-new.1 +1 -1
  47. package/man/man1/gji-open.1 +1 -1
  48. package/man/man1/gji-pr.1 +1 -1
  49. package/man/man1/gji-root.1 +1 -1
  50. package/man/man1/gji-run-hook.1 +3 -3
  51. package/man/man1/gji-status.1 +1 -1
  52. package/man/man1/gji-sync-files.1 +1 -1
  53. package/man/man1/gji-sync.1 +1 -1
  54. package/man/man1/gji-task.1 +1 -1
  55. package/man/man1/gji-undo.1 +1 -1
  56. package/man/man1/gji.1 +2 -12
  57. package/package.json +3 -2
  58. package/dist/cli/commands/remove.d.ts +0 -21
  59. package/dist/cli/commands/remove.js +0 -189
  60. package/dist/cli/commands/warp.d.ts +0 -28
  61. package/dist/cli/commands/warp.js +0 -123
  62. package/man/man1/gji-remove.1 +0 -19
  63. package/man/man1/gji-warp.1 +0 -16
@@ -23,7 +23,7 @@ export function createPrOpenCommand(dependencies = {}) {
23
23
  return 1;
24
24
  }
25
25
  if (options.select && isHeadless()) {
26
- options.stderr("gji pr open --select: selector is unavailable in non-interactive mode (GJI_NO_TUI=1)\n");
26
+ options.stderr("gji pr open --select: selector is unavailable in non-interactive mode; pass a branch or PR number instead\n");
27
27
  return 1;
28
28
  }
29
29
  const repository = await detectRepository(options.cwd).catch((error) => {
@@ -236,7 +236,7 @@ async function openPullRequests(pullRequests, options, promptForPullRequest, ope
236
236
  return 1;
237
237
  }
238
238
  if (pullRequests.length > 1 && isHeadless()) {
239
- options.stderr("gji pr open: multiple open PRs found; pass an explicit PR number in non-interactive mode (GJI_NO_TUI=1)\n");
239
+ options.stderr("gji pr open: multiple open PRs found; pass an explicit PR number in non-interactive mode\n");
240
240
  return 1;
241
241
  }
242
242
  const selected = pullRequests.length === 1
@@ -54,7 +54,7 @@ export function createPrCommand(dependencies = {}) {
54
54
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
55
55
  }
56
56
  else {
57
- options.stderr(`gji pr: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
57
+ options.stderr(`gji pr: ${message} in non-interactive mode\n`);
58
58
  options.stderr(`Hint: Use 'gji done pr/${prNumber}' or 'gji clean' to remove the existing worktree\n`);
59
59
  }
60
60
  return 1;
@@ -108,10 +108,21 @@ export function createPrCommand(dependencies = {}) {
108
108
  }
109
109
  await mkdir(dirname(worktreePath), { recursive: true });
110
110
  const branchAlreadyExists = await localBranchExists(repository.repoRoot, branchName, runtime);
111
- const worktreeArgs = branchAlreadyExists
112
- ? ["worktree", "add", worktreePath, branchName]
113
- : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
114
- await runtime.git.runGit(repository.repoRoot, worktreeArgs);
111
+ try {
112
+ await runtime.git.addWorktree(repository.repoRoot, worktreePath, branchAlreadyExists
113
+ ? { branch: branchName }
114
+ : { branch: branchName, create: { startPoint: remoteRef } });
115
+ }
116
+ catch (error) {
117
+ const message = `failed to create worktree: ${error instanceof Error ? error.message : String(error)}`;
118
+ if (options.json) {
119
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
120
+ }
121
+ else {
122
+ options.stderr(`gji pr: ${message}\n`);
123
+ }
124
+ return 1;
125
+ }
115
126
  const bootstrap = await runtime.bootstrap.bootstrapWorktree({
116
127
  branch: branchName,
117
128
  config,
@@ -173,7 +184,7 @@ export const runPrCommand = createPrCommand();
173
184
  async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef, runtime) {
174
185
  for (const sourceRef of listPullRequestSourceRefs(input, prNumber)) {
175
186
  try {
176
- await runtime.git.runGit(repoRoot, [
187
+ await runtime.git.runGitRetryingContention(repoRoot, [
177
188
  "fetch",
178
189
  "origin",
179
190
  `${sourceRef}:${remoteRef}`,
@@ -1,12 +1,16 @@
1
1
  import { defaultCliDependencies } from "../dependencies.js";
2
2
  const VALID_HOOKS = [
3
+ "after-branch-create",
3
4
  "after-create",
4
5
  "after-enter",
6
+ "after-switch",
5
7
  "before-remove",
6
8
  ];
7
9
  const CAMEL_ALIASES = {
10
+ afterBranchCreate: "after-branch-create",
8
11
  afterCreate: "after-create",
9
12
  afterEnter: "after-enter",
13
+ afterSwitch: "after-switch",
10
14
  beforeRemove: "before-remove",
11
15
  };
12
16
  function isValidHook(hook) {
@@ -1,14 +1,11 @@
1
- import { access, mkdir, open, readFile, rename, stat, unlink, writeFile, } from "node:fs/promises";
1
+ import { access, readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
3
+ import { join, resolve } from "node:path";
4
4
  import { confirm, isCancel } from "@clack/prompts";
5
+ import { withFileLock, writeJsonAtomic, } from "../../infrastructure/filesystem/locked-file.js";
5
6
  import { defaultCliDependencies, } from "../dependencies.js";
6
7
  import { isHeadless } from "../runtime/headless.js";
7
8
  const MAX_UNDO_RECORDS = 20;
8
- const UNDO_LOCK_STALE_AFTER_MS = 60_000;
9
- const UNDO_LOCK_RETRY_DELAY_MS = 5;
10
- const UNDO_LOCK_WAIT_TIMEOUT_MS = UNDO_LOCK_STALE_AFTER_MS + UNDO_LOCK_RETRY_DELAY_MS;
11
- const undoLogQueues = new Map();
12
9
  export function undoLogPath(home = homedir(), globalConfigDirectory = defaultCliDependencies.configStore
13
10
  .GLOBAL_CONFIG_DIRECTORY) {
14
11
  const configured = process.env.GJI_CONFIG_DIR;
@@ -65,16 +62,16 @@ export async function recordUndoOperation(op, repoRoot, worktrees, home = homedi
65
62
  timestamp,
66
63
  entries,
67
64
  };
68
- return enqueueUndoLogWrite(home, async () => {
65
+ return withFileLock(undoLogPath(home, globalConfigDirectory), async () => {
69
66
  const existing = await loadUndoRecords(home, globalConfigDirectory);
70
67
  await writeUndoRecords(home, [record, ...existing].slice(0, MAX_UNDO_RECORDS), globalConfigDirectory);
71
68
  return record;
72
- }, globalConfigDirectory);
69
+ });
73
70
  }
74
71
  export async function finalizeUndoOperation(record, removedWorktrees, home = homedir(), globalConfigDirectory = defaultCliDependencies.configStore
75
72
  .GLOBAL_CONFIG_DIRECTORY) {
76
73
  const removedPaths = new Set(removedWorktrees.map((worktree) => worktree.path));
77
- await enqueueUndoLogWrite(home, async () => {
74
+ await withFileLock(undoLogPath(home, globalConfigDirectory), async () => {
78
75
  const records = await loadUndoRecords(home, globalConfigDirectory);
79
76
  const remaining = records.flatMap((candidate) => {
80
77
  if (candidate.id !== record.id)
@@ -83,7 +80,7 @@ export async function finalizeUndoOperation(record, removedWorktrees, home = hom
83
80
  return entries.length === 0 ? [] : [{ ...candidate, entries }];
84
81
  });
85
82
  await writeUndoRecords(home, remaining, globalConfigDirectory);
86
- }, globalConfigDirectory);
83
+ });
87
84
  }
88
85
  export async function restoreUndoRecord(record, home = homedir(), runtime = defaultCliDependencies) {
89
86
  const globalConfigDirectory = runtime.configStore.GLOBAL_CONFIG_DIRECTORY;
@@ -104,7 +101,7 @@ export async function restoreUndoRecord(record, home = homedir(), runtime = defa
104
101
  unresolved.push(entry);
105
102
  }
106
103
  }
107
- await enqueueUndoLogWrite(home, async () => {
104
+ await withFileLock(undoLogPath(home, globalConfigDirectory), async () => {
108
105
  const records = await loadUndoRecords(home, globalConfigDirectory);
109
106
  const remaining = records.flatMap((candidate) => {
110
107
  if (candidate.id !== record.id)
@@ -114,7 +111,7 @@ export async function restoreUndoRecord(record, home = homedir(), runtime = defa
114
111
  return [{ ...candidate, entries: unresolved }];
115
112
  });
116
113
  await writeUndoRecords(home, remaining, globalConfigDirectory);
117
- }, globalConfigDirectory);
114
+ });
118
115
  return { restored, failed };
119
116
  }
120
117
  export async function runUndoCommand(options) {
@@ -292,70 +289,9 @@ async function assertCommitExists(repoRoot, headSha, runGit) {
292
289
  throw new Error("commit no longer exists (gc'd) — cannot restore");
293
290
  }
294
291
  }
295
- async function enqueueUndoLogWrite(home, operation, globalConfigDirectory = defaultCliDependencies.configStore
296
- .GLOBAL_CONFIG_DIRECTORY) {
297
- const path = undoLogPath(home, globalConfigDirectory);
298
- await mkdir(dirname(path), { recursive: true });
299
- const previous = undoLogQueues.get(path) ?? Promise.resolve();
300
- const runLocked = async () => {
301
- const release = await acquireUndoLogLock(path);
302
- try {
303
- return await operation();
304
- }
305
- finally {
306
- await release();
307
- }
308
- };
309
- const current = previous.then(runLocked, runLocked);
310
- const settled = current.then(() => undefined, () => undefined);
311
- undoLogQueues.set(path, settled);
312
- try {
313
- return await current;
314
- }
315
- finally {
316
- if (undoLogQueues.get(path) === settled)
317
- undoLogQueues.delete(path);
318
- }
319
- }
320
- async function acquireUndoLogLock(path) {
321
- const lockPath = `${path}.lock`;
322
- const deadline = Date.now() + UNDO_LOCK_WAIT_TIMEOUT_MS;
323
- while (Date.now() < deadline) {
324
- try {
325
- const handle = await open(lockPath, "wx");
326
- return async () => {
327
- await handle.close();
328
- await unlink(lockPath).catch(() => undefined);
329
- };
330
- }
331
- catch (error) {
332
- if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
333
- throw error;
334
- const lockStat = await stat(lockPath).catch(() => null);
335
- if (lockStat && Date.now() - lockStat.mtimeMs > UNDO_LOCK_STALE_AFTER_MS)
336
- await unlink(lockPath).catch(() => undefined);
337
- const remainingMs = deadline - Date.now();
338
- if (remainingMs > 0)
339
- await new Promise((resolve) => setTimeout(resolve, Math.min(UNDO_LOCK_RETRY_DELAY_MS, remainingMs)));
340
- }
341
- }
342
- throw new Error("timed out waiting for the undo journal lock");
343
- }
344
292
  async function writeUndoRecords(home, records, globalConfigDirectory = defaultCliDependencies.configStore
345
293
  .GLOBAL_CONFIG_DIRECTORY) {
346
- const path = undoLogPath(home, globalConfigDirectory);
347
- await mkdir(dirname(path), { recursive: true });
348
- const temporaryPath = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
349
- try {
350
- await writeFile(temporaryPath, `${JSON.stringify(records, null, 2)}\n`, {
351
- encoding: "utf8",
352
- flag: "wx",
353
- });
354
- await rename(temporaryPath, path);
355
- }
356
- finally {
357
- await unlink(temporaryPath).catch(() => undefined);
358
- }
294
+ await writeJsonAtomic(undoLogPath(home, globalConfigDirectory), records);
359
295
  }
360
296
  function isUndoRecord(value) {
361
297
  if (!value || typeof value !== "object")
@@ -12,7 +12,7 @@ import { clearTask, readTask, writeTask } from "../infrastructure/persistence/ta
12
12
  import { extractHooks, runHook } from "../infrastructure/process/hooks.js";
13
13
  import { loadRegistry, REGISTRY_FILE_PATH, registerRepo, removeMissingRegistryEntries } from "../infrastructure/repository/registry.js";
14
14
  import { readWorktreeInfo, readWorktreeInfos, serializeWorktreeInfo } from "../infrastructure/worktree/info.js";
15
- import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isSubmoduleWorktreeRemovalError, isWorktreeDeletionError, isWorktreeForceRemovalError, removeWorktree } from "../infrastructure/worktree/lifecycle.js";
15
+ import { addWorktree, createBranch, deleteBranch, deleteCreatedBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isSubmoduleWorktreeRemovalError, isWorktreeDeletionError, isWorktreeForceRemovalError, removeCreatedWorktree, removeWorktree } from "../infrastructure/worktree/lifecycle.js";
16
16
  import type { DependencyBootstrapPort, WorktreeBootstrapPort } from "../ports/bootstrap.js";
17
17
  import type { ConfigPort } from "../ports/config.js";
18
18
  import type { GitCommandPort } from "../ports/git.js";
@@ -42,6 +42,10 @@ export interface CliDependencies {
42
42
  runHook: typeof runHook;
43
43
  };
44
44
  git: GitCommandPort & {
45
+ addWorktree: typeof addWorktree;
46
+ createBranch: typeof createBranch;
47
+ deleteCreatedBranch: typeof deleteCreatedBranch;
48
+ removeCreatedWorktree: typeof removeCreatedWorktree;
45
49
  isDirtyWorktree: typeof isDirtyWorktree;
46
50
  readWorktreeHealth: typeof readWorktreeHealth;
47
51
  isBranchMergedInto: typeof isBranchMergedInto;
@@ -2,7 +2,7 @@ import { prepareDependencyBootstrap, previewDependencyBootstrap, resolveDependen
2
2
  import { validateSyncFilePattern } from "../infrastructure/filesystem/file-sync.js";
3
3
  import { isDirtyWorktree, readWorktreeHealth, } from "../infrastructure/git/health.js";
4
4
  import { isBranchMergedInto, readBranchLastCommitTimestamp, resolveRemoteBase, resolveRemoteDefaultBranch, } from "../infrastructure/git/refs.js";
5
- import { runGit, runGitRaw } from "../infrastructure/git/runner.js";
5
+ import { runGit, runGitRaw, runGitRetryingContention, } from "../infrastructure/git/runner.js";
6
6
  import { openBrowser } from "../infrastructure/integrations/browser.js";
7
7
  import { defaultSpawnEditor, EDITORS, } from "../infrastructure/integrations/editor.js";
8
8
  import { createPullRequestQuery } from "../infrastructure/integrations/pull-requests.js";
@@ -15,7 +15,7 @@ import { configPort, repositoryContextPort, repositoryRefPort, repositoryRegistr
15
15
  import { loadRegistry, REGISTRY_FILE_PATH, registerRepo, removeMissingRegistryEntries, } from "../infrastructure/repository/registry.js";
16
16
  import { bootstrapWorktree } from "../infrastructure/worktree/bootstrap.js";
17
17
  import { readWorktreeInfo, readWorktreeInfos, serializeWorktreeInfo, } from "../infrastructure/worktree/info.js";
18
- import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isSubmoduleWorktreeRemovalError, isWorktreeDeletionError, isWorktreeForceRemovalError, removeWorktree, } from "../infrastructure/worktree/lifecycle.js";
18
+ import { addWorktree, createBranch, deleteBranch, deleteCreatedBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isSubmoduleWorktreeRemovalError, isWorktreeDeletionError, isWorktreeForceRemovalError, removeCreatedWorktree, removeWorktree, } from "../infrastructure/worktree/lifecycle.js";
19
19
  import { formatLastCommit, formatRelativeAge, formatUpstreamState, } from "../presentation/worktree/format.js";
20
20
  export function createCliDependencies() {
21
21
  const pullRequests = createPullRequestQuery();
@@ -49,6 +49,11 @@ export function createCliDependencies() {
49
49
  git: {
50
50
  runGit,
51
51
  runGitRaw,
52
+ runGitRetryingContention,
53
+ addWorktree,
54
+ createBranch,
55
+ deleteCreatedBranch,
56
+ removeCreatedWorktree,
52
57
  isDirtyWorktree,
53
58
  readWorktreeHealth,
54
59
  isBranchMergedInto,
@@ -16,7 +16,6 @@ import { runNewCommand } from "./commands/new.js";
16
16
  import { runOpenCommand } from "./commands/open.js";
17
17
  import { runPrCommand } from "./commands/pr.js";
18
18
  import { createPrOpenCommand, } from "./commands/pr-open.js";
19
- import { runRemoveCommand } from "./commands/remove.js";
20
19
  import { runRootCommand } from "./commands/root.js";
21
20
  import { runHookCommand } from "./commands/run-hook.js";
22
21
  import { runStatusCommand } from "./commands/status.js";
@@ -24,7 +23,6 @@ import { runSyncCommand } from "./commands/sync.js";
24
23
  import { runSyncFilesCommand } from "./commands/sync-files-command.js";
25
24
  import { runTaskCommand } from "./commands/task-command.js";
26
25
  import { runUndoCommand } from "./commands/undo.js";
27
- import { runWarpCommand } from "./commands/warp.js";
28
26
  import { createCliDependencies } from "./dependencies.js";
29
27
  import { isHeadless } from "./runtime/headless.js";
30
28
  export function createProgram() {
@@ -299,27 +297,11 @@ function registerCommands(program) {
299
297
  .option("--dry-run", "show what would be deleted without removing anything")
300
298
  .option("--json", "emit JSON on success or error instead of human-readable output")
301
299
  .action(notImplemented("clean"));
302
- program
303
- .command("remove [branch]")
304
- .alias("rm")
305
- .description("deprecated: use gji done for one worktree or gji clean for bulk cleanup")
306
- .option("-f, --force", "bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch")
307
- .option("--dry-run", "show what would be deleted without removing anything")
308
- .option("--json", "emit JSON on success or error instead of human-readable output")
309
- .action(notImplemented("remove"));
310
300
  program
311
301
  .command("run-hook <hook>")
312
302
  .alias("trigger-hook")
313
- .description("run a named hook (after-create, after-enter, before-remove) in the current worktree")
303
+ .description("run a named hook (after-branch-create, after-create, after-enter, after-switch, before-remove) in the current worktree")
314
304
  .action(notImplemented("run-hook"));
315
- program
316
- .command("warp [branch]")
317
- .description("deprecated: use gji go (press Tab for all known repos)")
318
- // --print is the shell-wrapper bypass signal (see SHELL_WRAPPED_COMMANDS in init.ts).
319
- // The shell omits GJI_WARP_OUTPUT_FILE, so writeShellOutput falls through to stdout.
320
- .option("--print", "print the resolved worktree path without changing directory")
321
- .option("--json", "emit JSON on success or error instead of human-readable output")
322
- .action(notImplemented("warp"));
323
305
  const configCommand = program
324
306
  .command("config")
325
307
  .description("manage global config defaults")
@@ -649,27 +631,6 @@ function attachCommandActions(program, options) {
649
631
  throw commanderExit(exitCode);
650
632
  }
651
633
  });
652
- const runRemovalCommand = async (branch, commandOptions = {}) => {
653
- const exitCode = await runRemoveCommand({
654
- branch,
655
- cwd: options.cwd,
656
- dryRun: commandOptions.dryRun,
657
- force: commandOptions.force,
658
- json: commandOptions.json,
659
- runtime: options.dependencies,
660
- stderr: options.stderr,
661
- stdout: options.stdout,
662
- });
663
- if (exitCode !== 0) {
664
- throw commanderExit(exitCode);
665
- }
666
- };
667
- requireCommand(program, "remove").action(async (branch, commandOptions) => {
668
- if (!commandOptions.json) {
669
- options.stderr("gji remove is deprecated; use 'gji done' for one worktree or 'gji clean' for bulk cleanup.\n");
670
- }
671
- return runRemovalCommand(branch, commandOptions);
672
- });
673
634
  requireCommand(program, "run-hook").action(async (hook) => {
674
635
  const exitCode = await runHookCommand({
675
636
  cwd: options.cwd,
@@ -681,22 +642,6 @@ function attachCommandActions(program, options) {
681
642
  throw commanderExit(exitCode);
682
643
  }
683
644
  });
684
- requireCommand(program, "warp").action(async (branch, commandOptions) => {
685
- if (!commandOptions.json) {
686
- options.stderr("gji warp is deprecated; use 'gji go' (press Tab for all repositories).\n");
687
- }
688
- const exitCode = await runWarpCommand({
689
- branch,
690
- cwd: options.cwd,
691
- json: commandOptions.json,
692
- runtime: options.dependencies,
693
- stderr: options.stderr,
694
- stdout: options.stdout,
695
- });
696
- if (exitCode !== 0) {
697
- throw commanderExit(exitCode);
698
- }
699
- });
700
645
  const configCommand = requireCommand(program, "config");
701
646
  configCommand.action(async () => {
702
647
  const exitCode = await runConfigCommand({
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Returns true when running in a non-interactive (headless) environment.
3
- * Set GJI_NO_TUI=1 to disable all interactive prompts.
4
- * Commands that would otherwise hang waiting for input must fail fast instead.
2
+ * Returns true when no one can answer a prompt: GJI_NO_TUI=1 is set, or stdin
3
+ * or stdout is not a terminal.
4
+ * Commands that would otherwise wait for input must fail fast instead.
5
5
  */
6
6
  export declare function isHeadless(): boolean;
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Returns true when running in a non-interactive (headless) environment.
3
- * Set GJI_NO_TUI=1 to disable all interactive prompts.
4
- * Commands that would otherwise hang waiting for input must fail fast instead.
2
+ * Returns true when no one can answer a prompt: GJI_NO_TUI=1 is set, or stdin
3
+ * or stdout is not a terminal.
4
+ * Commands that would otherwise wait 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
+ process.stdin.isTTY !== true ||
9
+ process.stdout.isTTY !== true);
8
10
  }