@solaqua/gji 0.6.2 → 0.7.1
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/README.md +26 -1
- package/dist/back.d.ts +1 -1
- package/dist/back.js +23 -17
- package/dist/clean.d.ts +1 -1
- package/dist/clean.js +44 -35
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +266 -165
- package/dist/completion.js +3 -3
- package/dist/config-command.js +5 -5
- package/dist/config.js +41 -35
- package/dist/conflict.d.ts +1 -1
- package/dist/conflict.js +14 -6
- package/dist/editor.js +29 -9
- package/dist/file-sync.d.ts +1 -0
- package/dist/file-sync.js +15 -11
- package/dist/git.d.ts +1 -1
- package/dist/git.js +21 -19
- package/dist/gji-bundle.mjs +1688 -865
- package/dist/go.d.ts +2 -2
- package/dist/go.js +39 -26
- package/dist/headless.js +1 -1
- package/dist/history-command.js +3 -3
- package/dist/history.js +12 -12
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.js +19 -19
- package/dist/index.js +13 -9
- package/dist/init.d.ts +5 -5
- package/dist/init.js +115 -103
- package/dist/install-prompt.d.ts +3 -3
- package/dist/install-prompt.js +57 -38
- package/dist/ls.d.ts +2 -2
- package/dist/ls.js +29 -29
- package/dist/new.d.ts +2 -2
- package/dist/new.js +97 -82
- package/dist/open.d.ts +2 -2
- package/dist/open.js +24 -21
- package/dist/package-manager.js +96 -45
- package/dist/pr.d.ts +2 -2
- package/dist/pr.js +47 -34
- package/dist/remove.d.ts +1 -1
- package/dist/remove.js +39 -27
- package/dist/repo-registry.js +14 -14
- package/dist/repo.js +29 -28
- package/dist/root.js +3 -3
- package/dist/run-hook.d.ts +6 -0
- package/dist/{trigger-hook.js → run-hook.js} +18 -8
- package/dist/shell-completion.d.ts +1 -1
- package/dist/shell-completion.js +67 -39
- package/dist/shell-handoff.js +2 -2
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +4 -4
- package/dist/status.d.ts +5 -5
- package/dist/status.js +23 -23
- package/dist/sync-files-command.d.ts +10 -0
- package/dist/sync-files-command.js +137 -0
- package/dist/sync.js +23 -15
- package/dist/warp.js +37 -33
- package/dist/worktree-info.d.ts +9 -9
- package/dist/worktree-info.js +31 -29
- package/dist/worktree-management.d.ts +1 -1
- package/dist/worktree-management.js +26 -11
- package/dist/worktree-prompts.js +5 -5
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-go.1 +1 -1
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +1 -1
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +1 -1
- package/man/man1/gji-remove.1 +1 -1
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +9 -0
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +23 -0
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +10 -4
- package/package.json +8 -2
- package/dist/trigger-hook.d.ts +0 -6
- package/man/man1/gji-trigger-hook.1 +0 -9
package/dist/gji-bundle.mjs
CHANGED
|
@@ -5386,7 +5386,7 @@ var require_minimist = __commonJS({
|
|
|
5386
5386
|
var require_rc = __commonJS({
|
|
5387
5387
|
"node_modules/.pnpm/rc@1.2.8/node_modules/rc/index.js"(exports, module) {
|
|
5388
5388
|
var cc = require_utils();
|
|
5389
|
-
var
|
|
5389
|
+
var join10 = __require("path").join;
|
|
5390
5390
|
var deepExtend = require_deep_extend();
|
|
5391
5391
|
var etc = "/etc";
|
|
5392
5392
|
var win = process.platform === "win32";
|
|
@@ -5411,15 +5411,15 @@ var require_rc = __commonJS({
|
|
|
5411
5411
|
}
|
|
5412
5412
|
if (!win)
|
|
5413
5413
|
[
|
|
5414
|
-
|
|
5415
|
-
|
|
5414
|
+
join10(etc, name, "config"),
|
|
5415
|
+
join10(etc, name + "rc")
|
|
5416
5416
|
].forEach(addConfigFile);
|
|
5417
5417
|
if (home)
|
|
5418
5418
|
[
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5419
|
+
join10(home, ".config", name, "config"),
|
|
5420
|
+
join10(home, ".config", name),
|
|
5421
|
+
join10(home, "." + name, "config"),
|
|
5422
|
+
join10(home, "." + name + "rc")
|
|
5423
5423
|
].forEach(addConfigFile);
|
|
5424
5424
|
addConfigFile(cc.find("." + name + "rc"));
|
|
5425
5425
|
if (env4.config) addConfigFile(env4.config);
|
|
@@ -9422,225 +9422,7 @@ var require_picocolors = __commonJS({
|
|
|
9422
9422
|
});
|
|
9423
9423
|
|
|
9424
9424
|
// src/index.ts
|
|
9425
|
-
import { homedir as
|
|
9426
|
-
|
|
9427
|
-
// src/config.ts
|
|
9428
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
9429
|
-
import { homedir } from "node:os";
|
|
9430
|
-
import { dirname, join, resolve } from "node:path";
|
|
9431
|
-
var CONFIG_FILE_NAME = ".gji.json";
|
|
9432
|
-
var GLOBAL_CONFIG_DIRECTORY = ".config/gji";
|
|
9433
|
-
var GLOBAL_CONFIG_NAME = "config.json";
|
|
9434
|
-
var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
9435
|
-
"branchPrefix",
|
|
9436
|
-
"editor",
|
|
9437
|
-
"hooks",
|
|
9438
|
-
"installSaveTarget",
|
|
9439
|
-
"shellIntegration",
|
|
9440
|
-
"skipInstallPrompt",
|
|
9441
|
-
"syncDefaultBranch",
|
|
9442
|
-
"syncFiles",
|
|
9443
|
-
"syncRemote",
|
|
9444
|
-
"worktreePath"
|
|
9445
|
-
]);
|
|
9446
|
-
var KNOWN_GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
9447
|
-
...KNOWN_CONFIG_KEYS,
|
|
9448
|
-
"repos"
|
|
9449
|
-
]);
|
|
9450
|
-
var DEFAULT_CONFIG = Object.freeze({});
|
|
9451
|
-
async function loadConfig(root) {
|
|
9452
|
-
const path9 = join(root, CONFIG_FILE_NAME);
|
|
9453
|
-
return loadConfigFile(path9);
|
|
9454
|
-
}
|
|
9455
|
-
async function loadEffectiveConfig(root, home = homedir(), onWarning) {
|
|
9456
|
-
const [globalConfig, localConfig] = await Promise.all([
|
|
9457
|
-
loadGlobalConfig(home),
|
|
9458
|
-
loadConfig(root)
|
|
9459
|
-
]);
|
|
9460
|
-
const repos = globalConfig.config.repos;
|
|
9461
|
-
const perRepoConfig = isPlainObject(repos) ? findPerRepoConfig(repos, root, home) : {};
|
|
9462
|
-
const globalBase = { ...globalConfig.config };
|
|
9463
|
-
delete globalBase.repos;
|
|
9464
|
-
if (onWarning) {
|
|
9465
|
-
if (globalConfig.exists) {
|
|
9466
|
-
warnUnknownKeys(globalBase, globalConfig.path, KNOWN_GLOBAL_CONFIG_KEYS, onWarning);
|
|
9467
|
-
if (Object.keys(perRepoConfig).length > 0) {
|
|
9468
|
-
warnUnknownKeys(perRepoConfig, globalConfig.path, KNOWN_CONFIG_KEYS, onWarning);
|
|
9469
|
-
}
|
|
9470
|
-
}
|
|
9471
|
-
if (localConfig.exists) {
|
|
9472
|
-
warnUnknownKeys(localConfig.config, localConfig.path, KNOWN_CONFIG_KEYS, onWarning);
|
|
9473
|
-
}
|
|
9474
|
-
}
|
|
9475
|
-
const merged = mergeConfig(globalBase, perRepoConfig, localConfig.config);
|
|
9476
|
-
const worktreePathValue = merged.worktreePath;
|
|
9477
|
-
if (onWarning && typeof worktreePathValue === "string" && worktreePathValue.length > 0 && !worktreePathValue.startsWith("/") && !worktreePathValue.startsWith("~")) {
|
|
9478
|
-
onWarning(
|
|
9479
|
-
`gji: "worktreePath" must be an absolute path or start with ~, got "${worktreePathValue}" \u2014 using default
|
|
9480
|
-
`
|
|
9481
|
-
);
|
|
9482
|
-
}
|
|
9483
|
-
const globalHooks = isPlainObject(globalBase.hooks) ? globalBase.hooks : {};
|
|
9484
|
-
const perRepoHooks = isPlainObject(perRepoConfig.hooks) ? perRepoConfig.hooks : {};
|
|
9485
|
-
const localHooks = isPlainObject(localConfig.config.hooks) ? localConfig.config.hooks : {};
|
|
9486
|
-
if (Object.keys(globalHooks).length > 0 || Object.keys(perRepoHooks).length > 0 || Object.keys(localHooks).length > 0) {
|
|
9487
|
-
merged.hooks = { ...globalHooks, ...perRepoHooks, ...localHooks };
|
|
9488
|
-
}
|
|
9489
|
-
return merged;
|
|
9490
|
-
}
|
|
9491
|
-
async function loadGlobalConfig(home = homedir()) {
|
|
9492
|
-
return loadConfigFile(GLOBAL_CONFIG_FILE_PATH(home));
|
|
9493
|
-
}
|
|
9494
|
-
async function saveLocalConfig(root, config) {
|
|
9495
|
-
const path9 = join(root, CONFIG_FILE_NAME);
|
|
9496
|
-
await writeFile(path9, `${JSON.stringify(config, null, 2)}
|
|
9497
|
-
`, "utf8");
|
|
9498
|
-
return path9;
|
|
9499
|
-
}
|
|
9500
|
-
async function updateLocalConfigKey(root, key, value) {
|
|
9501
|
-
const loaded = await loadConfig(root);
|
|
9502
|
-
const nextConfig = {
|
|
9503
|
-
...loaded.config,
|
|
9504
|
-
[key]: value
|
|
9505
|
-
};
|
|
9506
|
-
await saveLocalConfig(root, nextConfig);
|
|
9507
|
-
return nextConfig;
|
|
9508
|
-
}
|
|
9509
|
-
async function saveGlobalConfig(config, home = homedir()) {
|
|
9510
|
-
const path9 = GLOBAL_CONFIG_FILE_PATH(home);
|
|
9511
|
-
await mkdir(dirname(path9), { recursive: true });
|
|
9512
|
-
await writeFile(path9, `${JSON.stringify(config, null, 2)}
|
|
9513
|
-
`, "utf8");
|
|
9514
|
-
return path9;
|
|
9515
|
-
}
|
|
9516
|
-
async function unsetGlobalConfigKey(key, home = homedir()) {
|
|
9517
|
-
const loaded = await loadGlobalConfig(home);
|
|
9518
|
-
const nextConfig = { ...loaded.config };
|
|
9519
|
-
delete nextConfig[key];
|
|
9520
|
-
await saveGlobalConfig(nextConfig, home);
|
|
9521
|
-
return nextConfig;
|
|
9522
|
-
}
|
|
9523
|
-
async function updateGlobalConfigKey(key, value, home = homedir()) {
|
|
9524
|
-
const loaded = await loadGlobalConfig(home);
|
|
9525
|
-
const nextConfig = {
|
|
9526
|
-
...loaded.config,
|
|
9527
|
-
[key]: value
|
|
9528
|
-
};
|
|
9529
|
-
await saveGlobalConfig(nextConfig, home);
|
|
9530
|
-
return nextConfig;
|
|
9531
|
-
}
|
|
9532
|
-
async function updateGlobalRepoConfigKey(repoRoot, key, value, home = homedir()) {
|
|
9533
|
-
const loaded = await loadGlobalConfig(home);
|
|
9534
|
-
const repos = isPlainObject(loaded.config.repos) ? { ...loaded.config.repos } : {};
|
|
9535
|
-
const existing = isPlainObject(repos[repoRoot]) ? repos[repoRoot] : {};
|
|
9536
|
-
repos[repoRoot] = { ...existing, [key]: value };
|
|
9537
|
-
const nextConfig = { ...loaded.config, repos };
|
|
9538
|
-
await saveGlobalConfig(nextConfig, home);
|
|
9539
|
-
return nextConfig;
|
|
9540
|
-
}
|
|
9541
|
-
function GLOBAL_CONFIG_FILE_PATH(home = homedir()) {
|
|
9542
|
-
const configDir = process.env.GJI_CONFIG_DIR;
|
|
9543
|
-
if (configDir) {
|
|
9544
|
-
return join(resolve(configDir), GLOBAL_CONFIG_NAME);
|
|
9545
|
-
}
|
|
9546
|
-
return join(home, GLOBAL_CONFIG_DIRECTORY, GLOBAL_CONFIG_NAME);
|
|
9547
|
-
}
|
|
9548
|
-
function parseConfigValue(value) {
|
|
9549
|
-
try {
|
|
9550
|
-
return JSON.parse(value);
|
|
9551
|
-
} catch {
|
|
9552
|
-
return value;
|
|
9553
|
-
}
|
|
9554
|
-
}
|
|
9555
|
-
function resolveConfigString(config, key) {
|
|
9556
|
-
const value = config[key];
|
|
9557
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
9558
|
-
}
|
|
9559
|
-
async function loadConfigFile(path9) {
|
|
9560
|
-
try {
|
|
9561
|
-
const rawConfig = await readFile(path9, "utf8");
|
|
9562
|
-
const parsedConfig = JSON.parse(rawConfig);
|
|
9563
|
-
return {
|
|
9564
|
-
config: mergeConfig(parsedConfig),
|
|
9565
|
-
exists: true,
|
|
9566
|
-
path: path9
|
|
9567
|
-
};
|
|
9568
|
-
} catch (error) {
|
|
9569
|
-
if (isMissingFileError(error)) {
|
|
9570
|
-
return {
|
|
9571
|
-
config: DEFAULT_CONFIG,
|
|
9572
|
-
exists: false,
|
|
9573
|
-
path: path9
|
|
9574
|
-
};
|
|
9575
|
-
}
|
|
9576
|
-
throw error;
|
|
9577
|
-
}
|
|
9578
|
-
}
|
|
9579
|
-
function mergeConfig(...values) {
|
|
9580
|
-
return values.reduce(
|
|
9581
|
-
(config, value) => ({
|
|
9582
|
-
...config,
|
|
9583
|
-
...value
|
|
9584
|
-
}),
|
|
9585
|
-
{ ...DEFAULT_CONFIG }
|
|
9586
|
-
);
|
|
9587
|
-
}
|
|
9588
|
-
function findPerRepoConfig(repos, repoRoot, home) {
|
|
9589
|
-
for (const [key, value] of Object.entries(repos)) {
|
|
9590
|
-
const expandedKey = expandTilde(key, home);
|
|
9591
|
-
if (expandedKey === repoRoot && isPlainObject(value)) {
|
|
9592
|
-
return value;
|
|
9593
|
-
}
|
|
9594
|
-
}
|
|
9595
|
-
return {};
|
|
9596
|
-
}
|
|
9597
|
-
function expandTilde(value, home) {
|
|
9598
|
-
if (value === "~") return home;
|
|
9599
|
-
if (value.startsWith("~/")) return join(home, value.slice(2));
|
|
9600
|
-
return value;
|
|
9601
|
-
}
|
|
9602
|
-
function isPlainObject(value) {
|
|
9603
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9604
|
-
}
|
|
9605
|
-
function isMissingFileError(error) {
|
|
9606
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
9607
|
-
}
|
|
9608
|
-
function warnUnknownKeys(config, filePath, knownKeys, onWarning) {
|
|
9609
|
-
for (const key of Object.keys(config)) {
|
|
9610
|
-
if (!knownKeys.has(key)) {
|
|
9611
|
-
const suggestion = closestKey(key, knownKeys);
|
|
9612
|
-
const hint = suggestion ? ` (did you mean "${suggestion}"?)` : "";
|
|
9613
|
-
onWarning(`gji: unknown config key "${key}" in ${filePath}${hint}
|
|
9614
|
-
`);
|
|
9615
|
-
}
|
|
9616
|
-
}
|
|
9617
|
-
}
|
|
9618
|
-
function closestKey(unknown, knownKeys) {
|
|
9619
|
-
let best = null;
|
|
9620
|
-
let bestDist = Infinity;
|
|
9621
|
-
for (const key of knownKeys) {
|
|
9622
|
-
const dist = levenshtein(unknown, key);
|
|
9623
|
-
if (dist < bestDist) {
|
|
9624
|
-
bestDist = dist;
|
|
9625
|
-
best = key;
|
|
9626
|
-
}
|
|
9627
|
-
}
|
|
9628
|
-
return bestDist <= Math.max(2, Math.floor(unknown.length / 2)) ? best : null;
|
|
9629
|
-
}
|
|
9630
|
-
function levenshtein(a, b3) {
|
|
9631
|
-
const m2 = a.length;
|
|
9632
|
-
const n = b3.length;
|
|
9633
|
-
const dp = Array.from(
|
|
9634
|
-
{ length: m2 + 1 },
|
|
9635
|
-
(_3, i) => Array.from({ length: n + 1 }, (_4, j2) => i === 0 ? j2 : j2 === 0 ? i : 0)
|
|
9636
|
-
);
|
|
9637
|
-
for (let i = 1; i <= m2; i++) {
|
|
9638
|
-
for (let j2 = 1; j2 <= n; j2++) {
|
|
9639
|
-
dp[i][j2] = a[i - 1] === b3[j2 - 1] ? dp[i - 1][j2 - 1] : 1 + Math.min(dp[i - 1][j2], dp[i][j2 - 1], dp[i - 1][j2 - 1]);
|
|
9640
|
-
}
|
|
9641
|
-
}
|
|
9642
|
-
return dp[m2][n];
|
|
9643
|
-
}
|
|
9425
|
+
import { homedir as homedir7 } from "node:os";
|
|
9644
9426
|
|
|
9645
9427
|
// src/cli.ts
|
|
9646
9428
|
import { createRequire } from "node:module";
|
|
@@ -13032,20 +12814,293 @@ function updateNotifier(options) {
|
|
|
13032
12814
|
import { access } from "node:fs/promises";
|
|
13033
12815
|
import { basename as basename2 } from "node:path";
|
|
13034
12816
|
|
|
13035
|
-
// src/
|
|
13036
|
-
import {
|
|
13037
|
-
|
|
13038
|
-
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
12817
|
+
// src/config.ts
|
|
12818
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12819
|
+
import { homedir } from "node:os";
|
|
12820
|
+
import { dirname, join, resolve } from "node:path";
|
|
12821
|
+
var CONFIG_FILE_NAME = ".gji.json";
|
|
12822
|
+
var GLOBAL_CONFIG_DIRECTORY = ".config/gji";
|
|
12823
|
+
var GLOBAL_CONFIG_NAME = "config.json";
|
|
12824
|
+
var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
12825
|
+
"branchPrefix",
|
|
12826
|
+
"editor",
|
|
12827
|
+
"hooks",
|
|
12828
|
+
"installSaveTarget",
|
|
12829
|
+
"shellIntegration",
|
|
12830
|
+
"skipInstallPrompt",
|
|
12831
|
+
"syncDefaultBranch",
|
|
12832
|
+
"syncFiles",
|
|
12833
|
+
"syncRemote",
|
|
12834
|
+
"worktreePath"
|
|
12835
|
+
]);
|
|
12836
|
+
var KNOWN_GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
12837
|
+
...KNOWN_CONFIG_KEYS,
|
|
12838
|
+
"repos"
|
|
12839
|
+
]);
|
|
12840
|
+
var DEFAULT_CONFIG = Object.freeze({});
|
|
12841
|
+
async function loadConfig(root) {
|
|
12842
|
+
const path9 = join(root, CONFIG_FILE_NAME);
|
|
12843
|
+
return loadConfigFile(path9);
|
|
13044
12844
|
}
|
|
13045
|
-
async function
|
|
13046
|
-
const [
|
|
13047
|
-
|
|
13048
|
-
|
|
12845
|
+
async function loadEffectiveConfig(root, home = homedir(), onWarning) {
|
|
12846
|
+
const [globalConfig, localConfig] = await Promise.all([
|
|
12847
|
+
loadGlobalConfig(home),
|
|
12848
|
+
loadConfig(root)
|
|
12849
|
+
]);
|
|
12850
|
+
const repos = globalConfig.config.repos;
|
|
12851
|
+
const perRepoConfig = isPlainObject(repos) ? findPerRepoConfig(repos, root, home) : {};
|
|
12852
|
+
const globalBase = { ...globalConfig.config };
|
|
12853
|
+
delete globalBase.repos;
|
|
12854
|
+
if (onWarning) {
|
|
12855
|
+
if (globalConfig.exists) {
|
|
12856
|
+
warnUnknownKeys(
|
|
12857
|
+
globalBase,
|
|
12858
|
+
globalConfig.path,
|
|
12859
|
+
KNOWN_GLOBAL_CONFIG_KEYS,
|
|
12860
|
+
onWarning
|
|
12861
|
+
);
|
|
12862
|
+
if (Object.keys(perRepoConfig).length > 0) {
|
|
12863
|
+
warnUnknownKeys(
|
|
12864
|
+
perRepoConfig,
|
|
12865
|
+
globalConfig.path,
|
|
12866
|
+
KNOWN_CONFIG_KEYS,
|
|
12867
|
+
onWarning
|
|
12868
|
+
);
|
|
12869
|
+
}
|
|
12870
|
+
}
|
|
12871
|
+
if (localConfig.exists) {
|
|
12872
|
+
warnUnknownKeys(
|
|
12873
|
+
localConfig.config,
|
|
12874
|
+
localConfig.path,
|
|
12875
|
+
KNOWN_CONFIG_KEYS,
|
|
12876
|
+
onWarning
|
|
12877
|
+
);
|
|
12878
|
+
}
|
|
12879
|
+
}
|
|
12880
|
+
const merged = mergeConfig(globalBase, perRepoConfig, localConfig.config);
|
|
12881
|
+
const worktreePathValue = merged.worktreePath;
|
|
12882
|
+
if (onWarning && typeof worktreePathValue === "string" && worktreePathValue.length > 0 && !worktreePathValue.startsWith("/") && !worktreePathValue.startsWith("~")) {
|
|
12883
|
+
onWarning(
|
|
12884
|
+
`gji: "worktreePath" must be an absolute path or start with ~, got "${worktreePathValue}" \u2014 using default
|
|
12885
|
+
`
|
|
12886
|
+
);
|
|
12887
|
+
}
|
|
12888
|
+
const globalHooks = isPlainObject(globalBase.hooks) ? globalBase.hooks : {};
|
|
12889
|
+
const perRepoHooks = isPlainObject(perRepoConfig.hooks) ? perRepoConfig.hooks : {};
|
|
12890
|
+
const localHooks = isPlainObject(localConfig.config.hooks) ? localConfig.config.hooks : {};
|
|
12891
|
+
if (Object.keys(globalHooks).length > 0 || Object.keys(perRepoHooks).length > 0 || Object.keys(localHooks).length > 0) {
|
|
12892
|
+
merged.hooks = { ...globalHooks, ...perRepoHooks, ...localHooks };
|
|
12893
|
+
}
|
|
12894
|
+
return merged;
|
|
12895
|
+
}
|
|
12896
|
+
async function loadGlobalConfig(home = homedir()) {
|
|
12897
|
+
return loadConfigFile(GLOBAL_CONFIG_FILE_PATH(home));
|
|
12898
|
+
}
|
|
12899
|
+
async function saveLocalConfig(root, config) {
|
|
12900
|
+
const path9 = join(root, CONFIG_FILE_NAME);
|
|
12901
|
+
await writeFile(path9, `${JSON.stringify(config, null, 2)}
|
|
12902
|
+
`, "utf8");
|
|
12903
|
+
return path9;
|
|
12904
|
+
}
|
|
12905
|
+
async function updateLocalConfigKey(root, key, value) {
|
|
12906
|
+
const loaded = await loadConfig(root);
|
|
12907
|
+
const nextConfig = {
|
|
12908
|
+
...loaded.config,
|
|
12909
|
+
[key]: value
|
|
12910
|
+
};
|
|
12911
|
+
await saveLocalConfig(root, nextConfig);
|
|
12912
|
+
return nextConfig;
|
|
12913
|
+
}
|
|
12914
|
+
async function saveGlobalConfig(config, home = homedir()) {
|
|
12915
|
+
const path9 = GLOBAL_CONFIG_FILE_PATH(home);
|
|
12916
|
+
await mkdir(dirname(path9), { recursive: true });
|
|
12917
|
+
await writeFile(path9, `${JSON.stringify(config, null, 2)}
|
|
12918
|
+
`, "utf8");
|
|
12919
|
+
return path9;
|
|
12920
|
+
}
|
|
12921
|
+
async function unsetGlobalConfigKey(key, home = homedir()) {
|
|
12922
|
+
const loaded = await loadGlobalConfig(home);
|
|
12923
|
+
const nextConfig = { ...loaded.config };
|
|
12924
|
+
delete nextConfig[key];
|
|
12925
|
+
await saveGlobalConfig(nextConfig, home);
|
|
12926
|
+
return nextConfig;
|
|
12927
|
+
}
|
|
12928
|
+
async function updateGlobalConfigKey(key, value, home = homedir()) {
|
|
12929
|
+
const loaded = await loadGlobalConfig(home);
|
|
12930
|
+
const nextConfig = {
|
|
12931
|
+
...loaded.config,
|
|
12932
|
+
[key]: value
|
|
12933
|
+
};
|
|
12934
|
+
await saveGlobalConfig(nextConfig, home);
|
|
12935
|
+
return nextConfig;
|
|
12936
|
+
}
|
|
12937
|
+
async function updateGlobalRepoConfigKey(repoRoot, key, value, home = homedir()) {
|
|
12938
|
+
const loaded = await loadGlobalConfig(home);
|
|
12939
|
+
const repos = isPlainObject(loaded.config.repos) ? { ...loaded.config.repos } : {};
|
|
12940
|
+
const existing = isPlainObject(repos[repoRoot]) ? repos[repoRoot] : {};
|
|
12941
|
+
repos[repoRoot] = { ...existing, [key]: value };
|
|
12942
|
+
const nextConfig = { ...loaded.config, repos };
|
|
12943
|
+
await saveGlobalConfig(nextConfig, home);
|
|
12944
|
+
return nextConfig;
|
|
12945
|
+
}
|
|
12946
|
+
function GLOBAL_CONFIG_FILE_PATH(home = homedir()) {
|
|
12947
|
+
const configDir = process.env.GJI_CONFIG_DIR;
|
|
12948
|
+
if (configDir) {
|
|
12949
|
+
return join(resolve(configDir), GLOBAL_CONFIG_NAME);
|
|
12950
|
+
}
|
|
12951
|
+
return join(home, GLOBAL_CONFIG_DIRECTORY, GLOBAL_CONFIG_NAME);
|
|
12952
|
+
}
|
|
12953
|
+
function parseConfigValue(value) {
|
|
12954
|
+
try {
|
|
12955
|
+
return JSON.parse(value);
|
|
12956
|
+
} catch {
|
|
12957
|
+
return value;
|
|
12958
|
+
}
|
|
12959
|
+
}
|
|
12960
|
+
function resolveConfigString(config, key) {
|
|
12961
|
+
const value = config[key];
|
|
12962
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
12963
|
+
}
|
|
12964
|
+
async function loadConfigFile(path9) {
|
|
12965
|
+
try {
|
|
12966
|
+
const rawConfig = await readFile(path9, "utf8");
|
|
12967
|
+
const parsedConfig = JSON.parse(rawConfig);
|
|
12968
|
+
return {
|
|
12969
|
+
config: mergeConfig(parsedConfig),
|
|
12970
|
+
exists: true,
|
|
12971
|
+
path: path9
|
|
12972
|
+
};
|
|
12973
|
+
} catch (error) {
|
|
12974
|
+
if (isMissingFileError(error)) {
|
|
12975
|
+
return {
|
|
12976
|
+
config: DEFAULT_CONFIG,
|
|
12977
|
+
exists: false,
|
|
12978
|
+
path: path9
|
|
12979
|
+
};
|
|
12980
|
+
}
|
|
12981
|
+
throw error;
|
|
12982
|
+
}
|
|
12983
|
+
}
|
|
12984
|
+
function mergeConfig(...values) {
|
|
12985
|
+
return values.reduce(
|
|
12986
|
+
(config, value) => ({
|
|
12987
|
+
...config,
|
|
12988
|
+
...value
|
|
12989
|
+
}),
|
|
12990
|
+
{ ...DEFAULT_CONFIG }
|
|
12991
|
+
);
|
|
12992
|
+
}
|
|
12993
|
+
function findPerRepoConfig(repos, repoRoot, home) {
|
|
12994
|
+
for (const [key, value] of Object.entries(repos)) {
|
|
12995
|
+
const expandedKey = expandTilde(key, home);
|
|
12996
|
+
if (expandedKey === repoRoot && isPlainObject(value)) {
|
|
12997
|
+
return value;
|
|
12998
|
+
}
|
|
12999
|
+
}
|
|
13000
|
+
return {};
|
|
13001
|
+
}
|
|
13002
|
+
function expandTilde(value, home) {
|
|
13003
|
+
if (value === "~") return home;
|
|
13004
|
+
if (value.startsWith("~/")) return join(home, value.slice(2));
|
|
13005
|
+
return value;
|
|
13006
|
+
}
|
|
13007
|
+
function isPlainObject(value) {
|
|
13008
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13009
|
+
}
|
|
13010
|
+
function isMissingFileError(error) {
|
|
13011
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
13012
|
+
}
|
|
13013
|
+
function warnUnknownKeys(config, filePath, knownKeys, onWarning) {
|
|
13014
|
+
for (const key of Object.keys(config)) {
|
|
13015
|
+
if (!knownKeys.has(key)) {
|
|
13016
|
+
const suggestion = closestKey(key, knownKeys);
|
|
13017
|
+
const hint = suggestion ? ` (did you mean "${suggestion}"?)` : "";
|
|
13018
|
+
onWarning(`gji: unknown config key "${key}" in ${filePath}${hint}
|
|
13019
|
+
`);
|
|
13020
|
+
}
|
|
13021
|
+
}
|
|
13022
|
+
}
|
|
13023
|
+
function closestKey(unknown, knownKeys) {
|
|
13024
|
+
let best = null;
|
|
13025
|
+
let bestDist = Infinity;
|
|
13026
|
+
for (const key of knownKeys) {
|
|
13027
|
+
const dist = levenshtein(unknown, key);
|
|
13028
|
+
if (dist < bestDist) {
|
|
13029
|
+
bestDist = dist;
|
|
13030
|
+
best = key;
|
|
13031
|
+
}
|
|
13032
|
+
}
|
|
13033
|
+
return bestDist <= Math.max(2, Math.floor(unknown.length / 2)) ? best : null;
|
|
13034
|
+
}
|
|
13035
|
+
function levenshtein(a, b3) {
|
|
13036
|
+
const m2 = a.length;
|
|
13037
|
+
const n = b3.length;
|
|
13038
|
+
const dp = Array.from(
|
|
13039
|
+
{ length: m2 + 1 },
|
|
13040
|
+
(_3, i) => Array.from({ length: n + 1 }, (_4, j2) => i === 0 ? j2 : j2 === 0 ? i : 0)
|
|
13041
|
+
);
|
|
13042
|
+
for (let i = 1; i <= m2; i++) {
|
|
13043
|
+
for (let j2 = 1; j2 <= n; j2++) {
|
|
13044
|
+
dp[i][j2] = a[i - 1] === b3[j2 - 1] ? dp[i - 1][j2 - 1] : 1 + Math.min(dp[i - 1][j2], dp[i][j2 - 1], dp[i - 1][j2 - 1]);
|
|
13045
|
+
}
|
|
13046
|
+
}
|
|
13047
|
+
return dp[m2][n];
|
|
13048
|
+
}
|
|
13049
|
+
|
|
13050
|
+
// src/history.ts
|
|
13051
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
13052
|
+
import { homedir as homedir2 } from "node:os";
|
|
13053
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
|
|
13054
|
+
var HISTORY_FILE_NAME = "history.json";
|
|
13055
|
+
var MAX_HISTORY_ENTRIES = 50;
|
|
13056
|
+
function HISTORY_FILE_PATH(home = homedir2()) {
|
|
13057
|
+
const configDir = process.env.GJI_CONFIG_DIR;
|
|
13058
|
+
if (configDir) {
|
|
13059
|
+
return join2(resolve2(configDir), HISTORY_FILE_NAME);
|
|
13060
|
+
}
|
|
13061
|
+
return join2(home, GLOBAL_CONFIG_DIRECTORY, HISTORY_FILE_NAME);
|
|
13062
|
+
}
|
|
13063
|
+
async function loadHistory(home = homedir2()) {
|
|
13064
|
+
const path9 = HISTORY_FILE_PATH(home);
|
|
13065
|
+
try {
|
|
13066
|
+
const raw = await readFile2(path9, "utf8");
|
|
13067
|
+
const parsed = JSON.parse(raw);
|
|
13068
|
+
if (!Array.isArray(parsed)) return [];
|
|
13069
|
+
return parsed.filter(isHistoryEntry);
|
|
13070
|
+
} catch {
|
|
13071
|
+
return [];
|
|
13072
|
+
}
|
|
13073
|
+
}
|
|
13074
|
+
async function appendHistory(path9, branch, home = homedir2()) {
|
|
13075
|
+
const historyPath = HISTORY_FILE_PATH(home);
|
|
13076
|
+
const existing = await loadHistory(home);
|
|
13077
|
+
if (existing.length > 0 && existing[0].path === path9) {
|
|
13078
|
+
return;
|
|
13079
|
+
}
|
|
13080
|
+
const entry = { branch, path: path9, timestamp: Date.now() };
|
|
13081
|
+
const next = [entry, ...existing].slice(0, MAX_HISTORY_ENTRIES);
|
|
13082
|
+
await mkdir2(dirname2(historyPath), { recursive: true });
|
|
13083
|
+
await writeFile2(historyPath, `${JSON.stringify(next, null, 2)}
|
|
13084
|
+
`, "utf8");
|
|
13085
|
+
}
|
|
13086
|
+
function isHistoryEntry(value) {
|
|
13087
|
+
return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string" && "timestamp" in value && typeof value.timestamp === "number";
|
|
13088
|
+
}
|
|
13089
|
+
|
|
13090
|
+
// src/hooks.ts
|
|
13091
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
13092
|
+
async function runHook(hookCmd, cwd, context, stderr) {
|
|
13093
|
+
if (!hookCmd) return;
|
|
13094
|
+
if (Array.isArray(hookCmd)) {
|
|
13095
|
+
await runArgvHook(hookCmd, cwd, context, stderr);
|
|
13096
|
+
return;
|
|
13097
|
+
}
|
|
13098
|
+
await runShellHook(hookCmd, cwd, context, stderr);
|
|
13099
|
+
}
|
|
13100
|
+
async function runArgvHook(hookCmd, cwd, context, stderr) {
|
|
13101
|
+
const [command, ...args] = hookCmd.map((arg) => interpolate(arg, context));
|
|
13102
|
+
if (!command) {
|
|
13103
|
+
stderr("gji: hook argv command must include a non-empty command\n");
|
|
13049
13104
|
return;
|
|
13050
13105
|
}
|
|
13051
13106
|
await new Promise((resolve6) => {
|
|
@@ -13060,8 +13115,10 @@ async function runArgvHook(hookCmd, cwd, context, stderr) {
|
|
|
13060
13115
|
});
|
|
13061
13116
|
child.on("close", (code) => {
|
|
13062
13117
|
if (code !== 0) {
|
|
13063
|
-
stderr(
|
|
13064
|
-
`)
|
|
13118
|
+
stderr(
|
|
13119
|
+
`gji: hook exited with code ${code}: ${formatArgvHook(command, args)}
|
|
13120
|
+
`
|
|
13121
|
+
);
|
|
13065
13122
|
}
|
|
13066
13123
|
resolve6();
|
|
13067
13124
|
});
|
|
@@ -13119,9 +13176,13 @@ function extractHooks(config) {
|
|
|
13119
13176
|
}
|
|
13120
13177
|
const hooks = raw;
|
|
13121
13178
|
return {
|
|
13122
|
-
|
|
13123
|
-
|
|
13124
|
-
|
|
13179
|
+
"after-create": parseHookCommand(
|
|
13180
|
+
hooks["after-create"] ?? hooks.afterCreate
|
|
13181
|
+
),
|
|
13182
|
+
"after-enter": parseHookCommand(hooks["after-enter"] ?? hooks.afterEnter),
|
|
13183
|
+
"before-remove": parseHookCommand(
|
|
13184
|
+
hooks["before-remove"] ?? hooks.beforeRemove
|
|
13185
|
+
)
|
|
13125
13186
|
};
|
|
13126
13187
|
}
|
|
13127
13188
|
function parseHookCommand(value) {
|
|
@@ -13132,49 +13193,9 @@ function parseHookCommand(value) {
|
|
|
13132
13193
|
return void 0;
|
|
13133
13194
|
}
|
|
13134
13195
|
|
|
13135
|
-
// src/history.ts
|
|
13136
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
13137
|
-
import { homedir as homedir2 } from "node:os";
|
|
13138
|
-
import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
|
|
13139
|
-
var HISTORY_FILE_NAME = "history.json";
|
|
13140
|
-
var MAX_HISTORY_ENTRIES = 50;
|
|
13141
|
-
function HISTORY_FILE_PATH(home = homedir2()) {
|
|
13142
|
-
const configDir = process.env.GJI_CONFIG_DIR;
|
|
13143
|
-
if (configDir) {
|
|
13144
|
-
return join2(resolve2(configDir), HISTORY_FILE_NAME);
|
|
13145
|
-
}
|
|
13146
|
-
return join2(home, GLOBAL_CONFIG_DIRECTORY, HISTORY_FILE_NAME);
|
|
13147
|
-
}
|
|
13148
|
-
async function loadHistory(home = homedir2()) {
|
|
13149
|
-
const path9 = HISTORY_FILE_PATH(home);
|
|
13150
|
-
try {
|
|
13151
|
-
const raw = await readFile2(path9, "utf8");
|
|
13152
|
-
const parsed = JSON.parse(raw);
|
|
13153
|
-
if (!Array.isArray(parsed)) return [];
|
|
13154
|
-
return parsed.filter(isHistoryEntry);
|
|
13155
|
-
} catch {
|
|
13156
|
-
return [];
|
|
13157
|
-
}
|
|
13158
|
-
}
|
|
13159
|
-
async function appendHistory(path9, branch, home = homedir2()) {
|
|
13160
|
-
const historyPath = HISTORY_FILE_PATH(home);
|
|
13161
|
-
const existing = await loadHistory(home);
|
|
13162
|
-
if (existing.length > 0 && existing[0].path === path9) {
|
|
13163
|
-
return;
|
|
13164
|
-
}
|
|
13165
|
-
const entry = { branch, path: path9, timestamp: Date.now() };
|
|
13166
|
-
const next = [entry, ...existing].slice(0, MAX_HISTORY_ENTRIES);
|
|
13167
|
-
await mkdir2(dirname2(historyPath), { recursive: true });
|
|
13168
|
-
await writeFile2(historyPath, `${JSON.stringify(next, null, 2)}
|
|
13169
|
-
`, "utf8");
|
|
13170
|
-
}
|
|
13171
|
-
function isHistoryEntry(value) {
|
|
13172
|
-
return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string" && "timestamp" in value && typeof value.timestamp === "number";
|
|
13173
|
-
}
|
|
13174
|
-
|
|
13175
13196
|
// src/repo.ts
|
|
13176
|
-
import { basename, dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
|
|
13177
13197
|
import { homedir as homedir3 } from "node:os";
|
|
13198
|
+
import { basename, dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
|
|
13178
13199
|
|
|
13179
13200
|
// src/git.ts
|
|
13180
13201
|
import { execFile } from "node:child_process";
|
|
@@ -13190,7 +13211,11 @@ async function runGit(cwd, args) {
|
|
|
13190
13211
|
}
|
|
13191
13212
|
}
|
|
13192
13213
|
async function readWorktreeHealth(cwd) {
|
|
13193
|
-
const { stdout } = await execFileAsync(
|
|
13214
|
+
const { stdout } = await execFileAsync(
|
|
13215
|
+
"git",
|
|
13216
|
+
["status", "--porcelain=v2", "--branch"],
|
|
13217
|
+
{ cwd }
|
|
13218
|
+
);
|
|
13194
13219
|
return parseWorktreeHealth(stdout);
|
|
13195
13220
|
}
|
|
13196
13221
|
async function isDirtyWorktree(cwd) {
|
|
@@ -13199,7 +13224,9 @@ async function isDirtyWorktree(cwd) {
|
|
|
13199
13224
|
}
|
|
13200
13225
|
async function isBranchMergedInto(cwd, branch, base = "HEAD") {
|
|
13201
13226
|
try {
|
|
13202
|
-
await execFileAsync("git", ["merge-base", "--is-ancestor", branch, base], {
|
|
13227
|
+
await execFileAsync("git", ["merge-base", "--is-ancestor", branch, base], {
|
|
13228
|
+
cwd
|
|
13229
|
+
});
|
|
13203
13230
|
return true;
|
|
13204
13231
|
} catch (error) {
|
|
13205
13232
|
if (hasExitCode(error, 1)) {
|
|
@@ -13209,7 +13236,11 @@ async function isBranchMergedInto(cwd, branch, base = "HEAD") {
|
|
|
13209
13236
|
}
|
|
13210
13237
|
}
|
|
13211
13238
|
async function resolveRemoteDefaultBranch(cwd, remote) {
|
|
13212
|
-
const { stdout } = await execFileAsync(
|
|
13239
|
+
const { stdout } = await execFileAsync(
|
|
13240
|
+
"git",
|
|
13241
|
+
["ls-remote", "--symref", remote, "HEAD"],
|
|
13242
|
+
{ cwd }
|
|
13243
|
+
);
|
|
13213
13244
|
const refLine = stdout.split("\n").find((line) => line.startsWith("ref: refs/heads/"));
|
|
13214
13245
|
if (!refLine) {
|
|
13215
13246
|
return null;
|
|
@@ -13219,7 +13250,11 @@ async function resolveRemoteDefaultBranch(cwd, remote) {
|
|
|
13219
13250
|
}
|
|
13220
13251
|
async function readBranchLastCommitTimestamp(cwd, branch) {
|
|
13221
13252
|
try {
|
|
13222
|
-
const { stdout } = await execFileAsync(
|
|
13253
|
+
const { stdout } = await execFileAsync(
|
|
13254
|
+
"git",
|
|
13255
|
+
["log", "-1", "--format=%ct", branch],
|
|
13256
|
+
{ cwd }
|
|
13257
|
+
);
|
|
13223
13258
|
const timestamp = Number(stdout.trim());
|
|
13224
13259
|
return Number.isFinite(timestamp) ? timestamp : null;
|
|
13225
13260
|
} catch {
|
|
@@ -13283,7 +13318,9 @@ function resolveWorktreePath(repoRoot, branch, basePath) {
|
|
|
13283
13318
|
throw new Error("Branch name must not be empty.");
|
|
13284
13319
|
}
|
|
13285
13320
|
if (segments.some((segment) => segment === "." || segment === "..")) {
|
|
13286
|
-
throw new Error(
|
|
13321
|
+
throw new Error(
|
|
13322
|
+
`Branch name '${branch}' contains an invalid path segment.`
|
|
13323
|
+
);
|
|
13287
13324
|
}
|
|
13288
13325
|
const base = basePath ? expandTildeInPath(basePath) : join3(dirname3(repoRoot), "worktrees", basename(repoRoot));
|
|
13289
13326
|
return join3(base, ...segments);
|
|
@@ -13403,17 +13440,27 @@ async function runBackCommand(options) {
|
|
|
13403
13440
|
}
|
|
13404
13441
|
if (!target) {
|
|
13405
13442
|
options.stderr("gji back: no previous worktree in history\n");
|
|
13406
|
-
options.stderr(
|
|
13443
|
+
options.stderr(
|
|
13444
|
+
"Hint: Use 'gji go', 'gji new', or 'gji pr' to navigate between worktrees\n"
|
|
13445
|
+
);
|
|
13407
13446
|
return 1;
|
|
13408
13447
|
}
|
|
13409
13448
|
try {
|
|
13410
13449
|
const repository = await detectRepository(target.path);
|
|
13411
|
-
const config = await loadEffectiveConfig(
|
|
13450
|
+
const config = await loadEffectiveConfig(
|
|
13451
|
+
repository.repoRoot,
|
|
13452
|
+
options.home,
|
|
13453
|
+
options.stderr
|
|
13454
|
+
);
|
|
13412
13455
|
const hooks = extractHooks(config);
|
|
13413
13456
|
await runHook(
|
|
13414
|
-
hooks
|
|
13457
|
+
hooks["after-enter"],
|
|
13415
13458
|
target.path,
|
|
13416
|
-
{
|
|
13459
|
+
{
|
|
13460
|
+
branch: target.branch ?? void 0,
|
|
13461
|
+
path: target.path,
|
|
13462
|
+
repo: basename2(repository.repoRoot)
|
|
13463
|
+
},
|
|
13417
13464
|
options.stderr
|
|
13418
13465
|
);
|
|
13419
13466
|
} catch {
|
|
@@ -13427,7 +13474,9 @@ function formatHistoryList(history, cwd) {
|
|
|
13427
13474
|
"BRANCH".length,
|
|
13428
13475
|
...history.map((e2) => (e2.branch ?? "(detached)").length)
|
|
13429
13476
|
);
|
|
13430
|
-
const lines = [
|
|
13477
|
+
const lines = [
|
|
13478
|
+
" " + "BRANCH".padEnd(branchWidth) + " WHEN PATH"
|
|
13479
|
+
];
|
|
13431
13480
|
for (const entry of history) {
|
|
13432
13481
|
const isCurrent = entry.path === cwd;
|
|
13433
13482
|
const branch = (entry.branch ?? "(detached)").padEnd(branchWidth);
|
|
@@ -14121,7 +14170,10 @@ function formatLastCommit(timestampSeconds) {
|
|
|
14121
14170
|
return timestampSeconds === null ? "n/a" : formatRelativeAge(timestampSeconds);
|
|
14122
14171
|
}
|
|
14123
14172
|
function formatRelativeAge(timestampSeconds) {
|
|
14124
|
-
const ageSeconds = Math.max(
|
|
14173
|
+
const ageSeconds = Math.max(
|
|
14174
|
+
0,
|
|
14175
|
+
Math.floor(Date.now() / 1e3) - timestampSeconds
|
|
14176
|
+
);
|
|
14125
14177
|
const units = [
|
|
14126
14178
|
{ label: "y", seconds: 365 * 24 * 60 * 60 },
|
|
14127
14179
|
{ label: "mo", seconds: 30 * 24 * 60 * 60 },
|
|
@@ -14154,16 +14206,28 @@ async function loadLinkedWorktrees(cwd) {
|
|
|
14154
14206
|
};
|
|
14155
14207
|
}
|
|
14156
14208
|
async function removeWorktree(repoRoot, worktreePath) {
|
|
14157
|
-
await execFileAsync2("git", ["worktree", "remove", worktreePath], {
|
|
14209
|
+
await execFileAsync2("git", ["worktree", "remove", worktreePath], {
|
|
14210
|
+
cwd: repoRoot,
|
|
14211
|
+
env: GIT_ENV
|
|
14212
|
+
});
|
|
14158
14213
|
}
|
|
14159
14214
|
async function forceRemoveWorktree(repoRoot, worktreePath) {
|
|
14160
|
-
await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], {
|
|
14215
|
+
await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], {
|
|
14216
|
+
cwd: repoRoot,
|
|
14217
|
+
env: GIT_ENV
|
|
14218
|
+
});
|
|
14161
14219
|
}
|
|
14162
14220
|
async function deleteBranch(repoRoot, branch) {
|
|
14163
|
-
await execFileAsync2("git", ["branch", "-d", branch], {
|
|
14221
|
+
await execFileAsync2("git", ["branch", "-d", branch], {
|
|
14222
|
+
cwd: repoRoot,
|
|
14223
|
+
env: GIT_ENV
|
|
14224
|
+
});
|
|
14164
14225
|
}
|
|
14165
14226
|
async function forceDeleteBranch(repoRoot, branch) {
|
|
14166
|
-
await execFileAsync2("git", ["branch", "-D", branch], {
|
|
14227
|
+
await execFileAsync2("git", ["branch", "-D", branch], {
|
|
14228
|
+
cwd: repoRoot,
|
|
14229
|
+
env: GIT_ENV
|
|
14230
|
+
});
|
|
14167
14231
|
}
|
|
14168
14232
|
function isWorktreeDirtyError(error) {
|
|
14169
14233
|
return hasStderr(error) && error.stderr.includes("contains modified or untracked files");
|
|
@@ -14202,12 +14266,18 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14202
14266
|
const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
|
|
14203
14267
|
const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
|
|
14204
14268
|
return async function runCleanCommand2(options) {
|
|
14205
|
-
const { linkedWorktrees, repository } = await loadLinkedWorktrees(
|
|
14269
|
+
const { linkedWorktrees, repository } = await loadLinkedWorktrees(
|
|
14270
|
+
options.cwd
|
|
14271
|
+
);
|
|
14206
14272
|
const linkedCleanupCandidates = linkedWorktrees.filter(
|
|
14207
14273
|
(worktree) => worktree.path !== repository.currentRoot
|
|
14208
14274
|
);
|
|
14209
14275
|
const staleBaseRef = options.stale ? await resolveStaleBaseRef(repository.repoRoot, options.stderr) : null;
|
|
14210
|
-
const cleanupCandidates = options.stale ? await filterStaleCleanupCandidates(
|
|
14276
|
+
const cleanupCandidates = options.stale ? await filterStaleCleanupCandidates(
|
|
14277
|
+
repository.repoRoot,
|
|
14278
|
+
linkedCleanupCandidates,
|
|
14279
|
+
staleBaseRef
|
|
14280
|
+
) : linkedCleanupCandidates;
|
|
14211
14281
|
if (cleanupCandidates.length === 0) {
|
|
14212
14282
|
if (options.stale) {
|
|
14213
14283
|
emitNoStaleCandidates(options);
|
|
@@ -14221,8 +14291,10 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14221
14291
|
if (options.json) {
|
|
14222
14292
|
emitError(options, message);
|
|
14223
14293
|
} else {
|
|
14224
|
-
options.stderr(
|
|
14225
|
-
`)
|
|
14294
|
+
options.stderr(
|
|
14295
|
+
`gji clean: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
14296
|
+
`
|
|
14297
|
+
);
|
|
14226
14298
|
}
|
|
14227
14299
|
return 1;
|
|
14228
14300
|
}
|
|
@@ -14232,7 +14304,10 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14232
14304
|
options.stderr("Aborted\n");
|
|
14233
14305
|
return 1;
|
|
14234
14306
|
}
|
|
14235
|
-
const selectedWorktrees = resolveSelectedWorktrees(
|
|
14307
|
+
const selectedWorktrees = resolveSelectedWorktrees(
|
|
14308
|
+
cleanupCandidates,
|
|
14309
|
+
selections
|
|
14310
|
+
);
|
|
14236
14311
|
if (selectedWorktrees.length !== selections.length) {
|
|
14237
14312
|
options.stderr("Selected worktree no longer exists\n");
|
|
14238
14313
|
return 1;
|
|
@@ -14247,13 +14322,19 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14247
14322
|
}
|
|
14248
14323
|
if (options.dryRun) {
|
|
14249
14324
|
if (options.json) {
|
|
14250
|
-
const removed = selectedWorktreeInfos.map(
|
|
14251
|
-
|
|
14252
|
-
|
|
14325
|
+
const removed = selectedWorktreeInfos.map(
|
|
14326
|
+
(info) => serializeWorktreeInfo(info)
|
|
14327
|
+
);
|
|
14328
|
+
options.stdout(
|
|
14329
|
+
`${JSON.stringify({ removed, dryRun: true }, null, 2)}
|
|
14330
|
+
`
|
|
14331
|
+
);
|
|
14253
14332
|
} else {
|
|
14254
14333
|
for (const info of selectedWorktreeInfos) {
|
|
14255
|
-
options.stdout(
|
|
14256
|
-
`)
|
|
14334
|
+
options.stdout(
|
|
14335
|
+
`Would remove worktree at ${info.path} (${formatCleanInfo(info)})
|
|
14336
|
+
`
|
|
14337
|
+
);
|
|
14257
14338
|
}
|
|
14258
14339
|
}
|
|
14259
14340
|
return 0;
|
|
@@ -14261,9 +14342,15 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14261
14342
|
const removedPaths = [];
|
|
14262
14343
|
const removedWorktrees = [];
|
|
14263
14344
|
for (const worktree of selectedWorktrees) {
|
|
14264
|
-
if (options.stale && !await isStaleCleanupCandidate(
|
|
14265
|
-
|
|
14266
|
-
|
|
14345
|
+
if (options.stale && !await isStaleCleanupCandidate(
|
|
14346
|
+
repository.repoRoot,
|
|
14347
|
+
worktree,
|
|
14348
|
+
staleBaseRef
|
|
14349
|
+
)) {
|
|
14350
|
+
options.stderr(
|
|
14351
|
+
`Skipped ${worktree.path}: no longer a safe stale cleanup candidate
|
|
14352
|
+
`
|
|
14353
|
+
);
|
|
14267
14354
|
continue;
|
|
14268
14355
|
}
|
|
14269
14356
|
try {
|
|
@@ -14273,8 +14360,10 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14273
14360
|
throw error;
|
|
14274
14361
|
}
|
|
14275
14362
|
if (options.stale) {
|
|
14276
|
-
options.stderr(
|
|
14277
|
-
`
|
|
14363
|
+
options.stderr(
|
|
14364
|
+
`Skipped ${worktree.path}: no longer a safe stale cleanup candidate
|
|
14365
|
+
`
|
|
14366
|
+
);
|
|
14278
14367
|
continue;
|
|
14279
14368
|
}
|
|
14280
14369
|
if (!options.force && !await confirmForceRemoveWorktree(worktree.path)) {
|
|
@@ -14288,7 +14377,10 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14288
14377
|
if (!options.json) {
|
|
14289
14378
|
reportRemovedPaths(removedPaths, options.stderr);
|
|
14290
14379
|
}
|
|
14291
|
-
emitError(
|
|
14380
|
+
emitError(
|
|
14381
|
+
options,
|
|
14382
|
+
`Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}`
|
|
14383
|
+
);
|
|
14292
14384
|
return 1;
|
|
14293
14385
|
}
|
|
14294
14386
|
}
|
|
@@ -14305,12 +14397,16 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14305
14397
|
try {
|
|
14306
14398
|
await forceDeleteBranch(repository.repoRoot, worktree.branch);
|
|
14307
14399
|
} catch (forceError) {
|
|
14308
|
-
options.stderr(
|
|
14309
|
-
`)
|
|
14400
|
+
options.stderr(
|
|
14401
|
+
`Failed to delete branch ${worktree.branch}: ${toMessage(forceError)}
|
|
14402
|
+
`
|
|
14403
|
+
);
|
|
14310
14404
|
}
|
|
14311
14405
|
} else {
|
|
14312
|
-
options.stderr(
|
|
14313
|
-
`)
|
|
14406
|
+
options.stderr(
|
|
14407
|
+
`Branch ${worktree.branch} was not deleted (has unmerged commits)
|
|
14408
|
+
`
|
|
14409
|
+
);
|
|
14314
14410
|
}
|
|
14315
14411
|
}
|
|
14316
14412
|
}
|
|
@@ -14335,19 +14431,30 @@ async function filterStaleCleanupCandidates(repoRoot, worktrees, baseBranch) {
|
|
|
14335
14431
|
return [];
|
|
14336
14432
|
}
|
|
14337
14433
|
const results = await Promise.all(
|
|
14338
|
-
worktrees.map(
|
|
14434
|
+
worktrees.map(
|
|
14435
|
+
(worktree) => isStaleCleanupCandidate(repoRoot, worktree, baseBranch)
|
|
14436
|
+
)
|
|
14339
14437
|
);
|
|
14340
14438
|
return worktrees.filter((_3, index) => results[index]);
|
|
14341
14439
|
}
|
|
14342
14440
|
async function resolveStaleBaseRef(repoRoot, stderr) {
|
|
14343
14441
|
const config = await loadEffectiveConfig(repoRoot, void 0, stderr);
|
|
14344
14442
|
const remote = resolveConfiguredString(config.syncRemote) ?? "origin";
|
|
14345
|
-
const configuredDefaultBranch = resolveConfiguredString(
|
|
14443
|
+
const configuredDefaultBranch = resolveConfiguredString(
|
|
14444
|
+
config.syncDefaultBranch
|
|
14445
|
+
);
|
|
14346
14446
|
if (configuredDefaultBranch) {
|
|
14347
|
-
return await resolveFetchedRemoteRef(
|
|
14447
|
+
return await resolveFetchedRemoteRef(
|
|
14448
|
+
repoRoot,
|
|
14449
|
+
remote,
|
|
14450
|
+
configuredDefaultBranch
|
|
14451
|
+
);
|
|
14348
14452
|
}
|
|
14349
14453
|
try {
|
|
14350
|
-
const remoteDefaultBranch = await resolveRemoteDefaultBranch(
|
|
14454
|
+
const remoteDefaultBranch = await resolveRemoteDefaultBranch(
|
|
14455
|
+
repoRoot,
|
|
14456
|
+
remote
|
|
14457
|
+
);
|
|
14351
14458
|
return remoteDefaultBranch === null ? null : await resolveFetchedRemoteRef(repoRoot, remote, remoteDefaultBranch);
|
|
14352
14459
|
} catch {
|
|
14353
14460
|
return null;
|
|
@@ -14442,14 +14549,22 @@ async function defaultPromptForWorktrees(worktrees) {
|
|
|
14442
14549
|
return pD(choice) ? null : choice;
|
|
14443
14550
|
}
|
|
14444
14551
|
async function defaultConfirmRemoval(worktrees) {
|
|
14445
|
-
const branchCount = worktrees.filter(
|
|
14552
|
+
const branchCount = worktrees.filter(
|
|
14553
|
+
(worktree) => worktree.branch !== null
|
|
14554
|
+
).length;
|
|
14446
14555
|
const detachedCount = worktrees.length - branchCount;
|
|
14447
|
-
const messageParts = [
|
|
14556
|
+
const messageParts = [
|
|
14557
|
+
`Remove ${worktrees.length} linked worktree${worktrees.length === 1 ? "" : "s"}`
|
|
14558
|
+
];
|
|
14448
14559
|
if (branchCount > 0) {
|
|
14449
|
-
messageParts.push(
|
|
14560
|
+
messageParts.push(
|
|
14561
|
+
`delete ${branchCount} branch${branchCount === 1 ? "" : "es"}`
|
|
14562
|
+
);
|
|
14450
14563
|
}
|
|
14451
14564
|
if (detachedCount > 0) {
|
|
14452
|
-
messageParts.push(
|
|
14565
|
+
messageParts.push(
|
|
14566
|
+
`remove ${detachedCount} detached worktree${detachedCount === 1 ? "" : "s"}`
|
|
14567
|
+
);
|
|
14453
14568
|
}
|
|
14454
14569
|
const choice = await ye({
|
|
14455
14570
|
active: "Yes",
|
|
@@ -14460,25 +14575,35 @@ async function defaultConfirmRemoval(worktrees) {
|
|
|
14460
14575
|
return !pD(choice) && choice;
|
|
14461
14576
|
}
|
|
14462
14577
|
|
|
14463
|
-
// src/
|
|
14464
|
-
|
|
14465
|
-
const
|
|
14466
|
-
if (
|
|
14467
|
-
|
|
14468
|
-
`);
|
|
14469
|
-
return 0;
|
|
14578
|
+
// src/shell.ts
|
|
14579
|
+
function resolveSupportedShell(requestedShell, detectedShell) {
|
|
14580
|
+
const requested = normalizeShell(requestedShell);
|
|
14581
|
+
if (requested) {
|
|
14582
|
+
return requested;
|
|
14470
14583
|
}
|
|
14471
|
-
|
|
14472
|
-
|
|
14473
|
-
|
|
14584
|
+
return normalizeShell(detectedShell);
|
|
14585
|
+
}
|
|
14586
|
+
function normalizeShell(value) {
|
|
14587
|
+
if (!value) {
|
|
14588
|
+
return null;
|
|
14589
|
+
}
|
|
14590
|
+
const candidate = value.split("/").at(-1)?.toLowerCase();
|
|
14591
|
+
switch (candidate) {
|
|
14592
|
+
case "bash":
|
|
14593
|
+
case "fish":
|
|
14594
|
+
case "zsh":
|
|
14595
|
+
return candidate;
|
|
14596
|
+
default:
|
|
14597
|
+
return null;
|
|
14474
14598
|
}
|
|
14475
|
-
options.stdout(formatHistoryList(history, options.cwd));
|
|
14476
|
-
return 0;
|
|
14477
14599
|
}
|
|
14478
14600
|
|
|
14479
14601
|
// src/shell-completion.ts
|
|
14480
14602
|
var TOP_LEVEL_COMMANDS = [
|
|
14481
|
-
{
|
|
14603
|
+
{
|
|
14604
|
+
name: "new",
|
|
14605
|
+
description: "create a new branch or detached linked worktree"
|
|
14606
|
+
},
|
|
14482
14607
|
{ name: "init", description: "print or install shell integration" },
|
|
14483
14608
|
{ name: "completion", description: "print shell completion definitions" },
|
|
14484
14609
|
{ name: "pr", description: "fetch a pull request into a linked worktree" },
|
|
@@ -14490,16 +14615,26 @@ var TOP_LEVEL_COMMANDS = [
|
|
|
14490
14615
|
{ name: "root", description: "print the main repository root path" },
|
|
14491
14616
|
{ name: "status", description: "summarize repository and worktree health" },
|
|
14492
14617
|
{ name: "sync", description: "fetch and update one or all worktrees" },
|
|
14618
|
+
{
|
|
14619
|
+
name: "sync-files",
|
|
14620
|
+
description: "manage local files copied into new worktrees"
|
|
14621
|
+
},
|
|
14493
14622
|
{ name: "ls", description: "list active worktrees" },
|
|
14494
14623
|
{ name: "clean", description: "interactively prune linked worktrees" },
|
|
14495
|
-
{
|
|
14624
|
+
{
|
|
14625
|
+
name: "remove",
|
|
14626
|
+
description: "remove a linked worktree and delete its branch when present"
|
|
14627
|
+
},
|
|
14496
14628
|
{ name: "rm", description: "alias of remove" },
|
|
14497
|
-
{
|
|
14629
|
+
{
|
|
14630
|
+
name: "run-hook",
|
|
14631
|
+
description: "run a named hook in the current worktree"
|
|
14632
|
+
},
|
|
14498
14633
|
{ name: "warp", description: "jump to any worktree across all known repos" },
|
|
14499
14634
|
{ name: "config", description: "manage global config defaults" }
|
|
14500
14635
|
];
|
|
14501
14636
|
var SHELL_NAMES = ["bash", "fish", "zsh"];
|
|
14502
|
-
var HOOK_NAMES = ["
|
|
14637
|
+
var HOOK_NAMES = ["after-create", "after-enter", "before-remove"];
|
|
14503
14638
|
var CONFIG_KEYS = Array.from(KNOWN_GLOBAL_CONFIG_KEYS);
|
|
14504
14639
|
function renderShellCompletion(shell) {
|
|
14505
14640
|
switch (shell) {
|
|
@@ -14512,7 +14647,9 @@ function renderShellCompletion(shell) {
|
|
|
14512
14647
|
}
|
|
14513
14648
|
}
|
|
14514
14649
|
function renderBashCompletion() {
|
|
14515
|
-
const topLevelCommands = TOP_LEVEL_COMMANDS.map(
|
|
14650
|
+
const topLevelCommands = TOP_LEVEL_COMMANDS.map(
|
|
14651
|
+
(command) => command.name
|
|
14652
|
+
).join(" ");
|
|
14516
14653
|
const shells = SHELL_NAMES.join(" ");
|
|
14517
14654
|
const hooks = HOOK_NAMES.join(" ");
|
|
14518
14655
|
const configKeys = CONFIG_KEYS.join(" ");
|
|
@@ -14566,6 +14703,12 @@ _gji_completion() {
|
|
|
14566
14703
|
sync)
|
|
14567
14704
|
COMPREPLY=( $(compgen -W "--all --json --help" -- "$cur") )
|
|
14568
14705
|
;;
|
|
14706
|
+
sync-files)
|
|
14707
|
+
if [ "$COMP_CWORD" -eq 2 ]; then
|
|
14708
|
+
COMPREPLY=( $(compgen -W "list add remove rm --json --help" -- "$cur") )
|
|
14709
|
+
return 0
|
|
14710
|
+
fi
|
|
14711
|
+
;;
|
|
14569
14712
|
ls)
|
|
14570
14713
|
COMPREPLY=( $(compgen -W "--compact --json --help" -- "$cur") )
|
|
14571
14714
|
;;
|
|
@@ -14575,7 +14718,7 @@ _gji_completion() {
|
|
|
14575
14718
|
remove|rm)
|
|
14576
14719
|
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) -f --force --dry-run --json --help" -- "$cur") )
|
|
14577
14720
|
;;
|
|
14578
|
-
|
|
14721
|
+
run-hook)
|
|
14579
14722
|
COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
|
|
14580
14723
|
;;
|
|
14581
14724
|
warp)
|
|
@@ -14613,7 +14756,7 @@ function renderFishCompletion() {
|
|
|
14613
14756
|
(shell) => `complete -c gji -n '__fish_seen_subcommand_from init' -a '${shell}' -d 'shell'`
|
|
14614
14757
|
).join("\n");
|
|
14615
14758
|
const hookLines = HOOK_NAMES.map(
|
|
14616
|
-
(hook) => `complete -c gji -n '__fish_seen_subcommand_from
|
|
14759
|
+
(hook) => `complete -c gji -n '__fish_seen_subcommand_from run-hook' -a '${hook}' -d 'hook'`
|
|
14617
14760
|
).join("\n");
|
|
14618
14761
|
const configKeyLines = CONFIG_KEYS.map(
|
|
14619
14762
|
(key) => `complete -c gji -n '__gji_should_complete_config_key' -a '${key}' -d 'config key'`
|
|
@@ -14679,6 +14822,12 @@ complete -c gji -n '__fish_seen_subcommand_from status' -l json -d 'print reposi
|
|
|
14679
14822
|
complete -c gji -n '__fish_seen_subcommand_from sync' -l all -d 'sync every worktree in the repository'
|
|
14680
14823
|
complete -c gji -n '__fish_seen_subcommand_from sync' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
14681
14824
|
|
|
14825
|
+
complete -c gji -n '__fish_seen_subcommand_from sync-files' -a 'list add remove rm' -d 'sync-files action'
|
|
14826
|
+
complete -c gji -n '__fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
|
|
14827
|
+
complete -c gji -n '__fish_seen_subcommand_from list; and __fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
|
|
14828
|
+
complete -c gji -n '__fish_seen_subcommand_from add; and __fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
|
|
14829
|
+
complete -c gji -n '__fish_seen_subcommand_from remove rm; and __fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
|
|
14830
|
+
|
|
14682
14831
|
complete -c gji -n '__fish_seen_subcommand_from ls' -l compact -d 'show only branch and path columns'
|
|
14683
14832
|
complete -c gji -n '__fish_seen_subcommand_from ls' -l json -d 'print active worktrees as JSON'
|
|
14684
14833
|
|
|
@@ -14760,6 +14909,9 @@ case "\${words[2]}" in
|
|
|
14760
14909
|
sync)
|
|
14761
14910
|
_arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
|
|
14762
14911
|
;;
|
|
14912
|
+
sync-files)
|
|
14913
|
+
_arguments '--json[emit JSON instead of human-readable output]' '2:action:(list add remove rm)' '*:path: '
|
|
14914
|
+
;;
|
|
14763
14915
|
ls)
|
|
14764
14916
|
_arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
|
|
14765
14917
|
;;
|
|
@@ -14769,7 +14921,7 @@ case "\${words[2]}" in
|
|
|
14769
14921
|
remove|rm)
|
|
14770
14922
|
_arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
|
|
14771
14923
|
;;
|
|
14772
|
-
|
|
14924
|
+
run-hook)
|
|
14773
14925
|
_arguments "2:hook:(${hooks})"
|
|
14774
14926
|
;;
|
|
14775
14927
|
warp)
|
|
@@ -14806,29 +14958,6 @@ function escapeSingleQuotes(value) {
|
|
|
14806
14958
|
return value.replace(/'/g, `'\\''`);
|
|
14807
14959
|
}
|
|
14808
14960
|
|
|
14809
|
-
// src/shell.ts
|
|
14810
|
-
function resolveSupportedShell(requestedShell, detectedShell) {
|
|
14811
|
-
const requested = normalizeShell(requestedShell);
|
|
14812
|
-
if (requested) {
|
|
14813
|
-
return requested;
|
|
14814
|
-
}
|
|
14815
|
-
return normalizeShell(detectedShell);
|
|
14816
|
-
}
|
|
14817
|
-
function normalizeShell(value) {
|
|
14818
|
-
if (!value) {
|
|
14819
|
-
return null;
|
|
14820
|
-
}
|
|
14821
|
-
const candidate = value.split("/").at(-1)?.toLowerCase();
|
|
14822
|
-
switch (candidate) {
|
|
14823
|
-
case "bash":
|
|
14824
|
-
case "fish":
|
|
14825
|
-
case "zsh":
|
|
14826
|
-
return candidate;
|
|
14827
|
-
default:
|
|
14828
|
-
return null;
|
|
14829
|
-
}
|
|
14830
|
-
}
|
|
14831
|
-
|
|
14832
14961
|
// src/completion.ts
|
|
14833
14962
|
async function runCompletionCommand(options) {
|
|
14834
14963
|
const shell = resolveSupportedShell(options.shell, process.env.SHELL);
|
|
@@ -14860,7 +14989,10 @@ async function runConfigCommand(options) {
|
|
|
14860
14989
|
}
|
|
14861
14990
|
case "set":
|
|
14862
14991
|
if (options.key && options.value !== void 0) {
|
|
14863
|
-
await updateGlobalConfigKey(
|
|
14992
|
+
await updateGlobalConfigKey(
|
|
14993
|
+
options.key,
|
|
14994
|
+
parseConfigValue(options.value)
|
|
14995
|
+
);
|
|
14864
14996
|
return 0;
|
|
14865
14997
|
}
|
|
14866
14998
|
break;
|
|
@@ -14888,19 +15020,72 @@ import { realpath as realpath2 } from "node:fs/promises";
|
|
|
14888
15020
|
import { basename as basename5, resolve as resolve5 } from "node:path";
|
|
14889
15021
|
|
|
14890
15022
|
// src/new.ts
|
|
15023
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
14891
15024
|
import { mkdir as mkdir4 } from "node:fs/promises";
|
|
14892
15025
|
import { basename as basename3, dirname as dirname5 } from "node:path";
|
|
14893
|
-
import { execFile as execFile3 } from "node:child_process";
|
|
14894
15026
|
import { promisify as promisify4 } from "node:util";
|
|
14895
15027
|
|
|
15028
|
+
// src/conflict.ts
|
|
15029
|
+
import { constants } from "node:fs";
|
|
15030
|
+
import { access as access2 } from "node:fs/promises";
|
|
15031
|
+
async function pathExists(path9) {
|
|
15032
|
+
try {
|
|
15033
|
+
await access2(path9, constants.F_OK);
|
|
15034
|
+
return true;
|
|
15035
|
+
} catch {
|
|
15036
|
+
return false;
|
|
15037
|
+
}
|
|
15038
|
+
}
|
|
15039
|
+
async function promptForPathConflict(path9) {
|
|
15040
|
+
const choice = await ve({
|
|
15041
|
+
message: `Target path already exists: ${path9}`,
|
|
15042
|
+
options: [
|
|
15043
|
+
{
|
|
15044
|
+
value: "abort",
|
|
15045
|
+
label: "Abort",
|
|
15046
|
+
hint: "Keep the existing directory untouched"
|
|
15047
|
+
},
|
|
15048
|
+
{
|
|
15049
|
+
value: "reuse",
|
|
15050
|
+
label: "Reuse path",
|
|
15051
|
+
hint: "Print the existing path and stop"
|
|
15052
|
+
}
|
|
15053
|
+
]
|
|
15054
|
+
});
|
|
15055
|
+
if (pD(choice)) {
|
|
15056
|
+
return "abort";
|
|
15057
|
+
}
|
|
15058
|
+
return choice;
|
|
15059
|
+
}
|
|
15060
|
+
|
|
14896
15061
|
// src/editor.ts
|
|
14897
15062
|
import { spawn as spawn3 } from "node:child_process";
|
|
14898
15063
|
var EDITORS = [
|
|
14899
|
-
{
|
|
14900
|
-
|
|
14901
|
-
|
|
15064
|
+
{
|
|
15065
|
+
cli: "cursor",
|
|
15066
|
+
name: "Cursor",
|
|
15067
|
+
newWindowFlag: "--new-window",
|
|
15068
|
+
supportsWorkspace: true
|
|
15069
|
+
},
|
|
15070
|
+
{
|
|
15071
|
+
cli: "code",
|
|
15072
|
+
name: "VS Code",
|
|
15073
|
+
newWindowFlag: "--new-window",
|
|
15074
|
+
supportsWorkspace: true
|
|
15075
|
+
},
|
|
15076
|
+
{
|
|
15077
|
+
cli: "windsurf",
|
|
15078
|
+
name: "Windsurf",
|
|
15079
|
+
newWindowFlag: "--new-window",
|
|
15080
|
+
supportsWorkspace: true
|
|
15081
|
+
},
|
|
14902
15082
|
{ cli: "zed", name: "Zed", supportsWorkspace: false },
|
|
14903
|
-
{
|
|
15083
|
+
{
|
|
15084
|
+
cli: "subl",
|
|
15085
|
+
name: "Sublime Text",
|
|
15086
|
+
newWindowFlag: "--new-window",
|
|
15087
|
+
supportsWorkspace: false
|
|
15088
|
+
}
|
|
14904
15089
|
];
|
|
14905
15090
|
async function defaultSpawnEditor(cli, args) {
|
|
14906
15091
|
const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
|
|
@@ -14916,13 +15101,7 @@ import { copyFile, mkdir as mkdir3, stat } from "node:fs/promises";
|
|
|
14916
15101
|
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join4, normalize } from "node:path";
|
|
14917
15102
|
async function syncFiles(mainRoot, targetPath, patterns) {
|
|
14918
15103
|
for (const pattern of patterns) {
|
|
14919
|
-
|
|
14920
|
-
throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
|
|
14921
|
-
}
|
|
14922
|
-
const normalized = normalize(pattern);
|
|
14923
|
-
if (normalized.startsWith("..")) {
|
|
14924
|
-
throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
|
|
14925
|
-
}
|
|
15104
|
+
const normalized = validateSyncFilePattern(pattern);
|
|
14926
15105
|
const sourcePath = join4(mainRoot, normalized);
|
|
14927
15106
|
const destPath = join4(targetPath, normalized);
|
|
14928
15107
|
const sourceExists = await fileExists(sourcePath);
|
|
@@ -14937,6 +15116,20 @@ async function syncFiles(mainRoot, targetPath, patterns) {
|
|
|
14937
15116
|
await copyFile(sourcePath, destPath);
|
|
14938
15117
|
}
|
|
14939
15118
|
}
|
|
15119
|
+
function validateSyncFilePattern(pattern) {
|
|
15120
|
+
if (isAbsolute2(pattern)) {
|
|
15121
|
+
throw new Error(
|
|
15122
|
+
`syncFiles: pattern must be a relative path, got: ${pattern}`
|
|
15123
|
+
);
|
|
15124
|
+
}
|
|
15125
|
+
const normalized = normalize(pattern);
|
|
15126
|
+
if (normalized.startsWith("..")) {
|
|
15127
|
+
throw new Error(
|
|
15128
|
+
`syncFiles: pattern must not contain '..' segments, got: ${pattern}`
|
|
15129
|
+
);
|
|
15130
|
+
}
|
|
15131
|
+
return normalized;
|
|
15132
|
+
}
|
|
14940
15133
|
async function fileExists(path9) {
|
|
14941
15134
|
try {
|
|
14942
15135
|
await stat(path9);
|
|
@@ -14956,7 +15149,7 @@ function isNotFoundError(error) {
|
|
|
14956
15149
|
import { spawn as spawn4 } from "node:child_process";
|
|
14957
15150
|
|
|
14958
15151
|
// src/package-manager.ts
|
|
14959
|
-
import { access as
|
|
15152
|
+
import { access as access3, readdir } from "node:fs/promises";
|
|
14960
15153
|
import { join as join5 } from "node:path";
|
|
14961
15154
|
var ENTRIES = [
|
|
14962
15155
|
// JavaScript / TypeScript
|
|
@@ -14970,10 +15163,22 @@ var ENTRIES = [
|
|
|
14970
15163
|
{ name: "uv", signals: ["uv.lock"], command: "uv sync" },
|
|
14971
15164
|
{ name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
|
|
14972
15165
|
{ name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
|
|
14973
|
-
{
|
|
14974
|
-
|
|
15166
|
+
{
|
|
15167
|
+
name: "conda-lock",
|
|
15168
|
+
signals: ["conda-lock.yml"],
|
|
15169
|
+
command: "conda-lock install"
|
|
15170
|
+
},
|
|
15171
|
+
{
|
|
15172
|
+
name: "conda",
|
|
15173
|
+
signals: ["environment.yml"],
|
|
15174
|
+
command: "conda env update --file environment.yml"
|
|
15175
|
+
},
|
|
14975
15176
|
// R
|
|
14976
|
-
{
|
|
15177
|
+
{
|
|
15178
|
+
name: "renv",
|
|
15179
|
+
signals: ["renv.lock"],
|
|
15180
|
+
command: "Rscript -e 'renv::restore()'"
|
|
15181
|
+
},
|
|
14977
15182
|
// Rust
|
|
14978
15183
|
{ name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
|
|
14979
15184
|
// Go
|
|
@@ -14990,25 +15195,56 @@ var ENTRIES = [
|
|
|
14990
15195
|
// Java / Kotlin / Scala
|
|
14991
15196
|
{ name: "maven", signals: ["pom.xml"], command: "mvn install" },
|
|
14992
15197
|
{ name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
|
|
14993
|
-
{
|
|
15198
|
+
{
|
|
15199
|
+
name: "gradle",
|
|
15200
|
+
signals: ["build.gradle", "build.gradle.kts"],
|
|
15201
|
+
command: "gradle build"
|
|
15202
|
+
},
|
|
14994
15203
|
{ name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
|
|
14995
15204
|
// .NET (C# / F# / VB)
|
|
14996
|
-
{
|
|
15205
|
+
{
|
|
15206
|
+
name: "dotnet",
|
|
15207
|
+
signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"],
|
|
15208
|
+
command: "dotnet restore",
|
|
15209
|
+
glob: true
|
|
15210
|
+
},
|
|
14997
15211
|
// Swift
|
|
14998
|
-
{
|
|
15212
|
+
{
|
|
15213
|
+
name: "swift",
|
|
15214
|
+
signals: ["Package.swift"],
|
|
15215
|
+
command: "swift package resolve"
|
|
15216
|
+
},
|
|
14999
15217
|
// Haskell
|
|
15000
15218
|
{ name: "stack", signals: ["stack.yaml"], command: "stack build" },
|
|
15001
|
-
{
|
|
15002
|
-
|
|
15219
|
+
{
|
|
15220
|
+
name: "cabal",
|
|
15221
|
+
signals: ["cabal.project"],
|
|
15222
|
+
command: "cabal install --only-dependencies"
|
|
15223
|
+
},
|
|
15224
|
+
{
|
|
15225
|
+
name: "cabal",
|
|
15226
|
+
signals: ["*.cabal"],
|
|
15227
|
+
command: "cabal install --only-dependencies",
|
|
15228
|
+
glob: true
|
|
15229
|
+
},
|
|
15003
15230
|
// Clojure
|
|
15004
15231
|
{ name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
|
|
15005
15232
|
{ name: "leiningen", signals: ["project.clj"], command: "lein deps" },
|
|
15006
15233
|
// OCaml
|
|
15007
15234
|
{ name: "dune", signals: ["dune-project"], command: "dune build" },
|
|
15008
15235
|
// Julia
|
|
15009
|
-
{
|
|
15236
|
+
{
|
|
15237
|
+
name: "julia",
|
|
15238
|
+
signals: ["Manifest.toml"],
|
|
15239
|
+
command: "julia --project -e 'using Pkg; Pkg.instantiate()'"
|
|
15240
|
+
},
|
|
15010
15241
|
// Nim
|
|
15011
|
-
{
|
|
15242
|
+
{
|
|
15243
|
+
name: "nimble",
|
|
15244
|
+
signals: ["*.nimble"],
|
|
15245
|
+
command: "nimble install",
|
|
15246
|
+
glob: true
|
|
15247
|
+
},
|
|
15012
15248
|
// Crystal
|
|
15013
15249
|
{ name: "shards", signals: ["shard.yml"], command: "shards install" },
|
|
15014
15250
|
// Perl
|
|
@@ -15017,12 +15253,20 @@ var ENTRIES = [
|
|
|
15017
15253
|
{ name: "zig", signals: ["build.zig.zon"], command: "zig build" },
|
|
15018
15254
|
// C / C++
|
|
15019
15255
|
{ name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
|
|
15020
|
-
{
|
|
15256
|
+
{
|
|
15257
|
+
name: "conan",
|
|
15258
|
+
signals: ["conanfile.py", "conanfile.txt"],
|
|
15259
|
+
command: "conan install ."
|
|
15260
|
+
},
|
|
15021
15261
|
// Nix
|
|
15022
15262
|
{ name: "nix", signals: ["flake.nix"], command: "nix develop" },
|
|
15023
15263
|
{ name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
|
|
15024
15264
|
// Terraform / OpenTofu
|
|
15025
|
-
{
|
|
15265
|
+
{
|
|
15266
|
+
name: "terraform",
|
|
15267
|
+
signals: ["terraform.lock.hcl"],
|
|
15268
|
+
command: "terraform init"
|
|
15269
|
+
}
|
|
15026
15270
|
];
|
|
15027
15271
|
async function detectPackageManager(repoRoot) {
|
|
15028
15272
|
for (const entry of ENTRIES) {
|
|
@@ -15036,7 +15280,7 @@ async function detectPackageManager(repoRoot) {
|
|
|
15036
15280
|
async function matchesExact(repoRoot, signals) {
|
|
15037
15281
|
for (const signal of signals) {
|
|
15038
15282
|
try {
|
|
15039
|
-
await
|
|
15283
|
+
await access3(join5(repoRoot, signal));
|
|
15040
15284
|
return true;
|
|
15041
15285
|
} catch {
|
|
15042
15286
|
}
|
|
@@ -15063,8 +15307,7 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
|
|
|
15063
15307
|
if (isHeadless() || nonInteractive) {
|
|
15064
15308
|
return;
|
|
15065
15309
|
}
|
|
15066
|
-
|
|
15067
|
-
if (isConfiguredHookCommand(hooks?.afterCreate)) {
|
|
15310
|
+
if (extractHooks(config)["after-create"]) {
|
|
15068
15311
|
return;
|
|
15069
15312
|
}
|
|
15070
15313
|
if (config.skipInstallPrompt === true) {
|
|
@@ -15085,8 +15328,10 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
|
|
|
15085
15328
|
try {
|
|
15086
15329
|
await runner(pm.installCommand, worktreePath, stderr);
|
|
15087
15330
|
} catch (error) {
|
|
15088
|
-
stderr(
|
|
15089
|
-
`)
|
|
15331
|
+
stderr(
|
|
15332
|
+
`gji: install command failed: ${error instanceof Error ? error.message : String(error)}
|
|
15333
|
+
`
|
|
15334
|
+
);
|
|
15090
15335
|
}
|
|
15091
15336
|
}
|
|
15092
15337
|
const saveGlobal = config.installSaveTarget === "global";
|
|
@@ -15095,16 +15340,35 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
|
|
|
15095
15340
|
if (choice === "always") {
|
|
15096
15341
|
try {
|
|
15097
15342
|
if (saveGlobal) {
|
|
15098
|
-
const
|
|
15099
|
-
|
|
15343
|
+
const existingRaw = await loadExistingGlobalRepoHooks(repoRoot);
|
|
15344
|
+
const existing = extractHooks({ hooks: existingRaw });
|
|
15345
|
+
await writeGlobalKey(repoRoot, "hooks", {
|
|
15346
|
+
...existing["after-enter"] !== void 0 && {
|
|
15347
|
+
"after-enter": existing["after-enter"]
|
|
15348
|
+
},
|
|
15349
|
+
...existing["before-remove"] !== void 0 && {
|
|
15350
|
+
"before-remove": existing["before-remove"]
|
|
15351
|
+
},
|
|
15352
|
+
"after-create": pm.installCommand
|
|
15353
|
+
});
|
|
15100
15354
|
} else {
|
|
15101
15355
|
const { config: localConfig } = await loadConfig(repoRoot);
|
|
15102
|
-
const
|
|
15103
|
-
await writeKey(repoRoot, "hooks", {
|
|
15356
|
+
const existing = extractHooks(localConfig);
|
|
15357
|
+
await writeKey(repoRoot, "hooks", {
|
|
15358
|
+
...existing["after-enter"] !== void 0 && {
|
|
15359
|
+
"after-enter": existing["after-enter"]
|
|
15360
|
+
},
|
|
15361
|
+
...existing["before-remove"] !== void 0 && {
|
|
15362
|
+
"before-remove": existing["before-remove"]
|
|
15363
|
+
},
|
|
15364
|
+
"after-create": pm.installCommand
|
|
15365
|
+
});
|
|
15104
15366
|
}
|
|
15105
15367
|
} catch (error) {
|
|
15106
|
-
stderr(
|
|
15107
|
-
`)
|
|
15368
|
+
stderr(
|
|
15369
|
+
`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
|
|
15370
|
+
`
|
|
15371
|
+
);
|
|
15108
15372
|
}
|
|
15109
15373
|
}
|
|
15110
15374
|
if (choice === "never") {
|
|
@@ -15115,14 +15379,20 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
|
|
|
15115
15379
|
await writeKey(repoRoot, "skipInstallPrompt", true);
|
|
15116
15380
|
}
|
|
15117
15381
|
} catch (error) {
|
|
15118
|
-
stderr(
|
|
15119
|
-
`)
|
|
15382
|
+
stderr(
|
|
15383
|
+
`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
|
|
15384
|
+
`
|
|
15385
|
+
);
|
|
15120
15386
|
}
|
|
15121
15387
|
}
|
|
15122
15388
|
}
|
|
15123
15389
|
async function defaultRunInstallCommand(command, cwd, stderr) {
|
|
15124
15390
|
await new Promise((resolve6, reject) => {
|
|
15125
|
-
const child = spawn4(command, {
|
|
15391
|
+
const child = spawn4(command, {
|
|
15392
|
+
cwd,
|
|
15393
|
+
shell: true,
|
|
15394
|
+
stdio: ["ignore", "inherit", "pipe"]
|
|
15395
|
+
});
|
|
15126
15396
|
child.stderr.on("data", (chunk) => {
|
|
15127
15397
|
stderr(chunk.toString());
|
|
15128
15398
|
});
|
|
@@ -15156,8 +15426,12 @@ async function defaultPromptForInstallChoice(pm) {
|
|
|
15156
15426
|
options: [
|
|
15157
15427
|
{ value: "yes", label: "Yes", hint: "run once" },
|
|
15158
15428
|
{ value: "no", label: "No", hint: "skip this time" },
|
|
15159
|
-
{ value: "always", label: "Always", hint: "save as
|
|
15160
|
-
{
|
|
15429
|
+
{ value: "always", label: "Always", hint: "save as after-create hook" },
|
|
15430
|
+
{
|
|
15431
|
+
value: "never",
|
|
15432
|
+
label: "Never",
|
|
15433
|
+
hint: "disable this prompt for this repo"
|
|
15434
|
+
}
|
|
15161
15435
|
]
|
|
15162
15436
|
});
|
|
15163
15437
|
if (pD(choice)) {
|
|
@@ -15168,35 +15442,6 @@ async function defaultPromptForInstallChoice(pm) {
|
|
|
15168
15442
|
function isPlainObject2(value) {
|
|
15169
15443
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15170
15444
|
}
|
|
15171
|
-
function isConfiguredHookCommand(value) {
|
|
15172
|
-
if (typeof value === "string") return value.length > 0;
|
|
15173
|
-
return Array.isArray(value) && value.length > 0 && value[0] !== "" && value.every((item) => typeof item === "string");
|
|
15174
|
-
}
|
|
15175
|
-
|
|
15176
|
-
// src/conflict.ts
|
|
15177
|
-
import { access as access3 } from "node:fs/promises";
|
|
15178
|
-
import { constants } from "node:fs";
|
|
15179
|
-
async function pathExists(path9) {
|
|
15180
|
-
try {
|
|
15181
|
-
await access3(path9, constants.F_OK);
|
|
15182
|
-
return true;
|
|
15183
|
-
} catch {
|
|
15184
|
-
return false;
|
|
15185
|
-
}
|
|
15186
|
-
}
|
|
15187
|
-
async function promptForPathConflict(path9) {
|
|
15188
|
-
const choice = await ve({
|
|
15189
|
-
message: `Target path already exists: ${path9}`,
|
|
15190
|
-
options: [
|
|
15191
|
-
{ value: "abort", label: "Abort", hint: "Keep the existing directory untouched" },
|
|
15192
|
-
{ value: "reuse", label: "Reuse path", hint: "Print the existing path and stop" }
|
|
15193
|
-
]
|
|
15194
|
-
});
|
|
15195
|
-
if (pD(choice)) {
|
|
15196
|
-
return "abort";
|
|
15197
|
-
}
|
|
15198
|
-
return choice;
|
|
15199
|
-
}
|
|
15200
15445
|
|
|
15201
15446
|
// src/new.ts
|
|
15202
15447
|
var execFileAsync3 = promisify4(execFile3);
|
|
@@ -15208,7 +15453,11 @@ function createNewCommand(dependencies = {}) {
|
|
|
15208
15453
|
const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
|
|
15209
15454
|
return async function runNewCommand2(options) {
|
|
15210
15455
|
const repository = await detectRepository(options.cwd);
|
|
15211
|
-
const config = await loadEffectiveConfig(
|
|
15456
|
+
const config = await loadEffectiveConfig(
|
|
15457
|
+
repository.repoRoot,
|
|
15458
|
+
void 0,
|
|
15459
|
+
options.stderr
|
|
15460
|
+
);
|
|
15212
15461
|
const usesGeneratedDetachedName = options.detached && options.branch === void 0;
|
|
15213
15462
|
if (options.editor && !options.open) {
|
|
15214
15463
|
options.stderr("gji new: --editor has no effect without --open\n");
|
|
@@ -15219,8 +15468,10 @@ function createNewCommand(dependencies = {}) {
|
|
|
15219
15468
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}
|
|
15220
15469
|
`);
|
|
15221
15470
|
} else {
|
|
15222
|
-
options.stderr(
|
|
15223
|
-
`)
|
|
15471
|
+
options.stderr(
|
|
15472
|
+
`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
15473
|
+
`
|
|
15474
|
+
);
|
|
15224
15475
|
}
|
|
15225
15476
|
return 1;
|
|
15226
15477
|
}
|
|
@@ -15238,8 +15489,10 @@ function createNewCommand(dependencies = {}) {
|
|
|
15238
15489
|
const branchError = validateBranchName(rawBranch);
|
|
15239
15490
|
if (branchError) {
|
|
15240
15491
|
if (options.json) {
|
|
15241
|
-
options.stderr(
|
|
15242
|
-
|
|
15492
|
+
options.stderr(
|
|
15493
|
+
`${JSON.stringify({ error: branchError }, null, 2)}
|
|
15494
|
+
`
|
|
15495
|
+
);
|
|
15243
15496
|
} else {
|
|
15244
15497
|
options.stderr(`gji new: ${branchError}
|
|
15245
15498
|
`);
|
|
@@ -15250,18 +15503,32 @@ function createNewCommand(dependencies = {}) {
|
|
|
15250
15503
|
const rawBasePath = resolveConfigString(config, "worktreePath");
|
|
15251
15504
|
const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
|
|
15252
15505
|
const worktreeName = options.detached ? rawBranch : applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
|
|
15253
|
-
const worktreePath = usesGeneratedDetachedName ? await resolveUniqueDetachedWorktreePath(
|
|
15506
|
+
const worktreePath = usesGeneratedDetachedName ? await resolveUniqueDetachedWorktreePath(
|
|
15507
|
+
repository.repoRoot,
|
|
15508
|
+
worktreeName,
|
|
15509
|
+
configuredBasePath
|
|
15510
|
+
) : resolveWorktreePath(
|
|
15511
|
+
repository.repoRoot,
|
|
15512
|
+
worktreeName,
|
|
15513
|
+
configuredBasePath
|
|
15514
|
+
);
|
|
15254
15515
|
if (!usesGeneratedDetachedName && await pathExists(worktreePath)) {
|
|
15255
15516
|
if (options.force) {
|
|
15256
15517
|
if (!options.dryRun) {
|
|
15257
15518
|
try {
|
|
15258
|
-
await execFileAsync3(
|
|
15519
|
+
await execFileAsync3(
|
|
15520
|
+
"git",
|
|
15521
|
+
["worktree", "remove", "--force", worktreePath],
|
|
15522
|
+
{ cwd: repository.repoRoot }
|
|
15523
|
+
);
|
|
15259
15524
|
} catch (err) {
|
|
15260
15525
|
if (!isNotRegisteredWorktreeError(err)) {
|
|
15261
15526
|
const msg = `could not remove existing worktree at ${worktreePath}: ${toExecMessage(err)}`;
|
|
15262
15527
|
if (options.json) {
|
|
15263
|
-
options.stderr(
|
|
15264
|
-
|
|
15528
|
+
options.stderr(
|
|
15529
|
+
`${JSON.stringify({ warning: msg }, null, 2)}
|
|
15530
|
+
`
|
|
15531
|
+
);
|
|
15265
15532
|
} else {
|
|
15266
15533
|
options.stderr(`Warning: ${msg}
|
|
15267
15534
|
`);
|
|
@@ -15270,7 +15537,9 @@ function createNewCommand(dependencies = {}) {
|
|
|
15270
15537
|
}
|
|
15271
15538
|
if (!options.detached) {
|
|
15272
15539
|
try {
|
|
15273
|
-
await execFileAsync3("git", ["branch", "-D", worktreeName], {
|
|
15540
|
+
await execFileAsync3("git", ["branch", "-D", worktreeName], {
|
|
15541
|
+
cwd: repository.repoRoot
|
|
15542
|
+
});
|
|
15274
15543
|
} catch {
|
|
15275
15544
|
}
|
|
15276
15545
|
}
|
|
@@ -15281,12 +15550,18 @@ function createNewCommand(dependencies = {}) {
|
|
|
15281
15550
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}
|
|
15282
15551
|
`);
|
|
15283
15552
|
} else {
|
|
15284
|
-
options.stderr(
|
|
15285
|
-
`)
|
|
15286
|
-
|
|
15287
|
-
|
|
15288
|
-
options.stderr(
|
|
15289
|
-
`
|
|
15553
|
+
options.stderr(
|
|
15554
|
+
`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
15555
|
+
`
|
|
15556
|
+
);
|
|
15557
|
+
options.stderr(
|
|
15558
|
+
`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree
|
|
15559
|
+
`
|
|
15560
|
+
);
|
|
15561
|
+
options.stderr(
|
|
15562
|
+
`Hint: Use 'gji run-hook after-create' inside the worktree to re-run setup hooks
|
|
15563
|
+
`
|
|
15564
|
+
);
|
|
15290
15565
|
}
|
|
15291
15566
|
return 1;
|
|
15292
15567
|
} else {
|
|
@@ -15296,53 +15571,81 @@ function createNewCommand(dependencies = {}) {
|
|
|
15296
15571
|
await writeOutput(worktreePath, options.stdout);
|
|
15297
15572
|
return 0;
|
|
15298
15573
|
}
|
|
15299
|
-
options.stderr(
|
|
15300
|
-
`
|
|
15574
|
+
options.stderr(
|
|
15575
|
+
`Aborted because target worktree path already exists: ${worktreePath}
|
|
15576
|
+
`
|
|
15577
|
+
);
|
|
15301
15578
|
return 1;
|
|
15302
15579
|
}
|
|
15303
15580
|
}
|
|
15304
15581
|
if (options.dryRun) {
|
|
15305
15582
|
if (options.json) {
|
|
15306
|
-
options.stdout(
|
|
15307
|
-
|
|
15583
|
+
options.stdout(
|
|
15584
|
+
`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
|
|
15585
|
+
`
|
|
15586
|
+
);
|
|
15308
15587
|
} else {
|
|
15309
15588
|
const resolvedEditor = options.open ? options.editor ?? resolveConfigString(config, "editor") : void 0;
|
|
15310
15589
|
const openNote = resolvedEditor ? `, then open in ${resolvedEditor}` : "";
|
|
15311
|
-
options.stdout(
|
|
15312
|
-
`)
|
|
15590
|
+
options.stdout(
|
|
15591
|
+
`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
|
|
15592
|
+
`
|
|
15593
|
+
);
|
|
15313
15594
|
}
|
|
15314
15595
|
return 0;
|
|
15315
15596
|
}
|
|
15316
15597
|
await mkdir4(dirname5(worktreePath), { recursive: true });
|
|
15317
15598
|
const gitArgs = options.detached ? ["worktree", "add", "--detach", worktreePath] : await localBranchExists(repository.repoRoot, worktreeName) ? ["worktree", "add", worktreePath, worktreeName] : ["worktree", "add", "-b", worktreeName, worktreePath];
|
|
15318
15599
|
await execFileAsync3("git", gitArgs, { cwd: repository.repoRoot });
|
|
15319
|
-
const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
|
|
15600
|
+
const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
|
|
15601
|
+
(p2) => typeof p2 === "string"
|
|
15602
|
+
) : [];
|
|
15320
15603
|
for (const pattern of syncPatterns) {
|
|
15321
15604
|
try {
|
|
15322
15605
|
await syncFiles(repository.repoRoot, worktreePath, [pattern]);
|
|
15323
15606
|
} catch (error) {
|
|
15324
|
-
options.stderr(
|
|
15325
|
-
`)
|
|
15607
|
+
options.stderr(
|
|
15608
|
+
`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
|
|
15609
|
+
`
|
|
15610
|
+
);
|
|
15326
15611
|
}
|
|
15327
15612
|
}
|
|
15328
|
-
await maybeRunInstallPrompt(
|
|
15613
|
+
await maybeRunInstallPrompt(
|
|
15614
|
+
worktreePath,
|
|
15615
|
+
repository.repoRoot,
|
|
15616
|
+
config,
|
|
15617
|
+
options.stderr,
|
|
15618
|
+
dependencies,
|
|
15619
|
+
!!options.json
|
|
15620
|
+
);
|
|
15329
15621
|
const hooks = extractHooks(config);
|
|
15330
15622
|
await runHook(
|
|
15331
|
-
hooks
|
|
15623
|
+
hooks["after-create"],
|
|
15332
15624
|
worktreePath,
|
|
15333
|
-
{
|
|
15625
|
+
{
|
|
15626
|
+
branch: worktreeName,
|
|
15627
|
+
path: worktreePath,
|
|
15628
|
+
repo: basename3(repository.repoRoot)
|
|
15629
|
+
},
|
|
15334
15630
|
options.stderr
|
|
15335
15631
|
);
|
|
15336
15632
|
if (options.json) {
|
|
15337
|
-
options.stdout(
|
|
15338
|
-
|
|
15633
|
+
options.stdout(
|
|
15634
|
+
`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}
|
|
15635
|
+
`
|
|
15636
|
+
);
|
|
15339
15637
|
} else {
|
|
15340
15638
|
await appendHistory(worktreePath, worktreeName);
|
|
15341
15639
|
await writeOutput(worktreePath, options.stdout);
|
|
15342
15640
|
}
|
|
15343
15641
|
if (options.open) {
|
|
15344
15642
|
const resolvedEditor = options.editor ?? resolveConfigString(config, "editor");
|
|
15345
|
-
await openWorktree(
|
|
15643
|
+
await openWorktree(
|
|
15644
|
+
worktreePath,
|
|
15645
|
+
resolvedEditor,
|
|
15646
|
+
spawnEditor,
|
|
15647
|
+
options.stderr
|
|
15648
|
+
);
|
|
15346
15649
|
}
|
|
15347
15650
|
return 0;
|
|
15348
15651
|
};
|
|
@@ -15403,7 +15706,11 @@ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName, basePath) {
|
|
|
15403
15706
|
let attempt = 1;
|
|
15404
15707
|
while (true) {
|
|
15405
15708
|
const candidateName = attempt === 1 ? baseName : `${baseName}-${attempt}`;
|
|
15406
|
-
const candidatePath = resolveWorktreePath(
|
|
15709
|
+
const candidatePath = resolveWorktreePath(
|
|
15710
|
+
repoRoot,
|
|
15711
|
+
candidateName,
|
|
15712
|
+
basePath
|
|
15713
|
+
);
|
|
15407
15714
|
if (!await pathExists(candidatePath)) {
|
|
15408
15715
|
return candidatePath;
|
|
15409
15716
|
}
|
|
@@ -15431,7 +15738,11 @@ function pickRandom(values, random) {
|
|
|
15431
15738
|
}
|
|
15432
15739
|
async function localBranchExists(repoRoot, branchName) {
|
|
15433
15740
|
try {
|
|
15434
|
-
await execFileAsync3(
|
|
15741
|
+
await execFileAsync3(
|
|
15742
|
+
"git",
|
|
15743
|
+
["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
|
|
15744
|
+
{ cwd: repoRoot }
|
|
15745
|
+
);
|
|
15435
15746
|
return true;
|
|
15436
15747
|
} catch {
|
|
15437
15748
|
return false;
|
|
@@ -15452,7 +15763,9 @@ function toExecMessage(error) {
|
|
|
15452
15763
|
}
|
|
15453
15764
|
async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
|
|
15454
15765
|
if (!editorCli) {
|
|
15455
|
-
stderr(
|
|
15766
|
+
stderr(
|
|
15767
|
+
"gji new: --open requires --editor <cli> or a saved editor in config\n"
|
|
15768
|
+
);
|
|
15456
15769
|
return;
|
|
15457
15770
|
}
|
|
15458
15771
|
const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
|
|
@@ -15566,11 +15879,17 @@ async function runWarpNavigate(options) {
|
|
|
15566
15879
|
}
|
|
15567
15880
|
return 1;
|
|
15568
15881
|
}
|
|
15569
|
-
const target = await resolveWarpTarget({
|
|
15882
|
+
const target = await resolveWarpTarget({
|
|
15883
|
+
...options,
|
|
15884
|
+
commandName: "gji warp",
|
|
15885
|
+
json: options.json
|
|
15886
|
+
});
|
|
15570
15887
|
if (!target) return 1;
|
|
15571
15888
|
if (options.json) {
|
|
15572
|
-
options.stdout(
|
|
15573
|
-
|
|
15889
|
+
options.stdout(
|
|
15890
|
+
`${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}
|
|
15891
|
+
`
|
|
15892
|
+
);
|
|
15574
15893
|
return 0;
|
|
15575
15894
|
}
|
|
15576
15895
|
appendHistory(target.path, target.branch).catch(() => void 0);
|
|
@@ -15771,14 +16090,19 @@ function createGoCommand(dependencies = {}) {
|
|
|
15771
16090
|
);
|
|
15772
16091
|
return 1;
|
|
15773
16092
|
}
|
|
15774
|
-
const target = await resolveWarpTarget({
|
|
16093
|
+
const target = await resolveWarpTarget({
|
|
16094
|
+
...options,
|
|
16095
|
+
commandName: "gji go"
|
|
16096
|
+
});
|
|
15775
16097
|
if (!target) return 1;
|
|
15776
16098
|
appendHistory(target.path, target.branch).catch(() => void 0);
|
|
15777
16099
|
await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
|
|
15778
16100
|
return 0;
|
|
15779
16101
|
}
|
|
15780
16102
|
if (!options.branch && isHeadless()) {
|
|
15781
|
-
options.stderr(
|
|
16103
|
+
options.stderr(
|
|
16104
|
+
"gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
|
|
16105
|
+
);
|
|
15782
16106
|
return 1;
|
|
15783
16107
|
}
|
|
15784
16108
|
const prompted = options.branch ? null : await prompt(sortByCurrentFirst(worktrees));
|
|
@@ -15795,15 +16119,25 @@ function createGoCommand(dependencies = {}) {
|
|
|
15795
16119
|
return 1;
|
|
15796
16120
|
}
|
|
15797
16121
|
const chosenWorktree = worktrees.find((w2) => w2.path === resolvedPath);
|
|
15798
|
-
const config = await loadEffectiveConfig(
|
|
16122
|
+
const config = await loadEffectiveConfig(
|
|
16123
|
+
repository.repoRoot,
|
|
16124
|
+
void 0,
|
|
16125
|
+
options.stderr
|
|
16126
|
+
);
|
|
15799
16127
|
const hooks = extractHooks(config);
|
|
15800
16128
|
await runHook(
|
|
15801
|
-
hooks
|
|
16129
|
+
hooks["after-enter"],
|
|
15802
16130
|
resolvedPath,
|
|
15803
|
-
{
|
|
16131
|
+
{
|
|
16132
|
+
branch: chosenWorktree?.branch ?? void 0,
|
|
16133
|
+
path: resolvedPath,
|
|
16134
|
+
repo: basename6(repository.repoRoot)
|
|
16135
|
+
},
|
|
15804
16136
|
options.stderr
|
|
15805
16137
|
);
|
|
15806
|
-
appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(
|
|
16138
|
+
appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(
|
|
16139
|
+
() => void 0
|
|
16140
|
+
);
|
|
15807
16141
|
await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
|
|
15808
16142
|
return 0;
|
|
15809
16143
|
};
|
|
@@ -15811,187 +16145,56 @@ function createGoCommand(dependencies = {}) {
|
|
|
15811
16145
|
var runGoCommand = createGoCommand();
|
|
15812
16146
|
async function promptForWorktree(worktrees) {
|
|
15813
16147
|
const healthResults = await Promise.allSettled(
|
|
15814
|
-
worktrees.map((w2) => readWorktreeHealth(w2.path))
|
|
15815
|
-
);
|
|
15816
|
-
const choice = await ve({
|
|
15817
|
-
message: "Choose a worktree",
|
|
15818
|
-
options: worktrees.map((worktree, i) => {
|
|
15819
|
-
const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
|
|
15820
|
-
const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
|
|
15821
|
-
const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
|
|
15822
|
-
return {
|
|
15823
|
-
value: worktree.path,
|
|
15824
|
-
label: worktree.branch ?? "(detached)",
|
|
15825
|
-
hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
|
|
15826
|
-
};
|
|
15827
|
-
})
|
|
15828
|
-
});
|
|
15829
|
-
if (pD(choice)) {
|
|
15830
|
-
return null;
|
|
15831
|
-
}
|
|
15832
|
-
return choice;
|
|
15833
|
-
}
|
|
15834
|
-
function formatUpstreamHint(branch, health) {
|
|
15835
|
-
if (branch === null) return null;
|
|
15836
|
-
if (!health.hasUpstream) return "no upstream";
|
|
15837
|
-
if (health.upstreamGone) return "upstream gone";
|
|
15838
|
-
if (health.ahead === 0 && health.behind === 0) return "up to date";
|
|
15839
|
-
if (health.ahead === 0) return `behind ${health.behind}`;
|
|
15840
|
-
if (health.behind === 0) return `ahead ${health.ahead}`;
|
|
15841
|
-
return `ahead ${health.ahead}, behind ${health.behind}`;
|
|
15842
|
-
}
|
|
15843
|
-
|
|
15844
|
-
// src/open.ts
|
|
15845
|
-
import { execFile as execFile4 } from "node:child_process";
|
|
15846
|
-
import { access as access4, writeFile as writeFile5 } from "node:fs/promises";
|
|
15847
|
-
import { join as join7 } from "node:path";
|
|
15848
|
-
import { promisify as promisify5 } from "node:util";
|
|
15849
|
-
var execFileAsync4 = promisify5(execFile4);
|
|
15850
|
-
function createOpenCommand(dependencies = {}) {
|
|
15851
|
-
const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
|
|
15852
|
-
const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
|
|
15853
|
-
const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
|
|
15854
|
-
const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
|
|
15855
|
-
return async function runOpenCommand2(options) {
|
|
15856
|
-
const [worktrees, repository] = await Promise.all([
|
|
15857
|
-
listWorktrees(options.cwd),
|
|
15858
|
-
detectRepository(options.cwd)
|
|
15859
|
-
]);
|
|
15860
|
-
let targetPath;
|
|
15861
|
-
if (options.branch) {
|
|
15862
|
-
const entry = worktrees.find((w2) => w2.branch === options.branch);
|
|
15863
|
-
if (!entry) {
|
|
15864
|
-
options.stderr(`gji open: no worktree found for branch: ${options.branch}
|
|
15865
|
-
`);
|
|
15866
|
-
options.stderr(`Hint: Use 'gji ls' to see available worktrees
|
|
15867
|
-
`);
|
|
15868
|
-
return 1;
|
|
15869
|
-
}
|
|
15870
|
-
targetPath = entry.path;
|
|
15871
|
-
} else if (isHeadless()) {
|
|
15872
|
-
targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
|
|
15873
|
-
} else {
|
|
15874
|
-
const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
|
|
15875
|
-
if (!chosen) {
|
|
15876
|
-
options.stderr("Aborted\n");
|
|
15877
|
-
return 1;
|
|
15878
|
-
}
|
|
15879
|
-
targetPath = chosen;
|
|
15880
|
-
}
|
|
15881
|
-
const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
|
|
15882
|
-
const savedEditor = resolveConfigString(config, "editor");
|
|
15883
|
-
let editorCli;
|
|
15884
|
-
if (options.editor) {
|
|
15885
|
-
editorCli = options.editor;
|
|
15886
|
-
} else if (savedEditor) {
|
|
15887
|
-
editorCli = savedEditor;
|
|
15888
|
-
} else {
|
|
15889
|
-
const installed = await detectEditors();
|
|
15890
|
-
if (installed.length === 0) {
|
|
15891
|
-
options.stderr(
|
|
15892
|
-
"gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n"
|
|
15893
|
-
);
|
|
15894
|
-
return 1;
|
|
15895
|
-
}
|
|
15896
|
-
if (installed.length === 1 || isHeadless()) {
|
|
15897
|
-
editorCli = installed[0].cli;
|
|
15898
|
-
} else {
|
|
15899
|
-
const chosen = await promptForEditor(installed);
|
|
15900
|
-
if (!chosen) {
|
|
15901
|
-
options.stderr("Aborted\n");
|
|
15902
|
-
return 1;
|
|
15903
|
-
}
|
|
15904
|
-
editorCli = chosen;
|
|
15905
|
-
}
|
|
15906
|
-
}
|
|
15907
|
-
if (options.save && editorCli !== savedEditor) {
|
|
15908
|
-
await updateGlobalConfigKey("editor", editorCli);
|
|
15909
|
-
const displayName2 = EDITORS.find((e2) => e2.cli === editorCli)?.name ?? editorCli;
|
|
15910
|
-
options.stdout(`Saved editor "${displayName2}" to global config
|
|
15911
|
-
`);
|
|
15912
|
-
}
|
|
15913
|
-
const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
|
|
15914
|
-
let openTarget = targetPath;
|
|
15915
|
-
if (options.workspace) {
|
|
15916
|
-
if (editorDef?.supportsWorkspace) {
|
|
15917
|
-
openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
|
|
15918
|
-
} else {
|
|
15919
|
-
const displayName2 = editorDef?.name ?? editorCli;
|
|
15920
|
-
options.stderr(`gji open: --workspace is not supported for ${displayName2}, ignoring
|
|
15921
|
-
`);
|
|
15922
|
-
}
|
|
15923
|
-
}
|
|
15924
|
-
const args = [];
|
|
15925
|
-
if (editorDef?.newWindowFlag) {
|
|
15926
|
-
args.push(editorDef.newWindowFlag);
|
|
15927
|
-
}
|
|
15928
|
-
args.push(openTarget);
|
|
15929
|
-
try {
|
|
15930
|
-
await spawnEditor(editorCli, args);
|
|
15931
|
-
} catch (error) {
|
|
15932
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
15933
|
-
options.stderr(`gji open: failed to launch editor: ${message}
|
|
15934
|
-
`);
|
|
15935
|
-
return 1;
|
|
15936
|
-
}
|
|
15937
|
-
const displayName = editorDef?.name ?? editorCli;
|
|
15938
|
-
options.stdout(`Opened ${targetPath} in ${displayName}
|
|
15939
|
-
`);
|
|
15940
|
-
return 0;
|
|
15941
|
-
};
|
|
15942
|
-
}
|
|
15943
|
-
var runOpenCommand = createOpenCommand();
|
|
15944
|
-
async function detectInstalledEditors() {
|
|
15945
|
-
const results = await Promise.all(
|
|
15946
|
-
EDITORS.map(async (editor) => ({ editor, available: await isCommandAvailable(editor.cli) }))
|
|
15947
|
-
);
|
|
15948
|
-
return results.filter((r2) => r2.available).map((r2) => r2.editor);
|
|
15949
|
-
}
|
|
15950
|
-
async function isCommandAvailable(command) {
|
|
15951
|
-
try {
|
|
15952
|
-
await execFileAsync4("which", [command]);
|
|
15953
|
-
return true;
|
|
15954
|
-
} catch {
|
|
15955
|
-
return false;
|
|
15956
|
-
}
|
|
15957
|
-
}
|
|
15958
|
-
async function defaultPromptForWorktree(worktrees) {
|
|
15959
|
-
const choice = await ve({
|
|
15960
|
-
message: "Choose a worktree to open",
|
|
15961
|
-
options: worktrees.map((w2) => ({
|
|
15962
|
-
value: w2.path,
|
|
15963
|
-
label: w2.branch ?? "(detached)",
|
|
15964
|
-
hint: w2.isCurrent ? `${w2.path} (current)` : w2.path
|
|
15965
|
-
}))
|
|
15966
|
-
});
|
|
15967
|
-
if (pD(choice)) return null;
|
|
15968
|
-
return choice;
|
|
15969
|
-
}
|
|
15970
|
-
async function defaultPromptForEditor(editors) {
|
|
16148
|
+
worktrees.map((w2) => readWorktreeHealth(w2.path))
|
|
16149
|
+
);
|
|
15971
16150
|
const choice = await ve({
|
|
15972
|
-
message: "Choose
|
|
15973
|
-
options:
|
|
16151
|
+
message: "Choose a worktree",
|
|
16152
|
+
options: worktrees.map((worktree, i) => {
|
|
16153
|
+
const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
|
|
16154
|
+
const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
|
|
16155
|
+
const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
|
|
16156
|
+
return {
|
|
16157
|
+
value: worktree.path,
|
|
16158
|
+
label: worktree.branch ?? "(detached)",
|
|
16159
|
+
hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
|
|
16160
|
+
};
|
|
16161
|
+
})
|
|
15974
16162
|
});
|
|
15975
|
-
if (pD(choice))
|
|
16163
|
+
if (pD(choice)) {
|
|
16164
|
+
return null;
|
|
16165
|
+
}
|
|
15976
16166
|
return choice;
|
|
15977
16167
|
}
|
|
15978
|
-
|
|
15979
|
-
|
|
15980
|
-
|
|
15981
|
-
|
|
15982
|
-
|
|
15983
|
-
|
|
16168
|
+
function formatUpstreamHint(branch, health) {
|
|
16169
|
+
if (branch === null) return null;
|
|
16170
|
+
if (!health.hasUpstream) return "no upstream";
|
|
16171
|
+
if (health.upstreamGone) return "upstream gone";
|
|
16172
|
+
if (health.ahead === 0 && health.behind === 0) return "up to date";
|
|
16173
|
+
if (health.ahead === 0) return `behind ${health.behind}`;
|
|
16174
|
+
if (health.behind === 0) return `ahead ${health.ahead}`;
|
|
16175
|
+
return `ahead ${health.ahead}, behind ${health.behind}`;
|
|
16176
|
+
}
|
|
16177
|
+
|
|
16178
|
+
// src/history-command.ts
|
|
16179
|
+
async function runHistoryCommand(options) {
|
|
16180
|
+
const history = await loadHistory(options.home);
|
|
16181
|
+
if (options.json) {
|
|
16182
|
+
options.stdout(`${JSON.stringify(history, null, 2)}
|
|
16183
|
+
`);
|
|
16184
|
+
return 0;
|
|
15984
16185
|
}
|
|
15985
|
-
|
|
15986
|
-
|
|
15987
|
-
|
|
15988
|
-
|
|
16186
|
+
if (history.length === 0) {
|
|
16187
|
+
options.stdout("No navigation history.\n");
|
|
16188
|
+
return 0;
|
|
16189
|
+
}
|
|
16190
|
+
options.stdout(formatHistoryList(history, options.cwd));
|
|
16191
|
+
return 0;
|
|
15989
16192
|
}
|
|
15990
16193
|
|
|
15991
16194
|
// src/init.ts
|
|
15992
|
-
import { mkdir as mkdir6, readFile as readFile4, writeFile as
|
|
16195
|
+
import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
|
|
15993
16196
|
import { homedir as homedir5 } from "node:os";
|
|
15994
|
-
import { dirname as dirname7, join as
|
|
16197
|
+
import { dirname as dirname7, join as join7 } from "node:path";
|
|
15995
16198
|
var START_MARKER = "# >>> gji init >>>";
|
|
15996
16199
|
var END_MARKER = "# <<< gji init <<<";
|
|
15997
16200
|
var SHELL_WRAPPED_COMMANDS = [
|
|
@@ -16063,7 +16266,7 @@ async function runInitCommand(options) {
|
|
|
16063
16266
|
await mkdir6(dirname7(rcPath), { recursive: true });
|
|
16064
16267
|
const current = await readExistingConfig(rcPath);
|
|
16065
16268
|
const next = upsertShellIntegration(current, script);
|
|
16066
|
-
await
|
|
16269
|
+
await writeFile5(rcPath, next, "utf8");
|
|
16067
16270
|
options.stdout(`${rcPath}
|
|
16068
16271
|
`);
|
|
16069
16272
|
const { config: globalConfig } = await loadGlobalConfig(home);
|
|
@@ -16074,7 +16277,11 @@ async function runInitCommand(options) {
|
|
|
16074
16277
|
const prompt = options.promptForSetup ?? defaultPromptForSetup;
|
|
16075
16278
|
const result = await prompt();
|
|
16076
16279
|
if (result) {
|
|
16077
|
-
await updateGlobalConfigKey(
|
|
16280
|
+
await updateGlobalConfigKey(
|
|
16281
|
+
"installSaveTarget",
|
|
16282
|
+
result.installSaveTarget,
|
|
16283
|
+
home
|
|
16284
|
+
);
|
|
16078
16285
|
await saveWizardConfig(result, options.cwd, home);
|
|
16079
16286
|
}
|
|
16080
16287
|
}
|
|
@@ -16132,9 +16339,12 @@ async function saveWizardConfig(result, cwd, home) {
|
|
|
16132
16339
|
if (result.branchPrefix) values.branchPrefix = result.branchPrefix;
|
|
16133
16340
|
if (result.worktreePath) values.worktreePath = result.worktreePath;
|
|
16134
16341
|
const hooks = {};
|
|
16135
|
-
if (result.hooks?.
|
|
16136
|
-
|
|
16137
|
-
if (result.hooks?.
|
|
16342
|
+
if (result.hooks?.["after-create"])
|
|
16343
|
+
hooks["after-create"] = result.hooks["after-create"];
|
|
16344
|
+
if (result.hooks?.["after-enter"])
|
|
16345
|
+
hooks["after-enter"] = result.hooks["after-enter"];
|
|
16346
|
+
if (result.hooks?.["before-remove"])
|
|
16347
|
+
hooks["before-remove"] = result.hooks["before-remove"];
|
|
16138
16348
|
if (Object.keys(hooks).length > 0) values.hooks = hooks;
|
|
16139
16349
|
if (Object.keys(values).length === 0) return;
|
|
16140
16350
|
if (result.installSaveTarget === "local") {
|
|
@@ -16148,11 +16358,11 @@ async function saveWizardConfig(result, cwd, home) {
|
|
|
16148
16358
|
function resolveShellConfigPath(shell, home) {
|
|
16149
16359
|
switch (shell) {
|
|
16150
16360
|
case "bash":
|
|
16151
|
-
return
|
|
16361
|
+
return join7(home, ".bashrc");
|
|
16152
16362
|
case "fish":
|
|
16153
|
-
return
|
|
16363
|
+
return join7(home, ".config", "fish", "config.fish");
|
|
16154
16364
|
case "zsh":
|
|
16155
|
-
return
|
|
16365
|
+
return join7(home, ".zshrc");
|
|
16156
16366
|
}
|
|
16157
16367
|
}
|
|
16158
16368
|
async function readExistingConfig(path9) {
|
|
@@ -16178,7 +16388,9 @@ function isMissingFileError2(error) {
|
|
|
16178
16388
|
function renderFishWrapper(command) {
|
|
16179
16389
|
const nameTests = command.names.map((name) => `test $argv[1] = ${name}`);
|
|
16180
16390
|
const nameCondition = nameTests.length === 1 ? nameTests[0] : `begin; ${nameTests.join("; or ")}; end`;
|
|
16181
|
-
const bypassTests = command.bypassOptions.map(
|
|
16391
|
+
const bypassTests = command.bypassOptions.map(
|
|
16392
|
+
(opt) => `test $argv[1] = ${opt}`
|
|
16393
|
+
);
|
|
16182
16394
|
const bypassCondition = bypassTests.length === 1 ? bypassTests[0] : `begin; ${bypassTests.join("; or ")}; end`;
|
|
16183
16395
|
return `if test (count $argv) -gt 0; and ${nameCondition}
|
|
16184
16396
|
set -e argv[1]
|
|
@@ -16230,8 +16442,16 @@ async function defaultPromptForSetup() {
|
|
|
16230
16442
|
const installSaveTarget = await ve({
|
|
16231
16443
|
message: "Where should preferences be saved?",
|
|
16232
16444
|
options: [
|
|
16233
|
-
{
|
|
16234
|
-
|
|
16445
|
+
{
|
|
16446
|
+
value: "global",
|
|
16447
|
+
label: "~/.config/gji/config.json",
|
|
16448
|
+
hint: "personal \u2014 never committed"
|
|
16449
|
+
},
|
|
16450
|
+
{
|
|
16451
|
+
value: "local",
|
|
16452
|
+
label: ".gji.json",
|
|
16453
|
+
hint: "repo \u2014 committed with the project"
|
|
16454
|
+
}
|
|
16235
16455
|
]
|
|
16236
16456
|
});
|
|
16237
16457
|
if (pD(installSaveTarget)) {
|
|
@@ -16255,7 +16475,7 @@ async function defaultPromptForSetup() {
|
|
|
16255
16475
|
return null;
|
|
16256
16476
|
}
|
|
16257
16477
|
const afterCreate = await he({
|
|
16258
|
-
message: "
|
|
16478
|
+
message: "after-create hook \u2014 run after creating a worktree?",
|
|
16259
16479
|
placeholder: "e.g. pnpm install \u2014 leave blank to skip"
|
|
16260
16480
|
});
|
|
16261
16481
|
if (pD(afterCreate)) {
|
|
@@ -16263,7 +16483,7 @@ async function defaultPromptForSetup() {
|
|
|
16263
16483
|
return null;
|
|
16264
16484
|
}
|
|
16265
16485
|
const afterEnter = await he({
|
|
16266
|
-
message: "
|
|
16486
|
+
message: "after-enter hook \u2014 run after entering a worktree?",
|
|
16267
16487
|
placeholder: "e.g. nvm use \u2014 leave blank to skip"
|
|
16268
16488
|
});
|
|
16269
16489
|
if (pD(afterEnter)) {
|
|
@@ -16271,7 +16491,7 @@ async function defaultPromptForSetup() {
|
|
|
16271
16491
|
return null;
|
|
16272
16492
|
}
|
|
16273
16493
|
const beforeRemove = await he({
|
|
16274
|
-
message: "
|
|
16494
|
+
message: "before-remove hook \u2014 run before removing a worktree?",
|
|
16275
16495
|
placeholder: "leave blank to skip"
|
|
16276
16496
|
});
|
|
16277
16497
|
if (pD(beforeRemove)) {
|
|
@@ -16280,9 +16500,9 @@ async function defaultPromptForSetup() {
|
|
|
16280
16500
|
}
|
|
16281
16501
|
Se("Setup complete!");
|
|
16282
16502
|
const hooks = {};
|
|
16283
|
-
if (afterCreate) hooks
|
|
16284
|
-
if (afterEnter) hooks
|
|
16285
|
-
if (beforeRemove) hooks
|
|
16503
|
+
if (afterCreate) hooks["after-create"] = afterCreate;
|
|
16504
|
+
if (afterEnter) hooks["after-enter"] = afterEnter;
|
|
16505
|
+
if (beforeRemove) hooks["before-remove"] = beforeRemove;
|
|
16286
16506
|
return {
|
|
16287
16507
|
branchPrefix: branchPrefix || void 0,
|
|
16288
16508
|
hooks: Object.keys(hooks).length > 0 ? hooks : void 0,
|
|
@@ -16334,10 +16554,22 @@ function formatDetailedWorktreeTable(worktrees) {
|
|
|
16334
16554
|
status: worktree.status,
|
|
16335
16555
|
upstream: formatUpstreamState(worktree.upstream)
|
|
16336
16556
|
}));
|
|
16337
|
-
const branchWidth = Math.max(
|
|
16338
|
-
|
|
16339
|
-
|
|
16340
|
-
|
|
16557
|
+
const branchWidth = Math.max(
|
|
16558
|
+
"BRANCH".length,
|
|
16559
|
+
...rows.map((row) => row.branch.length)
|
|
16560
|
+
);
|
|
16561
|
+
const statusWidth = Math.max(
|
|
16562
|
+
"STATUS".length,
|
|
16563
|
+
...rows.map((row) => row.status.length)
|
|
16564
|
+
);
|
|
16565
|
+
const upstreamWidth = Math.max(
|
|
16566
|
+
"UPSTREAM".length,
|
|
16567
|
+
...rows.map((row) => row.upstream.length)
|
|
16568
|
+
);
|
|
16569
|
+
const lastCommitWidth = Math.max(
|
|
16570
|
+
"LAST".length,
|
|
16571
|
+
...rows.map((row) => row.lastCommit.length)
|
|
16572
|
+
);
|
|
16341
16573
|
const lines = [
|
|
16342
16574
|
" " + "BRANCH".padEnd(branchWidth, " ") + " " + "STATUS".padEnd(statusWidth, " ") + " " + "UPSTREAM".padEnd(upstreamWidth, " ") + " " + "LAST".padEnd(lastCommitWidth, " ") + " PATH"
|
|
16343
16575
|
];
|
|
@@ -16360,7 +16592,9 @@ function formatWorktreeTable(worktrees) {
|
|
|
16360
16592
|
);
|
|
16361
16593
|
const lines = [" " + "BRANCH".padEnd(branchWidth, " ") + " PATH"];
|
|
16362
16594
|
for (const row of rows) {
|
|
16363
|
-
lines.push(
|
|
16595
|
+
lines.push(
|
|
16596
|
+
`${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`
|
|
16597
|
+
);
|
|
16364
16598
|
}
|
|
16365
16599
|
return lines.join("\n");
|
|
16366
16600
|
}
|
|
@@ -16372,10 +16606,172 @@ function sortWorktrees(worktrees) {
|
|
|
16372
16606
|
});
|
|
16373
16607
|
}
|
|
16374
16608
|
|
|
16609
|
+
// src/open.ts
|
|
16610
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
16611
|
+
import { access as access4, writeFile as writeFile6 } from "node:fs/promises";
|
|
16612
|
+
import { join as join8 } from "node:path";
|
|
16613
|
+
import { promisify as promisify5 } from "node:util";
|
|
16614
|
+
var execFileAsync4 = promisify5(execFile4);
|
|
16615
|
+
function createOpenCommand(dependencies = {}) {
|
|
16616
|
+
const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
|
|
16617
|
+
const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
|
|
16618
|
+
const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
|
|
16619
|
+
const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
|
|
16620
|
+
return async function runOpenCommand2(options) {
|
|
16621
|
+
const [worktrees, repository] = await Promise.all([
|
|
16622
|
+
listWorktrees(options.cwd),
|
|
16623
|
+
detectRepository(options.cwd)
|
|
16624
|
+
]);
|
|
16625
|
+
let targetPath;
|
|
16626
|
+
if (options.branch) {
|
|
16627
|
+
const entry = worktrees.find((w2) => w2.branch === options.branch);
|
|
16628
|
+
if (!entry) {
|
|
16629
|
+
options.stderr(
|
|
16630
|
+
`gji open: no worktree found for branch: ${options.branch}
|
|
16631
|
+
`
|
|
16632
|
+
);
|
|
16633
|
+
options.stderr(`Hint: Use 'gji ls' to see available worktrees
|
|
16634
|
+
`);
|
|
16635
|
+
return 1;
|
|
16636
|
+
}
|
|
16637
|
+
targetPath = entry.path;
|
|
16638
|
+
} else if (isHeadless()) {
|
|
16639
|
+
targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
|
|
16640
|
+
} else {
|
|
16641
|
+
const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
|
|
16642
|
+
if (!chosen) {
|
|
16643
|
+
options.stderr("Aborted\n");
|
|
16644
|
+
return 1;
|
|
16645
|
+
}
|
|
16646
|
+
targetPath = chosen;
|
|
16647
|
+
}
|
|
16648
|
+
const config = await loadEffectiveConfig(
|
|
16649
|
+
repository.repoRoot,
|
|
16650
|
+
void 0,
|
|
16651
|
+
options.stderr
|
|
16652
|
+
);
|
|
16653
|
+
const savedEditor = resolveConfigString(config, "editor");
|
|
16654
|
+
let editorCli;
|
|
16655
|
+
if (options.editor) {
|
|
16656
|
+
editorCli = options.editor;
|
|
16657
|
+
} else if (savedEditor) {
|
|
16658
|
+
editorCli = savedEditor;
|
|
16659
|
+
} else {
|
|
16660
|
+
const installed = await detectEditors();
|
|
16661
|
+
if (installed.length === 0) {
|
|
16662
|
+
options.stderr(
|
|
16663
|
+
"gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n"
|
|
16664
|
+
);
|
|
16665
|
+
return 1;
|
|
16666
|
+
}
|
|
16667
|
+
if (installed.length === 1 || isHeadless()) {
|
|
16668
|
+
editorCli = installed[0].cli;
|
|
16669
|
+
} else {
|
|
16670
|
+
const chosen = await promptForEditor(installed);
|
|
16671
|
+
if (!chosen) {
|
|
16672
|
+
options.stderr("Aborted\n");
|
|
16673
|
+
return 1;
|
|
16674
|
+
}
|
|
16675
|
+
editorCli = chosen;
|
|
16676
|
+
}
|
|
16677
|
+
}
|
|
16678
|
+
if (options.save && editorCli !== savedEditor) {
|
|
16679
|
+
await updateGlobalConfigKey("editor", editorCli);
|
|
16680
|
+
const displayName2 = EDITORS.find((e2) => e2.cli === editorCli)?.name ?? editorCli;
|
|
16681
|
+
options.stdout(`Saved editor "${displayName2}" to global config
|
|
16682
|
+
`);
|
|
16683
|
+
}
|
|
16684
|
+
const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
|
|
16685
|
+
let openTarget = targetPath;
|
|
16686
|
+
if (options.workspace) {
|
|
16687
|
+
if (editorDef?.supportsWorkspace) {
|
|
16688
|
+
openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
|
|
16689
|
+
} else {
|
|
16690
|
+
const displayName2 = editorDef?.name ?? editorCli;
|
|
16691
|
+
options.stderr(
|
|
16692
|
+
`gji open: --workspace is not supported for ${displayName2}, ignoring
|
|
16693
|
+
`
|
|
16694
|
+
);
|
|
16695
|
+
}
|
|
16696
|
+
}
|
|
16697
|
+
const args = [];
|
|
16698
|
+
if (editorDef?.newWindowFlag) {
|
|
16699
|
+
args.push(editorDef.newWindowFlag);
|
|
16700
|
+
}
|
|
16701
|
+
args.push(openTarget);
|
|
16702
|
+
try {
|
|
16703
|
+
await spawnEditor(editorCli, args);
|
|
16704
|
+
} catch (error) {
|
|
16705
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16706
|
+
options.stderr(`gji open: failed to launch editor: ${message}
|
|
16707
|
+
`);
|
|
16708
|
+
return 1;
|
|
16709
|
+
}
|
|
16710
|
+
const displayName = editorDef?.name ?? editorCli;
|
|
16711
|
+
options.stdout(`Opened ${targetPath} in ${displayName}
|
|
16712
|
+
`);
|
|
16713
|
+
return 0;
|
|
16714
|
+
};
|
|
16715
|
+
}
|
|
16716
|
+
var runOpenCommand = createOpenCommand();
|
|
16717
|
+
async function detectInstalledEditors() {
|
|
16718
|
+
const results = await Promise.all(
|
|
16719
|
+
EDITORS.map(async (editor) => ({
|
|
16720
|
+
editor,
|
|
16721
|
+
available: await isCommandAvailable(editor.cli)
|
|
16722
|
+
}))
|
|
16723
|
+
);
|
|
16724
|
+
return results.filter((r2) => r2.available).map((r2) => r2.editor);
|
|
16725
|
+
}
|
|
16726
|
+
async function isCommandAvailable(command) {
|
|
16727
|
+
try {
|
|
16728
|
+
await execFileAsync4("which", [command]);
|
|
16729
|
+
return true;
|
|
16730
|
+
} catch {
|
|
16731
|
+
return false;
|
|
16732
|
+
}
|
|
16733
|
+
}
|
|
16734
|
+
async function defaultPromptForWorktree(worktrees) {
|
|
16735
|
+
const choice = await ve({
|
|
16736
|
+
message: "Choose a worktree to open",
|
|
16737
|
+
options: worktrees.map((w2) => ({
|
|
16738
|
+
value: w2.path,
|
|
16739
|
+
label: w2.branch ?? "(detached)",
|
|
16740
|
+
hint: w2.isCurrent ? `${w2.path} (current)` : w2.path
|
|
16741
|
+
}))
|
|
16742
|
+
});
|
|
16743
|
+
if (pD(choice)) return null;
|
|
16744
|
+
return choice;
|
|
16745
|
+
}
|
|
16746
|
+
async function defaultPromptForEditor(editors) {
|
|
16747
|
+
const choice = await ve({
|
|
16748
|
+
message: "Choose an editor",
|
|
16749
|
+
options: editors.map((e2) => ({ value: e2.cli, label: e2.name }))
|
|
16750
|
+
});
|
|
16751
|
+
if (pD(choice)) return null;
|
|
16752
|
+
return choice;
|
|
16753
|
+
}
|
|
16754
|
+
async function ensureWorkspaceFile(worktreePath, repoName) {
|
|
16755
|
+
const workspacePath = join8(worktreePath, `${repoName}.code-workspace`);
|
|
16756
|
+
try {
|
|
16757
|
+
await access4(workspacePath);
|
|
16758
|
+
return workspacePath;
|
|
16759
|
+
} catch {
|
|
16760
|
+
}
|
|
16761
|
+
const workspace = { folders: [{ path: "." }], settings: {} };
|
|
16762
|
+
await writeFile6(
|
|
16763
|
+
workspacePath,
|
|
16764
|
+
`${JSON.stringify(workspace, null, 2)}
|
|
16765
|
+
`,
|
|
16766
|
+
"utf8"
|
|
16767
|
+
);
|
|
16768
|
+
return workspacePath;
|
|
16769
|
+
}
|
|
16770
|
+
|
|
16375
16771
|
// src/pr.ts
|
|
16772
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
16376
16773
|
import { mkdir as mkdir7 } from "node:fs/promises";
|
|
16377
16774
|
import { basename as basename7, dirname as dirname8 } from "node:path";
|
|
16378
|
-
import { execFile as execFile5 } from "node:child_process";
|
|
16379
16775
|
import { promisify as promisify6 } from "node:util";
|
|
16380
16776
|
var execFileAsync5 = promisify6(execFile5);
|
|
16381
16777
|
var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
|
|
@@ -16383,7 +16779,9 @@ function parsePrInput(input) {
|
|
|
16383
16779
|
if (/^\d+$/.test(input)) return input;
|
|
16384
16780
|
const hashMatch = input.match(/^#(\d+)$/);
|
|
16385
16781
|
if (hashMatch) return hashMatch[1];
|
|
16386
|
-
const urlMatch = input.match(
|
|
16782
|
+
const urlMatch = input.match(
|
|
16783
|
+
/\/(?:pull|pull-requests|merge_requests)\/(\d+)/
|
|
16784
|
+
);
|
|
16387
16785
|
if (urlMatch) return urlMatch[1];
|
|
16388
16786
|
return null;
|
|
16389
16787
|
}
|
|
@@ -16403,12 +16801,20 @@ function createPrCommand(dependencies = {}) {
|
|
|
16403
16801
|
return 1;
|
|
16404
16802
|
}
|
|
16405
16803
|
const repository = await detectRepository(options.cwd);
|
|
16406
|
-
const config = await loadEffectiveConfig(
|
|
16804
|
+
const config = await loadEffectiveConfig(
|
|
16805
|
+
repository.repoRoot,
|
|
16806
|
+
void 0,
|
|
16807
|
+
options.stderr
|
|
16808
|
+
);
|
|
16407
16809
|
const branchName = `pr/${prNumber}`;
|
|
16408
16810
|
const remoteRef = `refs/remotes/origin/pull/${prNumber}/head`;
|
|
16409
16811
|
const rawBasePath = resolveConfigString(config, "worktreePath");
|
|
16410
16812
|
const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
|
|
16411
|
-
const worktreePath = resolveWorktreePath(
|
|
16813
|
+
const worktreePath = resolveWorktreePath(
|
|
16814
|
+
repository.repoRoot,
|
|
16815
|
+
branchName,
|
|
16816
|
+
configuredBasePath
|
|
16817
|
+
);
|
|
16412
16818
|
if (await pathExists(worktreePath)) {
|
|
16413
16819
|
if (options.json || isHeadless()) {
|
|
16414
16820
|
const message = `target worktree path already exists: ${worktreePath}`;
|
|
@@ -16416,10 +16822,14 @@ function createPrCommand(dependencies = {}) {
|
|
|
16416
16822
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}
|
|
16417
16823
|
`);
|
|
16418
16824
|
} else {
|
|
16419
|
-
options.stderr(
|
|
16420
|
-
`)
|
|
16421
|
-
|
|
16422
|
-
|
|
16825
|
+
options.stderr(
|
|
16826
|
+
`gji pr: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
16827
|
+
`
|
|
16828
|
+
);
|
|
16829
|
+
options.stderr(
|
|
16830
|
+
`Hint: Use 'gji remove pr/${prNumber}' or 'gji clean' to remove the existing worktree
|
|
16831
|
+
`
|
|
16832
|
+
);
|
|
16423
16833
|
}
|
|
16424
16834
|
return 1;
|
|
16425
16835
|
}
|
|
@@ -16429,22 +16839,33 @@ function createPrCommand(dependencies = {}) {
|
|
|
16429
16839
|
await writeOutput2(worktreePath, options.stdout);
|
|
16430
16840
|
return 0;
|
|
16431
16841
|
}
|
|
16432
|
-
options.stderr(
|
|
16433
|
-
`
|
|
16842
|
+
options.stderr(
|
|
16843
|
+
`Aborted because target worktree path already exists: ${worktreePath}
|
|
16844
|
+
`
|
|
16845
|
+
);
|
|
16434
16846
|
return 1;
|
|
16435
16847
|
}
|
|
16436
16848
|
if (options.dryRun) {
|
|
16437
16849
|
if (options.json) {
|
|
16438
|
-
options.stdout(
|
|
16439
|
-
|
|
16850
|
+
options.stdout(
|
|
16851
|
+
`${JSON.stringify({ branch: branchName, path: worktreePath, dryRun: true }, null, 2)}
|
|
16852
|
+
`
|
|
16853
|
+
);
|
|
16440
16854
|
} else {
|
|
16441
|
-
options.stdout(
|
|
16442
|
-
`)
|
|
16855
|
+
options.stdout(
|
|
16856
|
+
`Would create worktree at ${worktreePath} (branch: ${branchName})
|
|
16857
|
+
`
|
|
16858
|
+
);
|
|
16443
16859
|
}
|
|
16444
16860
|
return 0;
|
|
16445
16861
|
}
|
|
16446
16862
|
try {
|
|
16447
|
-
await fetchPullRequestRef(
|
|
16863
|
+
await fetchPullRequestRef(
|
|
16864
|
+
repository.repoRoot,
|
|
16865
|
+
options.number,
|
|
16866
|
+
prNumber,
|
|
16867
|
+
remoteRef
|
|
16868
|
+
);
|
|
16448
16869
|
} catch {
|
|
16449
16870
|
const message = `Failed to fetch PR #${prNumber} from origin`;
|
|
16450
16871
|
if (options.json) {
|
|
@@ -16453,35 +16874,57 @@ function createPrCommand(dependencies = {}) {
|
|
|
16453
16874
|
} else {
|
|
16454
16875
|
options.stderr(`${message}
|
|
16455
16876
|
`);
|
|
16456
|
-
options.stderr(
|
|
16457
|
-
`
|
|
16877
|
+
options.stderr(
|
|
16878
|
+
`Hint: Verify the remote is reachable: git fetch origin
|
|
16879
|
+
`
|
|
16880
|
+
);
|
|
16458
16881
|
}
|
|
16459
16882
|
return 1;
|
|
16460
16883
|
}
|
|
16461
16884
|
await mkdir7(dirname8(worktreePath), { recursive: true });
|
|
16462
|
-
const branchAlreadyExists = await localBranchExists2(
|
|
16885
|
+
const branchAlreadyExists = await localBranchExists2(
|
|
16886
|
+
repository.repoRoot,
|
|
16887
|
+
branchName
|
|
16888
|
+
);
|
|
16463
16889
|
const worktreeArgs = branchAlreadyExists ? ["worktree", "add", worktreePath, branchName] : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
|
|
16464
16890
|
await execFileAsync5("git", worktreeArgs, { cwd: repository.repoRoot });
|
|
16465
|
-
const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
|
|
16891
|
+
const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
|
|
16892
|
+
(p2) => typeof p2 === "string"
|
|
16893
|
+
) : [];
|
|
16466
16894
|
for (const pattern of syncPatterns) {
|
|
16467
16895
|
try {
|
|
16468
16896
|
await syncFiles(repository.repoRoot, worktreePath, [pattern]);
|
|
16469
16897
|
} catch (error) {
|
|
16470
|
-
options.stderr(
|
|
16471
|
-
`)
|
|
16898
|
+
options.stderr(
|
|
16899
|
+
`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
|
|
16900
|
+
`
|
|
16901
|
+
);
|
|
16472
16902
|
}
|
|
16473
16903
|
}
|
|
16474
|
-
await maybeRunInstallPrompt(
|
|
16904
|
+
await maybeRunInstallPrompt(
|
|
16905
|
+
worktreePath,
|
|
16906
|
+
repository.repoRoot,
|
|
16907
|
+
config,
|
|
16908
|
+
options.stderr,
|
|
16909
|
+
dependencies,
|
|
16910
|
+
!!options.json
|
|
16911
|
+
);
|
|
16475
16912
|
const hooks = extractHooks(config);
|
|
16476
16913
|
await runHook(
|
|
16477
|
-
hooks
|
|
16914
|
+
hooks["after-create"],
|
|
16478
16915
|
worktreePath,
|
|
16479
|
-
{
|
|
16916
|
+
{
|
|
16917
|
+
branch: branchName,
|
|
16918
|
+
path: worktreePath,
|
|
16919
|
+
repo: basename7(repository.repoRoot)
|
|
16920
|
+
},
|
|
16480
16921
|
options.stderr
|
|
16481
16922
|
);
|
|
16482
16923
|
if (options.json) {
|
|
16483
|
-
options.stdout(
|
|
16484
|
-
|
|
16924
|
+
options.stdout(
|
|
16925
|
+
`${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}
|
|
16926
|
+
`
|
|
16927
|
+
);
|
|
16485
16928
|
} else {
|
|
16486
16929
|
await appendHistory(worktreePath, branchName);
|
|
16487
16930
|
await writeOutput2(worktreePath, options.stdout);
|
|
@@ -16517,9 +16960,16 @@ async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
|
|
|
16517
16960
|
throw new Error(`No pull request ref found for #${prNumber}`);
|
|
16518
16961
|
}
|
|
16519
16962
|
function listPullRequestSourceRefs(input, prNumber) {
|
|
16520
|
-
const allForges = [
|
|
16963
|
+
const allForges = [
|
|
16964
|
+
"github",
|
|
16965
|
+
"gitlab",
|
|
16966
|
+
"bitbucket"
|
|
16967
|
+
];
|
|
16521
16968
|
const preferredForge = detectPullRequestForge(input);
|
|
16522
|
-
const orderedForges = preferredForge === "unknown" ? allForges : [
|
|
16969
|
+
const orderedForges = preferredForge === "unknown" ? allForges : [
|
|
16970
|
+
preferredForge,
|
|
16971
|
+
...allForges.filter((forge) => forge !== preferredForge)
|
|
16972
|
+
];
|
|
16523
16973
|
return orderedForges.map((forge) => sourceRefForForge(forge, prNumber));
|
|
16524
16974
|
}
|
|
16525
16975
|
function detectPullRequestForge(input) {
|
|
@@ -16557,7 +17007,9 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16557
17007
|
const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
|
|
16558
17008
|
const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
|
|
16559
17009
|
return async function runRemoveCommand2(options) {
|
|
16560
|
-
const { linkedWorktrees, repository } = await loadLinkedWorktrees(
|
|
17010
|
+
const { linkedWorktrees, repository } = await loadLinkedWorktrees(
|
|
17011
|
+
options.cwd
|
|
17012
|
+
);
|
|
16561
17013
|
if (linkedWorktrees.length === 0) {
|
|
16562
17014
|
emitError2(options, "No linked worktrees to finish");
|
|
16563
17015
|
return 1;
|
|
@@ -16567,8 +17019,10 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16567
17019
|
if (options.json) {
|
|
16568
17020
|
emitError2(options, message);
|
|
16569
17021
|
} else {
|
|
16570
|
-
options.stderr(
|
|
16571
|
-
`)
|
|
17022
|
+
options.stderr(
|
|
17023
|
+
`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
17024
|
+
`
|
|
17025
|
+
);
|
|
16572
17026
|
}
|
|
16573
17027
|
return 1;
|
|
16574
17028
|
}
|
|
@@ -16589,8 +17043,10 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16589
17043
|
if (options.json) {
|
|
16590
17044
|
emitError2(options, message);
|
|
16591
17045
|
} else {
|
|
16592
|
-
options.stderr(
|
|
16593
|
-
`)
|
|
17046
|
+
options.stderr(
|
|
17047
|
+
`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
17048
|
+
`
|
|
17049
|
+
);
|
|
16594
17050
|
}
|
|
16595
17051
|
return 1;
|
|
16596
17052
|
}
|
|
@@ -16600,8 +17056,10 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16600
17056
|
}
|
|
16601
17057
|
if (options.dryRun) {
|
|
16602
17058
|
if (options.json) {
|
|
16603
|
-
options.stdout(
|
|
16604
|
-
|
|
17059
|
+
options.stdout(
|
|
17060
|
+
`${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}
|
|
17061
|
+
`
|
|
17062
|
+
);
|
|
16605
17063
|
} else {
|
|
16606
17064
|
const desc = worktree.branch ? `branch: ${worktree.branch}` : "detached";
|
|
16607
17065
|
options.stdout(`Would remove worktree at ${worktree.path} (${desc})
|
|
@@ -16609,12 +17067,20 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16609
17067
|
}
|
|
16610
17068
|
return 0;
|
|
16611
17069
|
}
|
|
16612
|
-
const config = await loadEffectiveConfig(
|
|
17070
|
+
const config = await loadEffectiveConfig(
|
|
17071
|
+
repository.repoRoot,
|
|
17072
|
+
void 0,
|
|
17073
|
+
options.stderr
|
|
17074
|
+
);
|
|
16613
17075
|
const hooks = extractHooks(config);
|
|
16614
17076
|
await runHook(
|
|
16615
|
-
hooks
|
|
17077
|
+
hooks["before-remove"],
|
|
16616
17078
|
worktree.path,
|
|
16617
|
-
{
|
|
17079
|
+
{
|
|
17080
|
+
branch: worktree.branch ?? void 0,
|
|
17081
|
+
path: worktree.path,
|
|
17082
|
+
repo: basename8(repository.repoRoot)
|
|
17083
|
+
},
|
|
16618
17084
|
options.stderr
|
|
16619
17085
|
);
|
|
16620
17086
|
try {
|
|
@@ -16630,7 +17096,10 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16630
17096
|
try {
|
|
16631
17097
|
await forceRemoveWorktree(repository.repoRoot, worktree.path);
|
|
16632
17098
|
} catch (forceError) {
|
|
16633
|
-
emitError2(
|
|
17099
|
+
emitError2(
|
|
17100
|
+
options,
|
|
17101
|
+
`Failed to remove worktree at ${worktree.path}: ${toMessage2(forceError)}`
|
|
17102
|
+
);
|
|
16634
17103
|
return 1;
|
|
16635
17104
|
}
|
|
16636
17105
|
}
|
|
@@ -16645,18 +17114,24 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16645
17114
|
try {
|
|
16646
17115
|
await forceDeleteBranch(repository.repoRoot, worktree.branch);
|
|
16647
17116
|
} catch (forceError) {
|
|
16648
|
-
options.stderr(
|
|
16649
|
-
`)
|
|
17117
|
+
options.stderr(
|
|
17118
|
+
`Failed to delete branch ${worktree.branch}: ${toMessage2(forceError)}
|
|
17119
|
+
`
|
|
17120
|
+
);
|
|
16650
17121
|
}
|
|
16651
17122
|
} else {
|
|
16652
|
-
options.stderr(
|
|
16653
|
-
`)
|
|
17123
|
+
options.stderr(
|
|
17124
|
+
`Branch ${worktree.branch} was not deleted (has unmerged commits)
|
|
17125
|
+
`
|
|
17126
|
+
);
|
|
16654
17127
|
}
|
|
16655
17128
|
}
|
|
16656
17129
|
}
|
|
16657
17130
|
if (options.json) {
|
|
16658
|
-
options.stdout(
|
|
16659
|
-
|
|
17131
|
+
options.stdout(
|
|
17132
|
+
`${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}
|
|
17133
|
+
`
|
|
17134
|
+
);
|
|
16660
17135
|
} else {
|
|
16661
17136
|
await writeOutput3(repository.repoRoot, options.stdout);
|
|
16662
17137
|
}
|
|
@@ -16705,7 +17180,11 @@ var ROOT_OUTPUT_FILE_ENV = "GJI_ROOT_OUTPUT_FILE";
|
|
|
16705
17180
|
async function runRootCommand(options) {
|
|
16706
17181
|
const repository = await detectRepository(options.cwd);
|
|
16707
17182
|
if (!options.print && process.env[ROOT_OUTPUT_FILE_ENV]) {
|
|
16708
|
-
await writeShellOutput(
|
|
17183
|
+
await writeShellOutput(
|
|
17184
|
+
ROOT_OUTPUT_FILE_ENV,
|
|
17185
|
+
repository.repoRoot,
|
|
17186
|
+
options.stdout
|
|
17187
|
+
);
|
|
16709
17188
|
return 0;
|
|
16710
17189
|
}
|
|
16711
17190
|
options.stdout(`${repository.repoRoot}
|
|
@@ -16713,6 +17192,54 @@ async function runRootCommand(options) {
|
|
|
16713
17192
|
return 0;
|
|
16714
17193
|
}
|
|
16715
17194
|
|
|
17195
|
+
// src/run-hook.ts
|
|
17196
|
+
var VALID_HOOKS = [
|
|
17197
|
+
"after-create",
|
|
17198
|
+
"after-enter",
|
|
17199
|
+
"before-remove"
|
|
17200
|
+
];
|
|
17201
|
+
var CAMEL_ALIASES = {
|
|
17202
|
+
afterCreate: "after-create",
|
|
17203
|
+
afterEnter: "after-enter",
|
|
17204
|
+
beforeRemove: "before-remove"
|
|
17205
|
+
};
|
|
17206
|
+
function isValidHook(hook) {
|
|
17207
|
+
return VALID_HOOKS.includes(hook);
|
|
17208
|
+
}
|
|
17209
|
+
async function runHookCommand(options) {
|
|
17210
|
+
const normalized = CAMEL_ALIASES[options.hook] ?? options.hook;
|
|
17211
|
+
if (!isValidHook(normalized)) {
|
|
17212
|
+
options.stderr(
|
|
17213
|
+
`gji run-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(", ")}
|
|
17214
|
+
`
|
|
17215
|
+
);
|
|
17216
|
+
return 1;
|
|
17217
|
+
}
|
|
17218
|
+
const hookName = normalized;
|
|
17219
|
+
const repository = await detectRepository(options.cwd);
|
|
17220
|
+
const config = await loadEffectiveConfig(
|
|
17221
|
+
repository.repoRoot,
|
|
17222
|
+
void 0,
|
|
17223
|
+
options.stderr
|
|
17224
|
+
);
|
|
17225
|
+
const hooks = extractHooks(config);
|
|
17226
|
+
const worktrees = await listWorktrees(options.cwd);
|
|
17227
|
+
const currentWorktree = worktrees.find(
|
|
17228
|
+
(w2) => w2.path === repository.currentRoot
|
|
17229
|
+
);
|
|
17230
|
+
await runHook(
|
|
17231
|
+
hooks[hookName],
|
|
17232
|
+
repository.currentRoot,
|
|
17233
|
+
{
|
|
17234
|
+
branch: currentWorktree?.branch ?? void 0,
|
|
17235
|
+
path: repository.currentRoot,
|
|
17236
|
+
repo: repository.repoName
|
|
17237
|
+
},
|
|
17238
|
+
options.stderr
|
|
17239
|
+
);
|
|
17240
|
+
return 0;
|
|
17241
|
+
}
|
|
17242
|
+
|
|
16716
17243
|
// src/status.ts
|
|
16717
17244
|
async function runStatusCommand(options) {
|
|
16718
17245
|
const repository = await detectRepository(options.cwd);
|
|
@@ -16721,18 +17248,31 @@ async function runStatusCommand(options) {
|
|
|
16721
17248
|
worktrees.map(async (worktree) => buildStatusRow(worktree))
|
|
16722
17249
|
);
|
|
16723
17250
|
if (options.json) {
|
|
16724
|
-
options.stdout(
|
|
16725
|
-
|
|
17251
|
+
options.stdout(
|
|
17252
|
+
`${JSON.stringify(formatStatusJson(repository.repoRoot, repository.currentRoot, rows), null, 2)}
|
|
17253
|
+
`
|
|
17254
|
+
);
|
|
16726
17255
|
return 0;
|
|
16727
17256
|
}
|
|
16728
|
-
options.stdout(
|
|
16729
|
-
|
|
17257
|
+
options.stdout(
|
|
17258
|
+
`${formatStatusOutput(repository.repoRoot, repository.currentRoot, rows)}
|
|
17259
|
+
`
|
|
17260
|
+
);
|
|
16730
17261
|
return 0;
|
|
16731
17262
|
}
|
|
16732
17263
|
function formatStatusOutput(repoRoot, currentRoot, rows) {
|
|
16733
|
-
const currentWidth = Math.max(
|
|
16734
|
-
|
|
16735
|
-
|
|
17264
|
+
const currentWidth = Math.max(
|
|
17265
|
+
"CURRENT".length,
|
|
17266
|
+
...rows.map((row) => row.current ? 1 : 0)
|
|
17267
|
+
);
|
|
17268
|
+
const branchWidth = Math.max(
|
|
17269
|
+
"BRANCH".length,
|
|
17270
|
+
...rows.map((row) => formatBranch(row.branch).length)
|
|
17271
|
+
);
|
|
17272
|
+
const statusWidth = Math.max(
|
|
17273
|
+
"STATUS".length,
|
|
17274
|
+
...rows.map((row) => row.status.length)
|
|
17275
|
+
);
|
|
16736
17276
|
const upstreamWidth = Math.max(
|
|
16737
17277
|
"UPSTREAM".length,
|
|
16738
17278
|
...rows.map((row) => formatUpstreamState2(row.upstream).length)
|
|
@@ -16768,7 +17308,9 @@ async function buildStatusRow(worktree) {
|
|
|
16768
17308
|
};
|
|
16769
17309
|
}
|
|
16770
17310
|
function sortWorktreesByPath(worktrees) {
|
|
16771
|
-
return [...worktrees].sort(
|
|
17311
|
+
return [...worktrees].sort(
|
|
17312
|
+
(left, right) => comparePaths(left.path, right.path)
|
|
17313
|
+
);
|
|
16772
17314
|
}
|
|
16773
17315
|
function formatBranch(branch) {
|
|
16774
17316
|
return branch ?? "(detached)";
|
|
@@ -16814,7 +17356,11 @@ function formatUpstreamState2(upstream) {
|
|
|
16814
17356
|
// src/sync.ts
|
|
16815
17357
|
async function runSyncCommand(options) {
|
|
16816
17358
|
const repository = await detectRepository(options.cwd);
|
|
16817
|
-
const config = await loadEffectiveConfig(
|
|
17359
|
+
const config = await loadEffectiveConfig(
|
|
17360
|
+
repository.repoRoot,
|
|
17361
|
+
void 0,
|
|
17362
|
+
options.stderr
|
|
17363
|
+
);
|
|
16818
17364
|
const worktrees = await listWorktrees(options.cwd);
|
|
16819
17365
|
const remote = resolveConfiguredString2(config.syncRemote) ?? "origin";
|
|
16820
17366
|
let defaultBranch;
|
|
@@ -16823,22 +17369,33 @@ async function runSyncCommand(options) {
|
|
|
16823
17369
|
} catch {
|
|
16824
17370
|
emitError3(options, `Unable to reach remote '${remote}'`);
|
|
16825
17371
|
if (!options.json) {
|
|
16826
|
-
options.stderr(
|
|
16827
|
-
`
|
|
17372
|
+
options.stderr(
|
|
17373
|
+
`Hint: Add the remote with: git remote add ${remote} <url>
|
|
17374
|
+
`
|
|
17375
|
+
);
|
|
16828
17376
|
}
|
|
16829
17377
|
return 1;
|
|
16830
17378
|
}
|
|
16831
17379
|
if (!defaultBranch) {
|
|
16832
17380
|
emitError3(options, "Unable to determine the default branch for sync.");
|
|
16833
17381
|
if (!options.json) {
|
|
16834
|
-
options.stderr(
|
|
16835
|
-
`
|
|
17382
|
+
options.stderr(
|
|
17383
|
+
`Hint: Add the remote with: git remote add ${remote} <url>
|
|
17384
|
+
`
|
|
17385
|
+
);
|
|
16836
17386
|
}
|
|
16837
17387
|
return 1;
|
|
16838
17388
|
}
|
|
16839
|
-
const targetWorktrees = selectTargetWorktrees(
|
|
17389
|
+
const targetWorktrees = selectTargetWorktrees(
|
|
17390
|
+
worktrees,
|
|
17391
|
+
repository.currentRoot,
|
|
17392
|
+
options.all
|
|
17393
|
+
);
|
|
16840
17394
|
if (targetWorktrees === "detached") {
|
|
16841
|
-
emitError3(
|
|
17395
|
+
emitError3(
|
|
17396
|
+
options,
|
|
17397
|
+
`Cannot sync detached worktree: ${repository.currentRoot}`
|
|
17398
|
+
);
|
|
16842
17399
|
return 1;
|
|
16843
17400
|
}
|
|
16844
17401
|
for (const worktree of targetWorktrees) {
|
|
@@ -16852,15 +17409,21 @@ async function runSyncCommand(options) {
|
|
|
16852
17409
|
} catch {
|
|
16853
17410
|
emitError3(options, `Failed to fetch from remote '${remote}'`);
|
|
16854
17411
|
if (!options.json) {
|
|
16855
|
-
options.stderr(
|
|
16856
|
-
`
|
|
17412
|
+
options.stderr(
|
|
17413
|
+
`Hint: Add the remote with: git remote add ${remote} <url>
|
|
17414
|
+
`
|
|
17415
|
+
);
|
|
16857
17416
|
}
|
|
16858
17417
|
return 1;
|
|
16859
17418
|
}
|
|
16860
17419
|
const updatedWorktrees = [];
|
|
16861
17420
|
for (const worktree of targetWorktrees) {
|
|
16862
17421
|
if (worktree.branch === defaultBranch) {
|
|
16863
|
-
await runGit(worktree.path, [
|
|
17422
|
+
await runGit(worktree.path, [
|
|
17423
|
+
"merge",
|
|
17424
|
+
"--ff-only",
|
|
17425
|
+
`${remote}/${defaultBranch}`
|
|
17426
|
+
]);
|
|
16864
17427
|
} else {
|
|
16865
17428
|
await runGit(worktree.path, ["rebase", `${remote}/${defaultBranch}`]);
|
|
16866
17429
|
}
|
|
@@ -16871,7 +17434,10 @@ async function runSyncCommand(options) {
|
|
|
16871
17434
|
}
|
|
16872
17435
|
}
|
|
16873
17436
|
if (options.json) {
|
|
16874
|
-
const updated = updatedWorktrees.map((w2) => ({
|
|
17437
|
+
const updated = updatedWorktrees.map((w2) => ({
|
|
17438
|
+
branch: w2.branch,
|
|
17439
|
+
path: w2.path
|
|
17440
|
+
}));
|
|
16875
17441
|
options.stdout(`${JSON.stringify({ updated }, null, 2)}
|
|
16876
17442
|
`);
|
|
16877
17443
|
}
|
|
@@ -16890,7 +17456,9 @@ function selectTargetWorktrees(worktrees, currentRoot, all) {
|
|
|
16890
17456
|
if (all) {
|
|
16891
17457
|
return worktrees.filter((worktree) => worktree.branch !== null).sort((left, right) => comparePaths(left.path, right.path));
|
|
16892
17458
|
}
|
|
16893
|
-
const currentWorktree = worktrees.find(
|
|
17459
|
+
const currentWorktree = worktrees.find(
|
|
17460
|
+
(worktree) => worktree.path === currentRoot
|
|
17461
|
+
);
|
|
16894
17462
|
if (!currentWorktree) {
|
|
16895
17463
|
return [];
|
|
16896
17464
|
}
|
|
@@ -16903,36 +17471,145 @@ function resolveConfiguredString2(value) {
|
|
|
16903
17471
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
16904
17472
|
}
|
|
16905
17473
|
|
|
16906
|
-
// src/
|
|
16907
|
-
|
|
16908
|
-
|
|
16909
|
-
|
|
16910
|
-
}
|
|
16911
|
-
async function runTriggerHookCommand(options) {
|
|
16912
|
-
if (!isValidHook(options.hook)) {
|
|
16913
|
-
options.stderr(
|
|
16914
|
-
`gji trigger-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(", ")}
|
|
16915
|
-
`
|
|
16916
|
-
);
|
|
16917
|
-
return 1;
|
|
16918
|
-
}
|
|
16919
|
-
const hookName = options.hook;
|
|
17474
|
+
// src/sync-files-command.ts
|
|
17475
|
+
import { homedir as homedir6 } from "node:os";
|
|
17476
|
+
import { join as join9 } from "node:path";
|
|
17477
|
+
async function runSyncFilesCommand(options) {
|
|
16920
17478
|
const repository = await detectRepository(options.cwd);
|
|
16921
|
-
const
|
|
16922
|
-
const
|
|
16923
|
-
const
|
|
16924
|
-
|
|
16925
|
-
|
|
16926
|
-
|
|
16927
|
-
repository.currentRoot,
|
|
16928
|
-
{
|
|
16929
|
-
branch: currentWorktree?.branch ?? void 0,
|
|
16930
|
-
path: repository.currentRoot,
|
|
16931
|
-
repo: repository.repoName
|
|
16932
|
-
},
|
|
16933
|
-
options.stderr
|
|
17479
|
+
const home = options.home ?? homedir6();
|
|
17480
|
+
const loaded = await loadGlobalConfig(home);
|
|
17481
|
+
const repoEntry = findRepoConfigEntry(
|
|
17482
|
+
loaded.config,
|
|
17483
|
+
repository.repoRoot,
|
|
17484
|
+
home
|
|
16934
17485
|
);
|
|
16935
|
-
|
|
17486
|
+
const repoConfig = repoEntry?.config ?? {};
|
|
17487
|
+
switch (options.action) {
|
|
17488
|
+
case void 0:
|
|
17489
|
+
case "list": {
|
|
17490
|
+
writeSyncFiles(options.stdout, readSyncFiles(repoConfig), !!options.json);
|
|
17491
|
+
return 0;
|
|
17492
|
+
}
|
|
17493
|
+
case "add": {
|
|
17494
|
+
const paths = validatePaths(options.paths ?? [], options);
|
|
17495
|
+
if (!paths) return 1;
|
|
17496
|
+
const nextFiles = mergeSyncFiles(readSyncFiles(repoConfig), paths);
|
|
17497
|
+
await saveRepoSyncFiles(
|
|
17498
|
+
loaded.config,
|
|
17499
|
+
repoEntry?.key ?? repository.repoRoot,
|
|
17500
|
+
nextFiles,
|
|
17501
|
+
home
|
|
17502
|
+
);
|
|
17503
|
+
writeSyncFiles(options.stdout, nextFiles, !!options.json);
|
|
17504
|
+
return 0;
|
|
17505
|
+
}
|
|
17506
|
+
case "remove": {
|
|
17507
|
+
const paths = validatePaths(options.paths ?? [], options);
|
|
17508
|
+
if (!paths) return 1;
|
|
17509
|
+
const existingFiles = readSyncFiles(repoConfig);
|
|
17510
|
+
const nextFiles = removeSyncFiles(existingFiles, paths);
|
|
17511
|
+
if (repoEntry && nextFiles.length !== existingFiles.length) {
|
|
17512
|
+
await saveRepoSyncFiles(loaded.config, repoEntry.key, nextFiles, home);
|
|
17513
|
+
}
|
|
17514
|
+
writeSyncFiles(options.stdout, nextFiles, !!options.json);
|
|
17515
|
+
return 0;
|
|
17516
|
+
}
|
|
17517
|
+
}
|
|
17518
|
+
writeError(options, `unknown action: ${options.action}`);
|
|
17519
|
+
return 1;
|
|
17520
|
+
}
|
|
17521
|
+
function findRepoConfigEntry(config, repoRoot, home) {
|
|
17522
|
+
const repos = config.repos;
|
|
17523
|
+
if (!isPlainObject3(repos)) return null;
|
|
17524
|
+
for (const [key, value] of Object.entries(repos)) {
|
|
17525
|
+
if (expandTilde2(key, home) === repoRoot && isPlainObject3(value)) {
|
|
17526
|
+
return { config: value, key };
|
|
17527
|
+
}
|
|
17528
|
+
}
|
|
17529
|
+
return null;
|
|
17530
|
+
}
|
|
17531
|
+
function readSyncFiles(config) {
|
|
17532
|
+
const syncFiles2 = config.syncFiles;
|
|
17533
|
+
if (!Array.isArray(syncFiles2)) return [];
|
|
17534
|
+
return syncFiles2.filter((item) => typeof item === "string");
|
|
17535
|
+
}
|
|
17536
|
+
function writeSyncFiles(stdout, files, json) {
|
|
17537
|
+
if (json) {
|
|
17538
|
+
stdout(`${JSON.stringify(files, null, 2)}
|
|
17539
|
+
`);
|
|
17540
|
+
return;
|
|
17541
|
+
}
|
|
17542
|
+
if (files.length === 0) {
|
|
17543
|
+
stdout("No sync files configured for this repo.\n");
|
|
17544
|
+
return;
|
|
17545
|
+
}
|
|
17546
|
+
stdout(`${files.join("\n")}
|
|
17547
|
+
`);
|
|
17548
|
+
}
|
|
17549
|
+
function validatePaths(paths, options) {
|
|
17550
|
+
if (paths.length === 0) {
|
|
17551
|
+
writeError(options, "at least one path is required");
|
|
17552
|
+
return null;
|
|
17553
|
+
}
|
|
17554
|
+
const validatedPaths = [];
|
|
17555
|
+
for (const path9 of paths) {
|
|
17556
|
+
try {
|
|
17557
|
+
validatedPaths.push(validateSyncFilePattern(path9));
|
|
17558
|
+
} catch (error) {
|
|
17559
|
+
writeError(
|
|
17560
|
+
options,
|
|
17561
|
+
error instanceof Error ? error.message : String(error)
|
|
17562
|
+
);
|
|
17563
|
+
return null;
|
|
17564
|
+
}
|
|
17565
|
+
}
|
|
17566
|
+
return validatedPaths;
|
|
17567
|
+
}
|
|
17568
|
+
function mergeSyncFiles(existing, additions) {
|
|
17569
|
+
const nextFiles = [...existing];
|
|
17570
|
+
for (const path9 of additions) {
|
|
17571
|
+
if (!nextFiles.includes(path9)) {
|
|
17572
|
+
nextFiles.push(path9);
|
|
17573
|
+
}
|
|
17574
|
+
}
|
|
17575
|
+
return nextFiles;
|
|
17576
|
+
}
|
|
17577
|
+
function removeSyncFiles(existing, removals) {
|
|
17578
|
+
const removalSet = new Set(removals);
|
|
17579
|
+
return existing.filter((path9) => !removalSet.has(path9));
|
|
17580
|
+
}
|
|
17581
|
+
async function saveRepoSyncFiles(config, repoKey, syncFiles2, home) {
|
|
17582
|
+
const repos = isPlainObject3(config.repos) ? { ...config.repos } : {};
|
|
17583
|
+
const repoConfig = isPlainObject3(repos[repoKey]) ? repos[repoKey] : {};
|
|
17584
|
+
const nextRepoConfig = { ...repoConfig };
|
|
17585
|
+
if (syncFiles2.length > 0) {
|
|
17586
|
+
nextRepoConfig.syncFiles = syncFiles2;
|
|
17587
|
+
} else {
|
|
17588
|
+
delete nextRepoConfig.syncFiles;
|
|
17589
|
+
}
|
|
17590
|
+
if (Object.keys(nextRepoConfig).length > 0) {
|
|
17591
|
+
repos[repoKey] = nextRepoConfig;
|
|
17592
|
+
} else {
|
|
17593
|
+
delete repos[repoKey];
|
|
17594
|
+
}
|
|
17595
|
+
await saveGlobalConfig({ ...config, repos }, home);
|
|
17596
|
+
}
|
|
17597
|
+
function isPlainObject3(value) {
|
|
17598
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17599
|
+
}
|
|
17600
|
+
function writeError(options, message) {
|
|
17601
|
+
if (options.json) {
|
|
17602
|
+
options.stderr(`${JSON.stringify({ error: message }, null, 2)}
|
|
17603
|
+
`);
|
|
17604
|
+
return;
|
|
17605
|
+
}
|
|
17606
|
+
options.stderr(`gji sync-files: ${message}
|
|
17607
|
+
`);
|
|
17608
|
+
}
|
|
17609
|
+
function expandTilde2(value, home) {
|
|
17610
|
+
if (value === "~") return home;
|
|
17611
|
+
if (value.startsWith("~/")) return join9(home, value.slice(2));
|
|
17612
|
+
return value;
|
|
16936
17613
|
}
|
|
16937
17614
|
|
|
16938
17615
|
// src/cli.ts
|
|
@@ -17001,46 +17678,117 @@ function maybeRegisterCurrentRepo(cwd) {
|
|
|
17001
17678
|
detectRepository(cwd).then(({ repoRoot }) => registerRepo(repoRoot)).catch(() => void 0);
|
|
17002
17679
|
}
|
|
17003
17680
|
function registerCommands(program2) {
|
|
17004
|
-
program2.command("new [branch]").description("create a new branch or detached linked worktree").option(
|
|
17681
|
+
program2.command("new [branch]").description("create a new branch or detached linked worktree").option(
|
|
17682
|
+
"-f, --force",
|
|
17683
|
+
"remove and recreate the worktree if the target path already exists"
|
|
17684
|
+
).option("--detached", "create a detached worktree without a branch").option("--open", "open the new worktree in an editor after creation").option(
|
|
17685
|
+
"--editor <cli>",
|
|
17686
|
+
"editor CLI to use with --open (code, cursor, zed, \u2026)"
|
|
17687
|
+
).option(
|
|
17688
|
+
"--dry-run",
|
|
17689
|
+
"show what would be created without executing any git commands or writing files"
|
|
17690
|
+
).option(
|
|
17691
|
+
"--json",
|
|
17692
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17693
|
+
).action(notImplemented("new"));
|
|
17005
17694
|
program2.command("init [shell]").description("print or install shell integration").option("--write", "write the integration to the shell config file").action(notImplemented("init"));
|
|
17006
17695
|
program2.command("completion [shell]").description("print shell completion definitions").action(notImplemented("completion"));
|
|
17007
|
-
program2.command("pr <ref>").description(
|
|
17008
|
-
|
|
17696
|
+
program2.command("pr <ref>").description(
|
|
17697
|
+
"fetch a pull request by number, #number, or URL into a linked worktree"
|
|
17698
|
+
).option(
|
|
17699
|
+
"--dry-run",
|
|
17700
|
+
"show what would be created without executing any git commands or writing files"
|
|
17701
|
+
).option(
|
|
17702
|
+
"--json",
|
|
17703
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17704
|
+
).action(notImplemented("pr"));
|
|
17705
|
+
program2.command("back [n]").description(
|
|
17706
|
+
"navigate to the previously visited worktree, optionally N steps back"
|
|
17707
|
+
).option("--print", "print the resolved worktree path explicitly").action(notImplemented("back"));
|
|
17009
17708
|
program2.command("history").description("show navigation history").option("--json", "print history as JSON").action(notImplemented("history"));
|
|
17010
|
-
program2.command("open [branch]").description("open the worktree in an editor").option(
|
|
17709
|
+
program2.command("open [branch]").description("open the worktree in an editor").option(
|
|
17710
|
+
"--editor <cli>",
|
|
17711
|
+
"editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)"
|
|
17712
|
+
).option("--save", "save the chosen editor to global config").option(
|
|
17713
|
+
"--workspace",
|
|
17714
|
+
"generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)"
|
|
17715
|
+
).action(notImplemented("open"));
|
|
17011
17716
|
program2.command("go [branch]").alias("jump").description("print or select a worktree path").option("--print", "print the resolved worktree path explicitly").action(notImplemented("go"));
|
|
17012
17717
|
program2.command("root").description("print the main repository root path").option("--print", "print the resolved repository root path explicitly").action(notImplemented("root"));
|
|
17013
17718
|
program2.command("status").description("summarize repository and worktree health").option("--json", "print repository and worktree health as JSON").action(notImplemented("status"));
|
|
17014
|
-
program2.command("sync").description("fetch and update one or all worktrees").option("--all", "sync every worktree in the repository").option(
|
|
17719
|
+
program2.command("sync").description("fetch and update one or all worktrees").option("--all", "sync every worktree in the repository").option(
|
|
17720
|
+
"--json",
|
|
17721
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17722
|
+
).action(notImplemented("sync"));
|
|
17723
|
+
const syncFilesCommand = program2.command("sync-files").description("manage local files copied into new worktrees").option("--json", "emit JSON instead of human-readable output").action(notImplemented("sync-files"));
|
|
17724
|
+
syncFilesCommand.command("list").description("list files synced into new worktrees for this repo").option("--json", "emit JSON instead of human-readable output").action(notImplemented("sync-files list"));
|
|
17725
|
+
syncFilesCommand.command("add <paths...>").description("add repo-local sync files to global config").option("--json", "emit JSON instead of human-readable output").action(notImplemented("sync-files add"));
|
|
17726
|
+
syncFilesCommand.command("remove <paths...>").alias("rm").description("remove repo-local sync files from global config").option("--json", "emit JSON instead of human-readable output").action(notImplemented("sync-files remove"));
|
|
17015
17727
|
program2.command("ls").description("list active worktrees").option("--compact", "show only branch and path columns").option("--json", "print active worktrees as JSON").action(notImplemented("ls"));
|
|
17016
|
-
program2.command("clean").description("interactively prune linked worktrees").option(
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
17728
|
+
program2.command("clean").description("interactively prune linked worktrees").option(
|
|
17729
|
+
"-f, --force",
|
|
17730
|
+
"bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches"
|
|
17731
|
+
).option(
|
|
17732
|
+
"--stale",
|
|
17733
|
+
"only target clean worktrees whose upstream is gone and branch is merged into the default branch"
|
|
17734
|
+
).option("--dry-run", "show what would be deleted without removing anything").option(
|
|
17735
|
+
"--json",
|
|
17736
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17737
|
+
).action(notImplemented("clean"));
|
|
17738
|
+
program2.command("remove [branch]").alias("rm").description("remove a linked worktree and delete its branch when present").option(
|
|
17739
|
+
"-f, --force",
|
|
17740
|
+
"bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch"
|
|
17741
|
+
).option("--dry-run", "show what would be deleted without removing anything").option(
|
|
17742
|
+
"--json",
|
|
17743
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17744
|
+
).action(notImplemented("remove"));
|
|
17745
|
+
program2.command("run-hook <hook>").alias("trigger-hook").description(
|
|
17746
|
+
"run a named hook (after-create, after-enter, before-remove) in the current worktree"
|
|
17747
|
+
).action(notImplemented("run-hook"));
|
|
17748
|
+
program2.command("warp [branch]").description("jump to any worktree across all known repos").option("-n, --new [branch]", "create a new worktree in a registered repo").option(
|
|
17749
|
+
"--print",
|
|
17750
|
+
"print the resolved worktree path without changing directory"
|
|
17751
|
+
).option(
|
|
17752
|
+
"--json",
|
|
17753
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17754
|
+
).action(notImplemented("warp"));
|
|
17020
17755
|
const configCommand = program2.command("config").description("manage global config defaults").action(notImplemented("config"));
|
|
17021
17756
|
configCommand.command("get [key]").description("print the global config or a single key").action(notImplemented("config get"));
|
|
17022
17757
|
configCommand.command("set <key> <value>").description("set a global config value").action(notImplemented("config set"));
|
|
17023
17758
|
configCommand.command("unset <key>").description("remove a global config value").action(notImplemented("config unset"));
|
|
17024
17759
|
}
|
|
17025
17760
|
function attachCommandActions(program2, options) {
|
|
17026
|
-
program2.commands.find((command) => command.name() === "new")?.action(
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17761
|
+
program2.commands.find((command) => command.name() === "new")?.action(
|
|
17762
|
+
async (branch, commandOptions) => {
|
|
17763
|
+
const exitCode = await runNewCommand({
|
|
17764
|
+
...options,
|
|
17765
|
+
branch,
|
|
17766
|
+
detached: commandOptions.detached,
|
|
17767
|
+
dryRun: commandOptions.dryRun,
|
|
17768
|
+
editor: commandOptions.editor,
|
|
17769
|
+
force: commandOptions.force,
|
|
17770
|
+
json: commandOptions.json,
|
|
17771
|
+
open: commandOptions.open
|
|
17772
|
+
});
|
|
17773
|
+
if (exitCode !== 0) {
|
|
17774
|
+
throw commanderExit(exitCode);
|
|
17775
|
+
}
|
|
17030
17776
|
}
|
|
17031
|
-
|
|
17032
|
-
program2.commands.find((command) => command.name() === "init")?.action(
|
|
17033
|
-
|
|
17034
|
-
|
|
17035
|
-
|
|
17036
|
-
|
|
17037
|
-
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17777
|
+
);
|
|
17778
|
+
program2.commands.find((command) => command.name() === "init")?.action(
|
|
17779
|
+
async (shell, commandOptions) => {
|
|
17780
|
+
const exitCode = await runInitCommand({
|
|
17781
|
+
cwd: options.cwd,
|
|
17782
|
+
shell,
|
|
17783
|
+
stderr: options.stderr,
|
|
17784
|
+
stdout: options.stdout,
|
|
17785
|
+
write: commandOptions.write
|
|
17786
|
+
});
|
|
17787
|
+
if (exitCode !== 0) {
|
|
17788
|
+
throw commanderExit(exitCode);
|
|
17789
|
+
}
|
|
17042
17790
|
}
|
|
17043
|
-
|
|
17791
|
+
);
|
|
17044
17792
|
program2.commands.find((command) => command.name() === "completion")?.action(async (shell) => {
|
|
17045
17793
|
const exitCode = await runCompletionCommand({
|
|
17046
17794
|
shell,
|
|
@@ -17051,32 +17799,93 @@ function attachCommandActions(program2, options) {
|
|
|
17051
17799
|
throw commanderExit(exitCode);
|
|
17052
17800
|
}
|
|
17053
17801
|
});
|
|
17054
|
-
program2.commands.find((command) => command.name() === "pr")?.action(
|
|
17055
|
-
|
|
17802
|
+
program2.commands.find((command) => command.name() === "pr")?.action(
|
|
17803
|
+
async (number, commandOptions) => {
|
|
17804
|
+
const exitCode = await runPrCommand({
|
|
17805
|
+
cwd: options.cwd,
|
|
17806
|
+
dryRun: commandOptions.dryRun,
|
|
17807
|
+
json: commandOptions.json,
|
|
17808
|
+
number,
|
|
17809
|
+
stderr: options.stderr,
|
|
17810
|
+
stdout: options.stdout
|
|
17811
|
+
});
|
|
17812
|
+
if (exitCode !== 0) {
|
|
17813
|
+
throw commanderExit(exitCode);
|
|
17814
|
+
}
|
|
17815
|
+
}
|
|
17816
|
+
);
|
|
17817
|
+
program2.commands.find((command) => command.name() === "back")?.action(
|
|
17818
|
+
async (n, commandOptions) => {
|
|
17819
|
+
if (n !== void 0 && !/^\d+$/.test(n)) {
|
|
17820
|
+
options.stderr(`gji back: invalid step count: ${n}
|
|
17821
|
+
`);
|
|
17822
|
+
throw commanderExit(1);
|
|
17823
|
+
}
|
|
17824
|
+
const steps = n !== void 0 ? parseInt(n, 10) : void 0;
|
|
17825
|
+
const exitCode = await runBackCommand({
|
|
17826
|
+
cwd: options.cwd,
|
|
17827
|
+
n: steps,
|
|
17828
|
+
print: commandOptions.print,
|
|
17829
|
+
stderr: options.stderr,
|
|
17830
|
+
stdout: options.stdout
|
|
17831
|
+
});
|
|
17832
|
+
if (exitCode !== 0) {
|
|
17833
|
+
throw commanderExit(exitCode);
|
|
17834
|
+
}
|
|
17835
|
+
}
|
|
17836
|
+
);
|
|
17837
|
+
program2.commands.find((command) => command.name() === "history")?.action(async (commandOptions) => {
|
|
17838
|
+
const exitCode = await runHistoryCommand({
|
|
17839
|
+
cwd: options.cwd,
|
|
17840
|
+
json: commandOptions.json,
|
|
17841
|
+
stdout: options.stdout
|
|
17842
|
+
});
|
|
17056
17843
|
if (exitCode !== 0) {
|
|
17057
17844
|
throw commanderExit(exitCode);
|
|
17058
17845
|
}
|
|
17059
17846
|
});
|
|
17060
|
-
program2.commands.find((command) => command.name() === "
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17064
|
-
|
|
17847
|
+
program2.commands.find((command) => command.name() === "open")?.action(
|
|
17848
|
+
async (branch, commandOptions) => {
|
|
17849
|
+
const exitCode = await runOpenCommand({
|
|
17850
|
+
branch,
|
|
17851
|
+
cwd: options.cwd,
|
|
17852
|
+
editor: commandOptions.editor,
|
|
17853
|
+
save: commandOptions.save,
|
|
17854
|
+
stderr: options.stderr,
|
|
17855
|
+
stdout: options.stdout,
|
|
17856
|
+
workspace: commandOptions.workspace
|
|
17857
|
+
});
|
|
17858
|
+
if (exitCode !== 0) {
|
|
17859
|
+
throw commanderExit(exitCode);
|
|
17860
|
+
}
|
|
17861
|
+
}
|
|
17862
|
+
);
|
|
17863
|
+
program2.commands.find((command) => command.name() === "go")?.action(
|
|
17864
|
+
async (branch, commandOptions) => {
|
|
17865
|
+
const exitCode = await runGoCommand({
|
|
17866
|
+
branch,
|
|
17867
|
+
cwd: options.cwd,
|
|
17868
|
+
print: commandOptions.print,
|
|
17869
|
+
stderr: options.stderr,
|
|
17870
|
+
stdout: options.stdout
|
|
17871
|
+
});
|
|
17872
|
+
if (exitCode !== 0) {
|
|
17873
|
+
throw commanderExit(exitCode);
|
|
17874
|
+
}
|
|
17065
17875
|
}
|
|
17066
|
-
|
|
17067
|
-
|
|
17876
|
+
);
|
|
17877
|
+
program2.commands.find((command) => command.name() === "root")?.action(async (commandOptions) => {
|
|
17878
|
+
const exitCode = await runRootCommand({
|
|
17068
17879
|
cwd: options.cwd,
|
|
17069
|
-
n: steps,
|
|
17070
17880
|
print: commandOptions.print,
|
|
17071
|
-
stderr: options.stderr,
|
|
17072
17881
|
stdout: options.stdout
|
|
17073
17882
|
});
|
|
17074
17883
|
if (exitCode !== 0) {
|
|
17075
17884
|
throw commanderExit(exitCode);
|
|
17076
17885
|
}
|
|
17077
17886
|
});
|
|
17078
|
-
program2.commands.find((command) => command.name() === "
|
|
17079
|
-
const exitCode = await
|
|
17887
|
+
program2.commands.find((command) => command.name() === "status")?.action(async (commandOptions) => {
|
|
17888
|
+
const exitCode = await runStatusCommand({
|
|
17080
17889
|
cwd: options.cwd,
|
|
17081
17890
|
json: commandOptions.json,
|
|
17082
17891
|
stdout: options.stdout
|
|
@@ -17085,25 +17894,26 @@ function attachCommandActions(program2, options) {
|
|
|
17085
17894
|
throw commanderExit(exitCode);
|
|
17086
17895
|
}
|
|
17087
17896
|
});
|
|
17088
|
-
program2.commands.find((command) => command.name() === "
|
|
17089
|
-
const exitCode = await
|
|
17090
|
-
|
|
17897
|
+
program2.commands.find((command) => command.name() === "sync")?.action(async (commandOptions) => {
|
|
17898
|
+
const exitCode = await runSyncCommand({
|
|
17899
|
+
all: commandOptions.all,
|
|
17091
17900
|
cwd: options.cwd,
|
|
17092
|
-
|
|
17093
|
-
save: commandOptions.save,
|
|
17901
|
+
json: commandOptions.json,
|
|
17094
17902
|
stderr: options.stderr,
|
|
17095
|
-
stdout: options.stdout
|
|
17096
|
-
workspace: commandOptions.workspace
|
|
17903
|
+
stdout: options.stdout
|
|
17097
17904
|
});
|
|
17098
17905
|
if (exitCode !== 0) {
|
|
17099
17906
|
throw commanderExit(exitCode);
|
|
17100
17907
|
}
|
|
17101
17908
|
});
|
|
17102
|
-
program2.commands.find(
|
|
17103
|
-
|
|
17104
|
-
|
|
17909
|
+
const syncFilesCommand = program2.commands.find(
|
|
17910
|
+
(command) => command.name() === "sync-files"
|
|
17911
|
+
);
|
|
17912
|
+
syncFilesCommand?.action(async (commandOptions) => {
|
|
17913
|
+
const exitCode = await runSyncFilesCommand({
|
|
17914
|
+
action: "list",
|
|
17105
17915
|
cwd: options.cwd,
|
|
17106
|
-
|
|
17916
|
+
json: commandOptions.json,
|
|
17107
17917
|
stderr: options.stderr,
|
|
17108
17918
|
stdout: options.stdout
|
|
17109
17919
|
});
|
|
@@ -17111,38 +17921,45 @@ function attachCommandActions(program2, options) {
|
|
|
17111
17921
|
throw commanderExit(exitCode);
|
|
17112
17922
|
}
|
|
17113
17923
|
});
|
|
17114
|
-
|
|
17115
|
-
const exitCode = await
|
|
17924
|
+
syncFilesCommand?.commands.find((command) => command.name() === "list")?.action(async (commandOptions) => {
|
|
17925
|
+
const exitCode = await runSyncFilesCommand({
|
|
17926
|
+
action: "list",
|
|
17116
17927
|
cwd: options.cwd,
|
|
17117
|
-
|
|
17928
|
+
json: commandOptions.json || syncFilesCommand?.opts().json,
|
|
17929
|
+
stderr: options.stderr,
|
|
17118
17930
|
stdout: options.stdout
|
|
17119
17931
|
});
|
|
17120
17932
|
if (exitCode !== 0) {
|
|
17121
17933
|
throw commanderExit(exitCode);
|
|
17122
17934
|
}
|
|
17123
17935
|
});
|
|
17124
|
-
|
|
17125
|
-
const exitCode = await
|
|
17936
|
+
syncFilesCommand?.commands.find((command) => command.name() === "add")?.action(async (paths, commandOptions) => {
|
|
17937
|
+
const exitCode = await runSyncFilesCommand({
|
|
17938
|
+
action: "add",
|
|
17126
17939
|
cwd: options.cwd,
|
|
17127
|
-
json: commandOptions.json,
|
|
17940
|
+
json: commandOptions.json || syncFilesCommand?.opts().json,
|
|
17941
|
+
paths,
|
|
17942
|
+
stderr: options.stderr,
|
|
17128
17943
|
stdout: options.stdout
|
|
17129
17944
|
});
|
|
17130
17945
|
if (exitCode !== 0) {
|
|
17131
17946
|
throw commanderExit(exitCode);
|
|
17132
17947
|
}
|
|
17133
17948
|
});
|
|
17134
|
-
|
|
17135
|
-
const exitCode = await
|
|
17136
|
-
|
|
17949
|
+
const runSyncFilesRemoveCommand = async (paths, commandOptions) => {
|
|
17950
|
+
const exitCode = await runSyncFilesCommand({
|
|
17951
|
+
action: "remove",
|
|
17137
17952
|
cwd: options.cwd,
|
|
17138
|
-
json: commandOptions.json,
|
|
17953
|
+
json: commandOptions.json || syncFilesCommand?.opts().json,
|
|
17954
|
+
paths,
|
|
17139
17955
|
stderr: options.stderr,
|
|
17140
17956
|
stdout: options.stdout
|
|
17141
17957
|
});
|
|
17142
17958
|
if (exitCode !== 0) {
|
|
17143
17959
|
throw commanderExit(exitCode);
|
|
17144
17960
|
}
|
|
17145
|
-
}
|
|
17961
|
+
};
|
|
17962
|
+
syncFilesCommand?.commands.find((command) => command.name() === "remove")?.action(runSyncFilesRemoveCommand);
|
|
17146
17963
|
program2.commands.find((command) => command.name() === "ls")?.action(async (commandOptions) => {
|
|
17147
17964
|
const exitCode = await runLsCommand({
|
|
17148
17965
|
compact: commandOptions.compact,
|
|
@@ -17154,20 +17971,22 @@ function attachCommandActions(program2, options) {
|
|
|
17154
17971
|
throw commanderExit(exitCode);
|
|
17155
17972
|
}
|
|
17156
17973
|
});
|
|
17157
|
-
program2.commands.find((command) => command.name() === "clean")?.action(
|
|
17158
|
-
|
|
17159
|
-
|
|
17160
|
-
|
|
17161
|
-
|
|
17162
|
-
|
|
17163
|
-
|
|
17164
|
-
|
|
17165
|
-
|
|
17166
|
-
|
|
17167
|
-
|
|
17168
|
-
|
|
17974
|
+
program2.commands.find((command) => command.name() === "clean")?.action(
|
|
17975
|
+
async (commandOptions) => {
|
|
17976
|
+
const exitCode = await runCleanCommand({
|
|
17977
|
+
cwd: options.cwd,
|
|
17978
|
+
dryRun: commandOptions.dryRun,
|
|
17979
|
+
force: commandOptions.force,
|
|
17980
|
+
json: commandOptions.json,
|
|
17981
|
+
stale: commandOptions.stale,
|
|
17982
|
+
stderr: options.stderr,
|
|
17983
|
+
stdout: options.stdout
|
|
17984
|
+
});
|
|
17985
|
+
if (exitCode !== 0) {
|
|
17986
|
+
throw commanderExit(exitCode);
|
|
17987
|
+
}
|
|
17169
17988
|
}
|
|
17170
|
-
|
|
17989
|
+
);
|
|
17171
17990
|
const runRemovalCommand = async (branch, commandOptions = {}) => {
|
|
17172
17991
|
const exitCode = await runRemoveCommand({
|
|
17173
17992
|
branch,
|
|
@@ -17183,8 +18002,8 @@ function attachCommandActions(program2, options) {
|
|
|
17183
18002
|
}
|
|
17184
18003
|
};
|
|
17185
18004
|
program2.commands.find((command) => command.name() === "remove")?.action(runRemovalCommand);
|
|
17186
|
-
program2.commands.find((command) => command.name() === "
|
|
17187
|
-
const exitCode = await
|
|
18005
|
+
program2.commands.find((command) => command.name() === "run-hook")?.action(async (hook) => {
|
|
18006
|
+
const exitCode = await runHookCommand({
|
|
17188
18007
|
cwd: options.cwd,
|
|
17189
18008
|
hook,
|
|
17190
18009
|
stderr: options.stderr
|
|
@@ -17193,23 +18012,27 @@ function attachCommandActions(program2, options) {
|
|
|
17193
18012
|
throw commanderExit(exitCode);
|
|
17194
18013
|
}
|
|
17195
18014
|
});
|
|
17196
|
-
program2.commands.find((command) => command.name() === "warp")?.action(
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
17200
|
-
|
|
17201
|
-
|
|
17202
|
-
|
|
17203
|
-
|
|
17204
|
-
|
|
17205
|
-
|
|
17206
|
-
|
|
17207
|
-
|
|
17208
|
-
|
|
17209
|
-
|
|
18015
|
+
program2.commands.find((command) => command.name() === "warp")?.action(
|
|
18016
|
+
async (branch, commandOptions) => {
|
|
18017
|
+
const newFlag = commandOptions.new;
|
|
18018
|
+
const newWorktree = newFlag !== void 0 && newFlag !== false;
|
|
18019
|
+
const newBranch = typeof newFlag === "string" ? newFlag : void 0;
|
|
18020
|
+
const exitCode = await runWarpCommand({
|
|
18021
|
+
branch: newWorktree ? newBranch ?? branch : branch,
|
|
18022
|
+
cwd: options.cwd,
|
|
18023
|
+
json: commandOptions.json,
|
|
18024
|
+
newWorktree,
|
|
18025
|
+
stderr: options.stderr,
|
|
18026
|
+
stdout: options.stdout
|
|
18027
|
+
});
|
|
18028
|
+
if (exitCode !== 0) {
|
|
18029
|
+
throw commanderExit(exitCode);
|
|
18030
|
+
}
|
|
17210
18031
|
}
|
|
17211
|
-
|
|
17212
|
-
const configCommand = program2.commands.find(
|
|
18032
|
+
);
|
|
18033
|
+
const configCommand = program2.commands.find(
|
|
18034
|
+
(command) => command.name() === "config"
|
|
18035
|
+
);
|
|
17213
18036
|
configCommand?.action(async () => {
|
|
17214
18037
|
const exitCode = await runConfigCommand({
|
|
17215
18038
|
cwd: options.cwd,
|
|
@@ -17291,7 +18114,7 @@ async function main() {
|
|
|
17291
18114
|
}
|
|
17292
18115
|
async function warnIfMissingShellIntegration() {
|
|
17293
18116
|
try {
|
|
17294
|
-
const { config } = await loadGlobalConfig(
|
|
18117
|
+
const { config } = await loadGlobalConfig(homedir7());
|
|
17295
18118
|
if (!config.shellIntegration) {
|
|
17296
18119
|
const shellBin = (process.env.SHELL ?? "").split("/").at(-1);
|
|
17297
18120
|
const shellArg = shellBin && ["bash", "zsh", "fish"].includes(shellBin) ? ` ${shellBin}` : "";
|