@solaqua/gji 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +264 -164
- 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 +1709 -850
- 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.js +16 -16
- package/dist/index.js +13 -9
- package/dist/init.d.ts +2 -2
- package/dist/init.js +106 -94
- package/dist/install-prompt.d.ts +3 -3
- package/dist/install-prompt.js +46 -28
- package/dist/ls.d.ts +2 -2
- package/dist/ls.js +29 -29
- package/dist/new.d.ts +2 -2
- package/dist/new.js +96 -81
- 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 +45 -19
- package/dist/repo.js +29 -28
- package/dist/root.js +3 -3
- package/dist/shell-completion.d.ts +1 -1
- package/dist/shell-completion.js +65 -37
- 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/trigger-hook.js +9 -5
- package/dist/warp.js +66 -34
- 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-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-trigger-hook.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +5 -1
- package/package.json +8 -2
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";
|
|
@@ -9740,7 +9522,7 @@ var retryifyAsync = (fn, options) => {
|
|
|
9740
9522
|
throw error;
|
|
9741
9523
|
const delay2 = Math.round(interval * Math.random());
|
|
9742
9524
|
if (delay2 > 0) {
|
|
9743
|
-
const delayPromise = new Promise((
|
|
9525
|
+
const delayPromise = new Promise((resolve6) => setTimeout(resolve6, delay2));
|
|
9744
9526
|
return delayPromise.then(() => attempt.apply(void 0, args));
|
|
9745
9527
|
} else {
|
|
9746
9528
|
return attempt.apply(void 0, args);
|
|
@@ -9999,14 +9781,14 @@ var Temp = {
|
|
|
9999
9781
|
}
|
|
10000
9782
|
},
|
|
10001
9783
|
truncate: (filePath) => {
|
|
10002
|
-
const
|
|
10003
|
-
if (
|
|
9784
|
+
const basename9 = path2.basename(filePath);
|
|
9785
|
+
if (basename9.length <= LIMIT_BASENAME_LENGTH)
|
|
10004
9786
|
return filePath;
|
|
10005
|
-
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(
|
|
9787
|
+
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename9);
|
|
10006
9788
|
if (!truncable)
|
|
10007
9789
|
return filePath;
|
|
10008
|
-
const truncationLength =
|
|
10009
|
-
return `${filePath.slice(0, -
|
|
9790
|
+
const truncationLength = basename9.length - LIMIT_BASENAME_LENGTH;
|
|
9791
|
+
return `${filePath.slice(0, -basename9.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
|
|
10010
9792
|
}
|
|
10011
9793
|
};
|
|
10012
9794
|
node_default(Temp.purgeSyncAll);
|
|
@@ -11306,14 +11088,14 @@ var TimeoutError = class extends Error {
|
|
|
11306
11088
|
|
|
11307
11089
|
// node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
|
|
11308
11090
|
async function timeout(request, init, abortController, options) {
|
|
11309
|
-
return new Promise((
|
|
11091
|
+
return new Promise((resolve6, reject) => {
|
|
11310
11092
|
const timeoutId = setTimeout(() => {
|
|
11311
11093
|
if (abortController) {
|
|
11312
11094
|
abortController.abort();
|
|
11313
11095
|
}
|
|
11314
11096
|
reject(new TimeoutError(request));
|
|
11315
11097
|
}, options.timeout);
|
|
11316
|
-
void options.fetch(request, init).then(
|
|
11098
|
+
void options.fetch(request, init).then(resolve6).catch(reject).then(() => {
|
|
11317
11099
|
clearTimeout(timeoutId);
|
|
11318
11100
|
});
|
|
11319
11101
|
});
|
|
@@ -11321,7 +11103,7 @@ async function timeout(request, init, abortController, options) {
|
|
|
11321
11103
|
|
|
11322
11104
|
// node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
|
|
11323
11105
|
async function delay(ms, { signal }) {
|
|
11324
|
-
return new Promise((
|
|
11106
|
+
return new Promise((resolve6, reject) => {
|
|
11325
11107
|
if (signal) {
|
|
11326
11108
|
signal.throwIfAborted();
|
|
11327
11109
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
@@ -11332,7 +11114,7 @@ async function delay(ms, { signal }) {
|
|
|
11332
11114
|
}
|
|
11333
11115
|
const timeoutId = setTimeout(() => {
|
|
11334
11116
|
signal?.removeEventListener("abort", abortHandler);
|
|
11335
|
-
|
|
11117
|
+
resolve6();
|
|
11336
11118
|
}, ms);
|
|
11337
11119
|
});
|
|
11338
11120
|
}
|
|
@@ -13032,23 +12814,296 @@ 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);
|
|
13049
13096
|
return;
|
|
13050
13097
|
}
|
|
13051
|
-
await
|
|
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");
|
|
13104
|
+
return;
|
|
13105
|
+
}
|
|
13106
|
+
await new Promise((resolve6) => {
|
|
13052
13107
|
const child = spawn2(command, args, {
|
|
13053
13108
|
cwd,
|
|
13054
13109
|
shell: false,
|
|
@@ -13060,21 +13115,23 @@ 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
|
});
|
|
13068
13125
|
child.on("error", (err) => {
|
|
13069
13126
|
stderr(`gji: hook failed to start: ${err.message}
|
|
13070
13127
|
`);
|
|
13071
|
-
|
|
13128
|
+
resolve6();
|
|
13072
13129
|
});
|
|
13073
13130
|
});
|
|
13074
13131
|
}
|
|
13075
13132
|
async function runShellHook(hookCmd, cwd, context, stderr) {
|
|
13076
13133
|
const interpolated = interpolate(hookCmd, context);
|
|
13077
|
-
await new Promise((
|
|
13134
|
+
await new Promise((resolve6) => {
|
|
13078
13135
|
const child = spawn2(interpolated, {
|
|
13079
13136
|
cwd,
|
|
13080
13137
|
shell: true,
|
|
@@ -13089,12 +13146,12 @@ async function runShellHook(hookCmd, cwd, context, stderr) {
|
|
|
13089
13146
|
stderr(`gji: hook exited with code ${code}: ${interpolated}
|
|
13090
13147
|
`);
|
|
13091
13148
|
}
|
|
13092
|
-
|
|
13149
|
+
resolve6();
|
|
13093
13150
|
});
|
|
13094
13151
|
child.on("error", (err) => {
|
|
13095
13152
|
stderr(`gji: hook failed to start: ${err.message}
|
|
13096
13153
|
`);
|
|
13097
|
-
|
|
13154
|
+
resolve6();
|
|
13098
13155
|
});
|
|
13099
13156
|
});
|
|
13100
13157
|
}
|
|
@@ -13132,49 +13189,9 @@ function parseHookCommand(value) {
|
|
|
13132
13189
|
return void 0;
|
|
13133
13190
|
}
|
|
13134
13191
|
|
|
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
13192
|
// src/repo.ts
|
|
13176
|
-
import { basename, dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
|
|
13177
13193
|
import { homedir as homedir3 } from "node:os";
|
|
13194
|
+
import { basename, dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
|
|
13178
13195
|
|
|
13179
13196
|
// src/git.ts
|
|
13180
13197
|
import { execFile } from "node:child_process";
|
|
@@ -13190,7 +13207,11 @@ async function runGit(cwd, args) {
|
|
|
13190
13207
|
}
|
|
13191
13208
|
}
|
|
13192
13209
|
async function readWorktreeHealth(cwd) {
|
|
13193
|
-
const { stdout } = await execFileAsync(
|
|
13210
|
+
const { stdout } = await execFileAsync(
|
|
13211
|
+
"git",
|
|
13212
|
+
["status", "--porcelain=v2", "--branch"],
|
|
13213
|
+
{ cwd }
|
|
13214
|
+
);
|
|
13194
13215
|
return parseWorktreeHealth(stdout);
|
|
13195
13216
|
}
|
|
13196
13217
|
async function isDirtyWorktree(cwd) {
|
|
@@ -13199,7 +13220,9 @@ async function isDirtyWorktree(cwd) {
|
|
|
13199
13220
|
}
|
|
13200
13221
|
async function isBranchMergedInto(cwd, branch, base = "HEAD") {
|
|
13201
13222
|
try {
|
|
13202
|
-
await execFileAsync("git", ["merge-base", "--is-ancestor", branch, base], {
|
|
13223
|
+
await execFileAsync("git", ["merge-base", "--is-ancestor", branch, base], {
|
|
13224
|
+
cwd
|
|
13225
|
+
});
|
|
13203
13226
|
return true;
|
|
13204
13227
|
} catch (error) {
|
|
13205
13228
|
if (hasExitCode(error, 1)) {
|
|
@@ -13209,7 +13232,11 @@ async function isBranchMergedInto(cwd, branch, base = "HEAD") {
|
|
|
13209
13232
|
}
|
|
13210
13233
|
}
|
|
13211
13234
|
async function resolveRemoteDefaultBranch(cwd, remote) {
|
|
13212
|
-
const { stdout } = await execFileAsync(
|
|
13235
|
+
const { stdout } = await execFileAsync(
|
|
13236
|
+
"git",
|
|
13237
|
+
["ls-remote", "--symref", remote, "HEAD"],
|
|
13238
|
+
{ cwd }
|
|
13239
|
+
);
|
|
13213
13240
|
const refLine = stdout.split("\n").find((line) => line.startsWith("ref: refs/heads/"));
|
|
13214
13241
|
if (!refLine) {
|
|
13215
13242
|
return null;
|
|
@@ -13219,7 +13246,11 @@ async function resolveRemoteDefaultBranch(cwd, remote) {
|
|
|
13219
13246
|
}
|
|
13220
13247
|
async function readBranchLastCommitTimestamp(cwd, branch) {
|
|
13221
13248
|
try {
|
|
13222
|
-
const { stdout } = await execFileAsync(
|
|
13249
|
+
const { stdout } = await execFileAsync(
|
|
13250
|
+
"git",
|
|
13251
|
+
["log", "-1", "--format=%ct", branch],
|
|
13252
|
+
{ cwd }
|
|
13253
|
+
);
|
|
13223
13254
|
const timestamp = Number(stdout.trim());
|
|
13224
13255
|
return Number.isFinite(timestamp) ? timestamp : null;
|
|
13225
13256
|
} catch {
|
|
@@ -13283,7 +13314,9 @@ function resolveWorktreePath(repoRoot, branch, basePath) {
|
|
|
13283
13314
|
throw new Error("Branch name must not be empty.");
|
|
13284
13315
|
}
|
|
13285
13316
|
if (segments.some((segment) => segment === "." || segment === "..")) {
|
|
13286
|
-
throw new Error(
|
|
13317
|
+
throw new Error(
|
|
13318
|
+
`Branch name '${branch}' contains an invalid path segment.`
|
|
13319
|
+
);
|
|
13287
13320
|
}
|
|
13288
13321
|
const base = basePath ? expandTildeInPath(basePath) : join3(dirname3(repoRoot), "worktrees", basename(repoRoot));
|
|
13289
13322
|
return join3(base, ...segments);
|
|
@@ -13403,17 +13436,27 @@ async function runBackCommand(options) {
|
|
|
13403
13436
|
}
|
|
13404
13437
|
if (!target) {
|
|
13405
13438
|
options.stderr("gji back: no previous worktree in history\n");
|
|
13406
|
-
options.stderr(
|
|
13439
|
+
options.stderr(
|
|
13440
|
+
"Hint: Use 'gji go', 'gji new', or 'gji pr' to navigate between worktrees\n"
|
|
13441
|
+
);
|
|
13407
13442
|
return 1;
|
|
13408
13443
|
}
|
|
13409
13444
|
try {
|
|
13410
13445
|
const repository = await detectRepository(target.path);
|
|
13411
|
-
const config = await loadEffectiveConfig(
|
|
13446
|
+
const config = await loadEffectiveConfig(
|
|
13447
|
+
repository.repoRoot,
|
|
13448
|
+
options.home,
|
|
13449
|
+
options.stderr
|
|
13450
|
+
);
|
|
13412
13451
|
const hooks = extractHooks(config);
|
|
13413
13452
|
await runHook(
|
|
13414
13453
|
hooks.afterEnter,
|
|
13415
13454
|
target.path,
|
|
13416
|
-
{
|
|
13455
|
+
{
|
|
13456
|
+
branch: target.branch ?? void 0,
|
|
13457
|
+
path: target.path,
|
|
13458
|
+
repo: basename2(repository.repoRoot)
|
|
13459
|
+
},
|
|
13417
13460
|
options.stderr
|
|
13418
13461
|
);
|
|
13419
13462
|
} catch {
|
|
@@ -13427,7 +13470,9 @@ function formatHistoryList(history, cwd) {
|
|
|
13427
13470
|
"BRANCH".length,
|
|
13428
13471
|
...history.map((e2) => (e2.branch ?? "(detached)").length)
|
|
13429
13472
|
);
|
|
13430
|
-
const lines = [
|
|
13473
|
+
const lines = [
|
|
13474
|
+
" " + "BRANCH".padEnd(branchWidth) + " WHEN PATH"
|
|
13475
|
+
];
|
|
13431
13476
|
for (const entry of history) {
|
|
13432
13477
|
const isCurrent = entry.path === cwd;
|
|
13433
13478
|
const branch = (entry.branch ?? "(detached)").padEnd(branchWidth);
|
|
@@ -14121,7 +14166,10 @@ function formatLastCommit(timestampSeconds) {
|
|
|
14121
14166
|
return timestampSeconds === null ? "n/a" : formatRelativeAge(timestampSeconds);
|
|
14122
14167
|
}
|
|
14123
14168
|
function formatRelativeAge(timestampSeconds) {
|
|
14124
|
-
const ageSeconds = Math.max(
|
|
14169
|
+
const ageSeconds = Math.max(
|
|
14170
|
+
0,
|
|
14171
|
+
Math.floor(Date.now() / 1e3) - timestampSeconds
|
|
14172
|
+
);
|
|
14125
14173
|
const units = [
|
|
14126
14174
|
{ label: "y", seconds: 365 * 24 * 60 * 60 },
|
|
14127
14175
|
{ label: "mo", seconds: 30 * 24 * 60 * 60 },
|
|
@@ -14154,16 +14202,28 @@ async function loadLinkedWorktrees(cwd) {
|
|
|
14154
14202
|
};
|
|
14155
14203
|
}
|
|
14156
14204
|
async function removeWorktree(repoRoot, worktreePath) {
|
|
14157
|
-
await execFileAsync2("git", ["worktree", "remove", worktreePath], {
|
|
14205
|
+
await execFileAsync2("git", ["worktree", "remove", worktreePath], {
|
|
14206
|
+
cwd: repoRoot,
|
|
14207
|
+
env: GIT_ENV
|
|
14208
|
+
});
|
|
14158
14209
|
}
|
|
14159
14210
|
async function forceRemoveWorktree(repoRoot, worktreePath) {
|
|
14160
|
-
await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], {
|
|
14211
|
+
await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], {
|
|
14212
|
+
cwd: repoRoot,
|
|
14213
|
+
env: GIT_ENV
|
|
14214
|
+
});
|
|
14161
14215
|
}
|
|
14162
14216
|
async function deleteBranch(repoRoot, branch) {
|
|
14163
|
-
await execFileAsync2("git", ["branch", "-d", branch], {
|
|
14217
|
+
await execFileAsync2("git", ["branch", "-d", branch], {
|
|
14218
|
+
cwd: repoRoot,
|
|
14219
|
+
env: GIT_ENV
|
|
14220
|
+
});
|
|
14164
14221
|
}
|
|
14165
14222
|
async function forceDeleteBranch(repoRoot, branch) {
|
|
14166
|
-
await execFileAsync2("git", ["branch", "-D", branch], {
|
|
14223
|
+
await execFileAsync2("git", ["branch", "-D", branch], {
|
|
14224
|
+
cwd: repoRoot,
|
|
14225
|
+
env: GIT_ENV
|
|
14226
|
+
});
|
|
14167
14227
|
}
|
|
14168
14228
|
function isWorktreeDirtyError(error) {
|
|
14169
14229
|
return hasStderr(error) && error.stderr.includes("contains modified or untracked files");
|
|
@@ -14202,12 +14262,18 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14202
14262
|
const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
|
|
14203
14263
|
const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
|
|
14204
14264
|
return async function runCleanCommand2(options) {
|
|
14205
|
-
const { linkedWorktrees, repository } = await loadLinkedWorktrees(
|
|
14265
|
+
const { linkedWorktrees, repository } = await loadLinkedWorktrees(
|
|
14266
|
+
options.cwd
|
|
14267
|
+
);
|
|
14206
14268
|
const linkedCleanupCandidates = linkedWorktrees.filter(
|
|
14207
14269
|
(worktree) => worktree.path !== repository.currentRoot
|
|
14208
14270
|
);
|
|
14209
14271
|
const staleBaseRef = options.stale ? await resolveStaleBaseRef(repository.repoRoot, options.stderr) : null;
|
|
14210
|
-
const cleanupCandidates = options.stale ? await filterStaleCleanupCandidates(
|
|
14272
|
+
const cleanupCandidates = options.stale ? await filterStaleCleanupCandidates(
|
|
14273
|
+
repository.repoRoot,
|
|
14274
|
+
linkedCleanupCandidates,
|
|
14275
|
+
staleBaseRef
|
|
14276
|
+
) : linkedCleanupCandidates;
|
|
14211
14277
|
if (cleanupCandidates.length === 0) {
|
|
14212
14278
|
if (options.stale) {
|
|
14213
14279
|
emitNoStaleCandidates(options);
|
|
@@ -14221,8 +14287,10 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14221
14287
|
if (options.json) {
|
|
14222
14288
|
emitError(options, message);
|
|
14223
14289
|
} else {
|
|
14224
|
-
options.stderr(
|
|
14225
|
-
`)
|
|
14290
|
+
options.stderr(
|
|
14291
|
+
`gji clean: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
14292
|
+
`
|
|
14293
|
+
);
|
|
14226
14294
|
}
|
|
14227
14295
|
return 1;
|
|
14228
14296
|
}
|
|
@@ -14232,7 +14300,10 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14232
14300
|
options.stderr("Aborted\n");
|
|
14233
14301
|
return 1;
|
|
14234
14302
|
}
|
|
14235
|
-
const selectedWorktrees = resolveSelectedWorktrees(
|
|
14303
|
+
const selectedWorktrees = resolveSelectedWorktrees(
|
|
14304
|
+
cleanupCandidates,
|
|
14305
|
+
selections
|
|
14306
|
+
);
|
|
14236
14307
|
if (selectedWorktrees.length !== selections.length) {
|
|
14237
14308
|
options.stderr("Selected worktree no longer exists\n");
|
|
14238
14309
|
return 1;
|
|
@@ -14247,13 +14318,19 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14247
14318
|
}
|
|
14248
14319
|
if (options.dryRun) {
|
|
14249
14320
|
if (options.json) {
|
|
14250
|
-
const removed = selectedWorktreeInfos.map(
|
|
14251
|
-
|
|
14252
|
-
|
|
14321
|
+
const removed = selectedWorktreeInfos.map(
|
|
14322
|
+
(info) => serializeWorktreeInfo(info)
|
|
14323
|
+
);
|
|
14324
|
+
options.stdout(
|
|
14325
|
+
`${JSON.stringify({ removed, dryRun: true }, null, 2)}
|
|
14326
|
+
`
|
|
14327
|
+
);
|
|
14253
14328
|
} else {
|
|
14254
14329
|
for (const info of selectedWorktreeInfos) {
|
|
14255
|
-
options.stdout(
|
|
14256
|
-
`)
|
|
14330
|
+
options.stdout(
|
|
14331
|
+
`Would remove worktree at ${info.path} (${formatCleanInfo(info)})
|
|
14332
|
+
`
|
|
14333
|
+
);
|
|
14257
14334
|
}
|
|
14258
14335
|
}
|
|
14259
14336
|
return 0;
|
|
@@ -14261,9 +14338,15 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14261
14338
|
const removedPaths = [];
|
|
14262
14339
|
const removedWorktrees = [];
|
|
14263
14340
|
for (const worktree of selectedWorktrees) {
|
|
14264
|
-
if (options.stale && !await isStaleCleanupCandidate(
|
|
14265
|
-
|
|
14266
|
-
|
|
14341
|
+
if (options.stale && !await isStaleCleanupCandidate(
|
|
14342
|
+
repository.repoRoot,
|
|
14343
|
+
worktree,
|
|
14344
|
+
staleBaseRef
|
|
14345
|
+
)) {
|
|
14346
|
+
options.stderr(
|
|
14347
|
+
`Skipped ${worktree.path}: no longer a safe stale cleanup candidate
|
|
14348
|
+
`
|
|
14349
|
+
);
|
|
14267
14350
|
continue;
|
|
14268
14351
|
}
|
|
14269
14352
|
try {
|
|
@@ -14273,8 +14356,10 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14273
14356
|
throw error;
|
|
14274
14357
|
}
|
|
14275
14358
|
if (options.stale) {
|
|
14276
|
-
options.stderr(
|
|
14277
|
-
`
|
|
14359
|
+
options.stderr(
|
|
14360
|
+
`Skipped ${worktree.path}: no longer a safe stale cleanup candidate
|
|
14361
|
+
`
|
|
14362
|
+
);
|
|
14278
14363
|
continue;
|
|
14279
14364
|
}
|
|
14280
14365
|
if (!options.force && !await confirmForceRemoveWorktree(worktree.path)) {
|
|
@@ -14288,7 +14373,10 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14288
14373
|
if (!options.json) {
|
|
14289
14374
|
reportRemovedPaths(removedPaths, options.stderr);
|
|
14290
14375
|
}
|
|
14291
|
-
emitError(
|
|
14376
|
+
emitError(
|
|
14377
|
+
options,
|
|
14378
|
+
`Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}`
|
|
14379
|
+
);
|
|
14292
14380
|
return 1;
|
|
14293
14381
|
}
|
|
14294
14382
|
}
|
|
@@ -14305,12 +14393,16 @@ function createCleanCommand(dependencies = {}) {
|
|
|
14305
14393
|
try {
|
|
14306
14394
|
await forceDeleteBranch(repository.repoRoot, worktree.branch);
|
|
14307
14395
|
} catch (forceError) {
|
|
14308
|
-
options.stderr(
|
|
14309
|
-
`)
|
|
14396
|
+
options.stderr(
|
|
14397
|
+
`Failed to delete branch ${worktree.branch}: ${toMessage(forceError)}
|
|
14398
|
+
`
|
|
14399
|
+
);
|
|
14310
14400
|
}
|
|
14311
14401
|
} else {
|
|
14312
|
-
options.stderr(
|
|
14313
|
-
`)
|
|
14402
|
+
options.stderr(
|
|
14403
|
+
`Branch ${worktree.branch} was not deleted (has unmerged commits)
|
|
14404
|
+
`
|
|
14405
|
+
);
|
|
14314
14406
|
}
|
|
14315
14407
|
}
|
|
14316
14408
|
}
|
|
@@ -14335,19 +14427,30 @@ async function filterStaleCleanupCandidates(repoRoot, worktrees, baseBranch) {
|
|
|
14335
14427
|
return [];
|
|
14336
14428
|
}
|
|
14337
14429
|
const results = await Promise.all(
|
|
14338
|
-
worktrees.map(
|
|
14430
|
+
worktrees.map(
|
|
14431
|
+
(worktree) => isStaleCleanupCandidate(repoRoot, worktree, baseBranch)
|
|
14432
|
+
)
|
|
14339
14433
|
);
|
|
14340
14434
|
return worktrees.filter((_3, index) => results[index]);
|
|
14341
14435
|
}
|
|
14342
14436
|
async function resolveStaleBaseRef(repoRoot, stderr) {
|
|
14343
14437
|
const config = await loadEffectiveConfig(repoRoot, void 0, stderr);
|
|
14344
14438
|
const remote = resolveConfiguredString(config.syncRemote) ?? "origin";
|
|
14345
|
-
const configuredDefaultBranch = resolveConfiguredString(
|
|
14439
|
+
const configuredDefaultBranch = resolveConfiguredString(
|
|
14440
|
+
config.syncDefaultBranch
|
|
14441
|
+
);
|
|
14346
14442
|
if (configuredDefaultBranch) {
|
|
14347
|
-
return await resolveFetchedRemoteRef(
|
|
14443
|
+
return await resolveFetchedRemoteRef(
|
|
14444
|
+
repoRoot,
|
|
14445
|
+
remote,
|
|
14446
|
+
configuredDefaultBranch
|
|
14447
|
+
);
|
|
14348
14448
|
}
|
|
14349
14449
|
try {
|
|
14350
|
-
const remoteDefaultBranch = await resolveRemoteDefaultBranch(
|
|
14450
|
+
const remoteDefaultBranch = await resolveRemoteDefaultBranch(
|
|
14451
|
+
repoRoot,
|
|
14452
|
+
remote
|
|
14453
|
+
);
|
|
14351
14454
|
return remoteDefaultBranch === null ? null : await resolveFetchedRemoteRef(repoRoot, remote, remoteDefaultBranch);
|
|
14352
14455
|
} catch {
|
|
14353
14456
|
return null;
|
|
@@ -14442,14 +14545,22 @@ async function defaultPromptForWorktrees(worktrees) {
|
|
|
14442
14545
|
return pD(choice) ? null : choice;
|
|
14443
14546
|
}
|
|
14444
14547
|
async function defaultConfirmRemoval(worktrees) {
|
|
14445
|
-
const branchCount = worktrees.filter(
|
|
14548
|
+
const branchCount = worktrees.filter(
|
|
14549
|
+
(worktree) => worktree.branch !== null
|
|
14550
|
+
).length;
|
|
14446
14551
|
const detachedCount = worktrees.length - branchCount;
|
|
14447
|
-
const messageParts = [
|
|
14552
|
+
const messageParts = [
|
|
14553
|
+
`Remove ${worktrees.length} linked worktree${worktrees.length === 1 ? "" : "s"}`
|
|
14554
|
+
];
|
|
14448
14555
|
if (branchCount > 0) {
|
|
14449
|
-
messageParts.push(
|
|
14556
|
+
messageParts.push(
|
|
14557
|
+
`delete ${branchCount} branch${branchCount === 1 ? "" : "es"}`
|
|
14558
|
+
);
|
|
14450
14559
|
}
|
|
14451
14560
|
if (detachedCount > 0) {
|
|
14452
|
-
messageParts.push(
|
|
14561
|
+
messageParts.push(
|
|
14562
|
+
`remove ${detachedCount} detached worktree${detachedCount === 1 ? "" : "s"}`
|
|
14563
|
+
);
|
|
14453
14564
|
}
|
|
14454
14565
|
const choice = await ye({
|
|
14455
14566
|
active: "Yes",
|
|
@@ -14460,25 +14571,35 @@ async function defaultConfirmRemoval(worktrees) {
|
|
|
14460
14571
|
return !pD(choice) && choice;
|
|
14461
14572
|
}
|
|
14462
14573
|
|
|
14463
|
-
// src/
|
|
14464
|
-
|
|
14465
|
-
const
|
|
14466
|
-
if (
|
|
14467
|
-
|
|
14468
|
-
`);
|
|
14469
|
-
return 0;
|
|
14574
|
+
// src/shell.ts
|
|
14575
|
+
function resolveSupportedShell(requestedShell, detectedShell) {
|
|
14576
|
+
const requested = normalizeShell(requestedShell);
|
|
14577
|
+
if (requested) {
|
|
14578
|
+
return requested;
|
|
14470
14579
|
}
|
|
14471
|
-
|
|
14472
|
-
|
|
14473
|
-
|
|
14580
|
+
return normalizeShell(detectedShell);
|
|
14581
|
+
}
|
|
14582
|
+
function normalizeShell(value) {
|
|
14583
|
+
if (!value) {
|
|
14584
|
+
return null;
|
|
14585
|
+
}
|
|
14586
|
+
const candidate = value.split("/").at(-1)?.toLowerCase();
|
|
14587
|
+
switch (candidate) {
|
|
14588
|
+
case "bash":
|
|
14589
|
+
case "fish":
|
|
14590
|
+
case "zsh":
|
|
14591
|
+
return candidate;
|
|
14592
|
+
default:
|
|
14593
|
+
return null;
|
|
14474
14594
|
}
|
|
14475
|
-
options.stdout(formatHistoryList(history, options.cwd));
|
|
14476
|
-
return 0;
|
|
14477
14595
|
}
|
|
14478
14596
|
|
|
14479
14597
|
// src/shell-completion.ts
|
|
14480
14598
|
var TOP_LEVEL_COMMANDS = [
|
|
14481
|
-
{
|
|
14599
|
+
{
|
|
14600
|
+
name: "new",
|
|
14601
|
+
description: "create a new branch or detached linked worktree"
|
|
14602
|
+
},
|
|
14482
14603
|
{ name: "init", description: "print or install shell integration" },
|
|
14483
14604
|
{ name: "completion", description: "print shell completion definitions" },
|
|
14484
14605
|
{ name: "pr", description: "fetch a pull request into a linked worktree" },
|
|
@@ -14490,11 +14611,21 @@ var TOP_LEVEL_COMMANDS = [
|
|
|
14490
14611
|
{ name: "root", description: "print the main repository root path" },
|
|
14491
14612
|
{ name: "status", description: "summarize repository and worktree health" },
|
|
14492
14613
|
{ name: "sync", description: "fetch and update one or all worktrees" },
|
|
14614
|
+
{
|
|
14615
|
+
name: "sync-files",
|
|
14616
|
+
description: "manage local files copied into new worktrees"
|
|
14617
|
+
},
|
|
14493
14618
|
{ name: "ls", description: "list active worktrees" },
|
|
14494
14619
|
{ name: "clean", description: "interactively prune linked worktrees" },
|
|
14495
|
-
{
|
|
14620
|
+
{
|
|
14621
|
+
name: "remove",
|
|
14622
|
+
description: "remove a linked worktree and delete its branch when present"
|
|
14623
|
+
},
|
|
14496
14624
|
{ name: "rm", description: "alias of remove" },
|
|
14497
|
-
{
|
|
14625
|
+
{
|
|
14626
|
+
name: "trigger-hook",
|
|
14627
|
+
description: "run a named hook in the current worktree"
|
|
14628
|
+
},
|
|
14498
14629
|
{ name: "warp", description: "jump to any worktree across all known repos" },
|
|
14499
14630
|
{ name: "config", description: "manage global config defaults" }
|
|
14500
14631
|
];
|
|
@@ -14512,7 +14643,9 @@ function renderShellCompletion(shell) {
|
|
|
14512
14643
|
}
|
|
14513
14644
|
}
|
|
14514
14645
|
function renderBashCompletion() {
|
|
14515
|
-
const topLevelCommands = TOP_LEVEL_COMMANDS.map(
|
|
14646
|
+
const topLevelCommands = TOP_LEVEL_COMMANDS.map(
|
|
14647
|
+
(command) => command.name
|
|
14648
|
+
).join(" ");
|
|
14516
14649
|
const shells = SHELL_NAMES.join(" ");
|
|
14517
14650
|
const hooks = HOOK_NAMES.join(" ");
|
|
14518
14651
|
const configKeys = CONFIG_KEYS.join(" ");
|
|
@@ -14566,6 +14699,12 @@ _gji_completion() {
|
|
|
14566
14699
|
sync)
|
|
14567
14700
|
COMPREPLY=( $(compgen -W "--all --json --help" -- "$cur") )
|
|
14568
14701
|
;;
|
|
14702
|
+
sync-files)
|
|
14703
|
+
if [ "$COMP_CWORD" -eq 2 ]; then
|
|
14704
|
+
COMPREPLY=( $(compgen -W "list add remove rm --json --help" -- "$cur") )
|
|
14705
|
+
return 0
|
|
14706
|
+
fi
|
|
14707
|
+
;;
|
|
14569
14708
|
ls)
|
|
14570
14709
|
COMPREPLY=( $(compgen -W "--compact --json --help" -- "$cur") )
|
|
14571
14710
|
;;
|
|
@@ -14679,6 +14818,12 @@ complete -c gji -n '__fish_seen_subcommand_from status' -l json -d 'print reposi
|
|
|
14679
14818
|
complete -c gji -n '__fish_seen_subcommand_from sync' -l all -d 'sync every worktree in the repository'
|
|
14680
14819
|
complete -c gji -n '__fish_seen_subcommand_from sync' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
14681
14820
|
|
|
14821
|
+
complete -c gji -n '__fish_seen_subcommand_from sync-files' -a 'list add remove rm' -d 'sync-files action'
|
|
14822
|
+
complete -c gji -n '__fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
|
|
14823
|
+
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'
|
|
14824
|
+
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'
|
|
14825
|
+
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'
|
|
14826
|
+
|
|
14682
14827
|
complete -c gji -n '__fish_seen_subcommand_from ls' -l compact -d 'show only branch and path columns'
|
|
14683
14828
|
complete -c gji -n '__fish_seen_subcommand_from ls' -l json -d 'print active worktrees as JSON'
|
|
14684
14829
|
|
|
@@ -14760,6 +14905,9 @@ case "\${words[2]}" in
|
|
|
14760
14905
|
sync)
|
|
14761
14906
|
_arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
|
|
14762
14907
|
;;
|
|
14908
|
+
sync-files)
|
|
14909
|
+
_arguments '--json[emit JSON instead of human-readable output]' '2:action:(list add remove rm)' '*:path: '
|
|
14910
|
+
;;
|
|
14763
14911
|
ls)
|
|
14764
14912
|
_arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
|
|
14765
14913
|
;;
|
|
@@ -14806,29 +14954,6 @@ function escapeSingleQuotes(value) {
|
|
|
14806
14954
|
return value.replace(/'/g, `'\\''`);
|
|
14807
14955
|
}
|
|
14808
14956
|
|
|
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
14957
|
// src/completion.ts
|
|
14833
14958
|
async function runCompletionCommand(options) {
|
|
14834
14959
|
const shell = resolveSupportedShell(options.shell, process.env.SHELL);
|
|
@@ -14860,7 +14985,10 @@ async function runConfigCommand(options) {
|
|
|
14860
14985
|
}
|
|
14861
14986
|
case "set":
|
|
14862
14987
|
if (options.key && options.value !== void 0) {
|
|
14863
|
-
await updateGlobalConfigKey(
|
|
14988
|
+
await updateGlobalConfigKey(
|
|
14989
|
+
options.key,
|
|
14990
|
+
parseConfigValue(options.value)
|
|
14991
|
+
);
|
|
14864
14992
|
return 0;
|
|
14865
14993
|
}
|
|
14866
14994
|
break;
|
|
@@ -14881,28 +15009,85 @@ function writeJson(stdout, value) {
|
|
|
14881
15009
|
}
|
|
14882
15010
|
|
|
14883
15011
|
// src/go.ts
|
|
14884
|
-
import { basename as
|
|
15012
|
+
import { basename as basename6 } from "node:path";
|
|
15013
|
+
|
|
15014
|
+
// src/warp.ts
|
|
15015
|
+
import { realpath as realpath2 } from "node:fs/promises";
|
|
15016
|
+
import { basename as basename5, resolve as resolve5 } from "node:path";
|
|
14885
15017
|
|
|
14886
15018
|
// src/new.ts
|
|
15019
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
14887
15020
|
import { mkdir as mkdir4 } from "node:fs/promises";
|
|
14888
15021
|
import { basename as basename3, dirname as dirname5 } from "node:path";
|
|
14889
|
-
import { execFile as execFile3 } from "node:child_process";
|
|
14890
15022
|
import { promisify as promisify4 } from "node:util";
|
|
14891
15023
|
|
|
15024
|
+
// src/conflict.ts
|
|
15025
|
+
import { constants } from "node:fs";
|
|
15026
|
+
import { access as access2 } from "node:fs/promises";
|
|
15027
|
+
async function pathExists(path9) {
|
|
15028
|
+
try {
|
|
15029
|
+
await access2(path9, constants.F_OK);
|
|
15030
|
+
return true;
|
|
15031
|
+
} catch {
|
|
15032
|
+
return false;
|
|
15033
|
+
}
|
|
15034
|
+
}
|
|
15035
|
+
async function promptForPathConflict(path9) {
|
|
15036
|
+
const choice = await ve({
|
|
15037
|
+
message: `Target path already exists: ${path9}`,
|
|
15038
|
+
options: [
|
|
15039
|
+
{
|
|
15040
|
+
value: "abort",
|
|
15041
|
+
label: "Abort",
|
|
15042
|
+
hint: "Keep the existing directory untouched"
|
|
15043
|
+
},
|
|
15044
|
+
{
|
|
15045
|
+
value: "reuse",
|
|
15046
|
+
label: "Reuse path",
|
|
15047
|
+
hint: "Print the existing path and stop"
|
|
15048
|
+
}
|
|
15049
|
+
]
|
|
15050
|
+
});
|
|
15051
|
+
if (pD(choice)) {
|
|
15052
|
+
return "abort";
|
|
15053
|
+
}
|
|
15054
|
+
return choice;
|
|
15055
|
+
}
|
|
15056
|
+
|
|
14892
15057
|
// src/editor.ts
|
|
14893
15058
|
import { spawn as spawn3 } from "node:child_process";
|
|
14894
15059
|
var EDITORS = [
|
|
14895
|
-
{
|
|
14896
|
-
|
|
14897
|
-
|
|
15060
|
+
{
|
|
15061
|
+
cli: "cursor",
|
|
15062
|
+
name: "Cursor",
|
|
15063
|
+
newWindowFlag: "--new-window",
|
|
15064
|
+
supportsWorkspace: true
|
|
15065
|
+
},
|
|
15066
|
+
{
|
|
15067
|
+
cli: "code",
|
|
15068
|
+
name: "VS Code",
|
|
15069
|
+
newWindowFlag: "--new-window",
|
|
15070
|
+
supportsWorkspace: true
|
|
15071
|
+
},
|
|
15072
|
+
{
|
|
15073
|
+
cli: "windsurf",
|
|
15074
|
+
name: "Windsurf",
|
|
15075
|
+
newWindowFlag: "--new-window",
|
|
15076
|
+
supportsWorkspace: true
|
|
15077
|
+
},
|
|
14898
15078
|
{ cli: "zed", name: "Zed", supportsWorkspace: false },
|
|
14899
|
-
{
|
|
15079
|
+
{
|
|
15080
|
+
cli: "subl",
|
|
15081
|
+
name: "Sublime Text",
|
|
15082
|
+
newWindowFlag: "--new-window",
|
|
15083
|
+
supportsWorkspace: false
|
|
15084
|
+
}
|
|
14900
15085
|
];
|
|
14901
15086
|
async function defaultSpawnEditor(cli, args) {
|
|
14902
15087
|
const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
|
|
14903
|
-
await new Promise((
|
|
15088
|
+
await new Promise((resolve6, reject) => {
|
|
14904
15089
|
child.once("error", reject);
|
|
14905
|
-
child.once("spawn",
|
|
15090
|
+
child.once("spawn", resolve6);
|
|
14906
15091
|
});
|
|
14907
15092
|
child.unref();
|
|
14908
15093
|
}
|
|
@@ -14912,13 +15097,7 @@ import { copyFile, mkdir as mkdir3, stat } from "node:fs/promises";
|
|
|
14912
15097
|
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join4, normalize } from "node:path";
|
|
14913
15098
|
async function syncFiles(mainRoot, targetPath, patterns) {
|
|
14914
15099
|
for (const pattern of patterns) {
|
|
14915
|
-
|
|
14916
|
-
throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
|
|
14917
|
-
}
|
|
14918
|
-
const normalized = normalize(pattern);
|
|
14919
|
-
if (normalized.startsWith("..")) {
|
|
14920
|
-
throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
|
|
14921
|
-
}
|
|
15100
|
+
const normalized = validateSyncFilePattern(pattern);
|
|
14922
15101
|
const sourcePath = join4(mainRoot, normalized);
|
|
14923
15102
|
const destPath = join4(targetPath, normalized);
|
|
14924
15103
|
const sourceExists = await fileExists(sourcePath);
|
|
@@ -14933,6 +15112,20 @@ async function syncFiles(mainRoot, targetPath, patterns) {
|
|
|
14933
15112
|
await copyFile(sourcePath, destPath);
|
|
14934
15113
|
}
|
|
14935
15114
|
}
|
|
15115
|
+
function validateSyncFilePattern(pattern) {
|
|
15116
|
+
if (isAbsolute2(pattern)) {
|
|
15117
|
+
throw new Error(
|
|
15118
|
+
`syncFiles: pattern must be a relative path, got: ${pattern}`
|
|
15119
|
+
);
|
|
15120
|
+
}
|
|
15121
|
+
const normalized = normalize(pattern);
|
|
15122
|
+
if (normalized.startsWith("..")) {
|
|
15123
|
+
throw new Error(
|
|
15124
|
+
`syncFiles: pattern must not contain '..' segments, got: ${pattern}`
|
|
15125
|
+
);
|
|
15126
|
+
}
|
|
15127
|
+
return normalized;
|
|
15128
|
+
}
|
|
14936
15129
|
async function fileExists(path9) {
|
|
14937
15130
|
try {
|
|
14938
15131
|
await stat(path9);
|
|
@@ -14952,7 +15145,7 @@ function isNotFoundError(error) {
|
|
|
14952
15145
|
import { spawn as spawn4 } from "node:child_process";
|
|
14953
15146
|
|
|
14954
15147
|
// src/package-manager.ts
|
|
14955
|
-
import { access as
|
|
15148
|
+
import { access as access3, readdir } from "node:fs/promises";
|
|
14956
15149
|
import { join as join5 } from "node:path";
|
|
14957
15150
|
var ENTRIES = [
|
|
14958
15151
|
// JavaScript / TypeScript
|
|
@@ -14966,10 +15159,22 @@ var ENTRIES = [
|
|
|
14966
15159
|
{ name: "uv", signals: ["uv.lock"], command: "uv sync" },
|
|
14967
15160
|
{ name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
|
|
14968
15161
|
{ name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
|
|
14969
|
-
{
|
|
14970
|
-
|
|
15162
|
+
{
|
|
15163
|
+
name: "conda-lock",
|
|
15164
|
+
signals: ["conda-lock.yml"],
|
|
15165
|
+
command: "conda-lock install"
|
|
15166
|
+
},
|
|
15167
|
+
{
|
|
15168
|
+
name: "conda",
|
|
15169
|
+
signals: ["environment.yml"],
|
|
15170
|
+
command: "conda env update --file environment.yml"
|
|
15171
|
+
},
|
|
14971
15172
|
// R
|
|
14972
|
-
{
|
|
15173
|
+
{
|
|
15174
|
+
name: "renv",
|
|
15175
|
+
signals: ["renv.lock"],
|
|
15176
|
+
command: "Rscript -e 'renv::restore()'"
|
|
15177
|
+
},
|
|
14973
15178
|
// Rust
|
|
14974
15179
|
{ name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
|
|
14975
15180
|
// Go
|
|
@@ -14986,25 +15191,56 @@ var ENTRIES = [
|
|
|
14986
15191
|
// Java / Kotlin / Scala
|
|
14987
15192
|
{ name: "maven", signals: ["pom.xml"], command: "mvn install" },
|
|
14988
15193
|
{ name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
|
|
14989
|
-
{
|
|
15194
|
+
{
|
|
15195
|
+
name: "gradle",
|
|
15196
|
+
signals: ["build.gradle", "build.gradle.kts"],
|
|
15197
|
+
command: "gradle build"
|
|
15198
|
+
},
|
|
14990
15199
|
{ name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
|
|
14991
15200
|
// .NET (C# / F# / VB)
|
|
14992
|
-
{
|
|
15201
|
+
{
|
|
15202
|
+
name: "dotnet",
|
|
15203
|
+
signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"],
|
|
15204
|
+
command: "dotnet restore",
|
|
15205
|
+
glob: true
|
|
15206
|
+
},
|
|
14993
15207
|
// Swift
|
|
14994
|
-
{
|
|
15208
|
+
{
|
|
15209
|
+
name: "swift",
|
|
15210
|
+
signals: ["Package.swift"],
|
|
15211
|
+
command: "swift package resolve"
|
|
15212
|
+
},
|
|
14995
15213
|
// Haskell
|
|
14996
15214
|
{ name: "stack", signals: ["stack.yaml"], command: "stack build" },
|
|
14997
|
-
{
|
|
14998
|
-
|
|
15215
|
+
{
|
|
15216
|
+
name: "cabal",
|
|
15217
|
+
signals: ["cabal.project"],
|
|
15218
|
+
command: "cabal install --only-dependencies"
|
|
15219
|
+
},
|
|
15220
|
+
{
|
|
15221
|
+
name: "cabal",
|
|
15222
|
+
signals: ["*.cabal"],
|
|
15223
|
+
command: "cabal install --only-dependencies",
|
|
15224
|
+
glob: true
|
|
15225
|
+
},
|
|
14999
15226
|
// Clojure
|
|
15000
15227
|
{ name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
|
|
15001
15228
|
{ name: "leiningen", signals: ["project.clj"], command: "lein deps" },
|
|
15002
15229
|
// OCaml
|
|
15003
15230
|
{ name: "dune", signals: ["dune-project"], command: "dune build" },
|
|
15004
15231
|
// Julia
|
|
15005
|
-
{
|
|
15232
|
+
{
|
|
15233
|
+
name: "julia",
|
|
15234
|
+
signals: ["Manifest.toml"],
|
|
15235
|
+
command: "julia --project -e 'using Pkg; Pkg.instantiate()'"
|
|
15236
|
+
},
|
|
15006
15237
|
// Nim
|
|
15007
|
-
{
|
|
15238
|
+
{
|
|
15239
|
+
name: "nimble",
|
|
15240
|
+
signals: ["*.nimble"],
|
|
15241
|
+
command: "nimble install",
|
|
15242
|
+
glob: true
|
|
15243
|
+
},
|
|
15008
15244
|
// Crystal
|
|
15009
15245
|
{ name: "shards", signals: ["shard.yml"], command: "shards install" },
|
|
15010
15246
|
// Perl
|
|
@@ -15013,12 +15249,20 @@ var ENTRIES = [
|
|
|
15013
15249
|
{ name: "zig", signals: ["build.zig.zon"], command: "zig build" },
|
|
15014
15250
|
// C / C++
|
|
15015
15251
|
{ name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
|
|
15016
|
-
{
|
|
15252
|
+
{
|
|
15253
|
+
name: "conan",
|
|
15254
|
+
signals: ["conanfile.py", "conanfile.txt"],
|
|
15255
|
+
command: "conan install ."
|
|
15256
|
+
},
|
|
15017
15257
|
// Nix
|
|
15018
15258
|
{ name: "nix", signals: ["flake.nix"], command: "nix develop" },
|
|
15019
15259
|
{ name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
|
|
15020
15260
|
// Terraform / OpenTofu
|
|
15021
|
-
{
|
|
15261
|
+
{
|
|
15262
|
+
name: "terraform",
|
|
15263
|
+
signals: ["terraform.lock.hcl"],
|
|
15264
|
+
command: "terraform init"
|
|
15265
|
+
}
|
|
15022
15266
|
];
|
|
15023
15267
|
async function detectPackageManager(repoRoot) {
|
|
15024
15268
|
for (const entry of ENTRIES) {
|
|
@@ -15032,7 +15276,7 @@ async function detectPackageManager(repoRoot) {
|
|
|
15032
15276
|
async function matchesExact(repoRoot, signals) {
|
|
15033
15277
|
for (const signal of signals) {
|
|
15034
15278
|
try {
|
|
15035
|
-
await
|
|
15279
|
+
await access3(join5(repoRoot, signal));
|
|
15036
15280
|
return true;
|
|
15037
15281
|
} catch {
|
|
15038
15282
|
}
|
|
@@ -15081,8 +15325,10 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
|
|
|
15081
15325
|
try {
|
|
15082
15326
|
await runner(pm.installCommand, worktreePath, stderr);
|
|
15083
15327
|
} catch (error) {
|
|
15084
|
-
stderr(
|
|
15085
|
-
`)
|
|
15328
|
+
stderr(
|
|
15329
|
+
`gji: install command failed: ${error instanceof Error ? error.message : String(error)}
|
|
15330
|
+
`
|
|
15331
|
+
);
|
|
15086
15332
|
}
|
|
15087
15333
|
}
|
|
15088
15334
|
const saveGlobal = config.installSaveTarget === "global";
|
|
@@ -15092,15 +15338,23 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
|
|
|
15092
15338
|
try {
|
|
15093
15339
|
if (saveGlobal) {
|
|
15094
15340
|
const existingHooks = await loadExistingGlobalRepoHooks(repoRoot);
|
|
15095
|
-
await writeGlobalKey(repoRoot, "hooks", {
|
|
15341
|
+
await writeGlobalKey(repoRoot, "hooks", {
|
|
15342
|
+
...existingHooks,
|
|
15343
|
+
afterCreate: pm.installCommand
|
|
15344
|
+
});
|
|
15096
15345
|
} else {
|
|
15097
15346
|
const { config: localConfig } = await loadConfig(repoRoot);
|
|
15098
15347
|
const existingLocalHooks = isPlainObject2(localConfig.hooks) ? localConfig.hooks : {};
|
|
15099
|
-
await writeKey(repoRoot, "hooks", {
|
|
15348
|
+
await writeKey(repoRoot, "hooks", {
|
|
15349
|
+
...existingLocalHooks,
|
|
15350
|
+
afterCreate: pm.installCommand
|
|
15351
|
+
});
|
|
15100
15352
|
}
|
|
15101
15353
|
} catch (error) {
|
|
15102
|
-
stderr(
|
|
15103
|
-
`)
|
|
15354
|
+
stderr(
|
|
15355
|
+
`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
|
|
15356
|
+
`
|
|
15357
|
+
);
|
|
15104
15358
|
}
|
|
15105
15359
|
}
|
|
15106
15360
|
if (choice === "never") {
|
|
@@ -15111,14 +15365,20 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
|
|
|
15111
15365
|
await writeKey(repoRoot, "skipInstallPrompt", true);
|
|
15112
15366
|
}
|
|
15113
15367
|
} catch (error) {
|
|
15114
|
-
stderr(
|
|
15115
|
-
`)
|
|
15368
|
+
stderr(
|
|
15369
|
+
`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
|
|
15370
|
+
`
|
|
15371
|
+
);
|
|
15116
15372
|
}
|
|
15117
15373
|
}
|
|
15118
15374
|
}
|
|
15119
15375
|
async function defaultRunInstallCommand(command, cwd, stderr) {
|
|
15120
|
-
await new Promise((
|
|
15121
|
-
const child = spawn4(command, {
|
|
15376
|
+
await new Promise((resolve6, reject) => {
|
|
15377
|
+
const child = spawn4(command, {
|
|
15378
|
+
cwd,
|
|
15379
|
+
shell: true,
|
|
15380
|
+
stdio: ["ignore", "inherit", "pipe"]
|
|
15381
|
+
});
|
|
15122
15382
|
child.stderr.on("data", (chunk) => {
|
|
15123
15383
|
stderr(chunk.toString());
|
|
15124
15384
|
});
|
|
@@ -15126,7 +15386,7 @@ async function defaultRunInstallCommand(command, cwd, stderr) {
|
|
|
15126
15386
|
if (code !== 0) {
|
|
15127
15387
|
reject(new Error(`exited with code ${code}`));
|
|
15128
15388
|
} else {
|
|
15129
|
-
|
|
15389
|
+
resolve6();
|
|
15130
15390
|
}
|
|
15131
15391
|
});
|
|
15132
15392
|
child.on("error", (err) => {
|
|
@@ -15153,7 +15413,11 @@ async function defaultPromptForInstallChoice(pm) {
|
|
|
15153
15413
|
{ value: "yes", label: "Yes", hint: "run once" },
|
|
15154
15414
|
{ value: "no", label: "No", hint: "skip this time" },
|
|
15155
15415
|
{ value: "always", label: "Always", hint: "save as afterCreate hook" },
|
|
15156
|
-
{
|
|
15416
|
+
{
|
|
15417
|
+
value: "never",
|
|
15418
|
+
label: "Never",
|
|
15419
|
+
hint: "disable this prompt for this repo"
|
|
15420
|
+
}
|
|
15157
15421
|
]
|
|
15158
15422
|
});
|
|
15159
15423
|
if (pD(choice)) {
|
|
@@ -15169,31 +15433,6 @@ function isConfiguredHookCommand(value) {
|
|
|
15169
15433
|
return Array.isArray(value) && value.length > 0 && value[0] !== "" && value.every((item) => typeof item === "string");
|
|
15170
15434
|
}
|
|
15171
15435
|
|
|
15172
|
-
// src/conflict.ts
|
|
15173
|
-
import { access as access3 } from "node:fs/promises";
|
|
15174
|
-
import { constants } from "node:fs";
|
|
15175
|
-
async function pathExists(path9) {
|
|
15176
|
-
try {
|
|
15177
|
-
await access3(path9, constants.F_OK);
|
|
15178
|
-
return true;
|
|
15179
|
-
} catch {
|
|
15180
|
-
return false;
|
|
15181
|
-
}
|
|
15182
|
-
}
|
|
15183
|
-
async function promptForPathConflict(path9) {
|
|
15184
|
-
const choice = await ve({
|
|
15185
|
-
message: `Target path already exists: ${path9}`,
|
|
15186
|
-
options: [
|
|
15187
|
-
{ value: "abort", label: "Abort", hint: "Keep the existing directory untouched" },
|
|
15188
|
-
{ value: "reuse", label: "Reuse path", hint: "Print the existing path and stop" }
|
|
15189
|
-
]
|
|
15190
|
-
});
|
|
15191
|
-
if (pD(choice)) {
|
|
15192
|
-
return "abort";
|
|
15193
|
-
}
|
|
15194
|
-
return choice;
|
|
15195
|
-
}
|
|
15196
|
-
|
|
15197
15436
|
// src/new.ts
|
|
15198
15437
|
var execFileAsync3 = promisify4(execFile3);
|
|
15199
15438
|
var NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
|
|
@@ -15204,7 +15443,11 @@ function createNewCommand(dependencies = {}) {
|
|
|
15204
15443
|
const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
|
|
15205
15444
|
return async function runNewCommand2(options) {
|
|
15206
15445
|
const repository = await detectRepository(options.cwd);
|
|
15207
|
-
const config = await loadEffectiveConfig(
|
|
15446
|
+
const config = await loadEffectiveConfig(
|
|
15447
|
+
repository.repoRoot,
|
|
15448
|
+
void 0,
|
|
15449
|
+
options.stderr
|
|
15450
|
+
);
|
|
15208
15451
|
const usesGeneratedDetachedName = options.detached && options.branch === void 0;
|
|
15209
15452
|
if (options.editor && !options.open) {
|
|
15210
15453
|
options.stderr("gji new: --editor has no effect without --open\n");
|
|
@@ -15215,8 +15458,10 @@ function createNewCommand(dependencies = {}) {
|
|
|
15215
15458
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}
|
|
15216
15459
|
`);
|
|
15217
15460
|
} else {
|
|
15218
|
-
options.stderr(
|
|
15219
|
-
`)
|
|
15461
|
+
options.stderr(
|
|
15462
|
+
`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
15463
|
+
`
|
|
15464
|
+
);
|
|
15220
15465
|
}
|
|
15221
15466
|
return 1;
|
|
15222
15467
|
}
|
|
@@ -15234,8 +15479,10 @@ function createNewCommand(dependencies = {}) {
|
|
|
15234
15479
|
const branchError = validateBranchName(rawBranch);
|
|
15235
15480
|
if (branchError) {
|
|
15236
15481
|
if (options.json) {
|
|
15237
|
-
options.stderr(
|
|
15238
|
-
|
|
15482
|
+
options.stderr(
|
|
15483
|
+
`${JSON.stringify({ error: branchError }, null, 2)}
|
|
15484
|
+
`
|
|
15485
|
+
);
|
|
15239
15486
|
} else {
|
|
15240
15487
|
options.stderr(`gji new: ${branchError}
|
|
15241
15488
|
`);
|
|
@@ -15246,18 +15493,32 @@ function createNewCommand(dependencies = {}) {
|
|
|
15246
15493
|
const rawBasePath = resolveConfigString(config, "worktreePath");
|
|
15247
15494
|
const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
|
|
15248
15495
|
const worktreeName = options.detached ? rawBranch : applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
|
|
15249
|
-
const worktreePath = usesGeneratedDetachedName ? await resolveUniqueDetachedWorktreePath(
|
|
15496
|
+
const worktreePath = usesGeneratedDetachedName ? await resolveUniqueDetachedWorktreePath(
|
|
15497
|
+
repository.repoRoot,
|
|
15498
|
+
worktreeName,
|
|
15499
|
+
configuredBasePath
|
|
15500
|
+
) : resolveWorktreePath(
|
|
15501
|
+
repository.repoRoot,
|
|
15502
|
+
worktreeName,
|
|
15503
|
+
configuredBasePath
|
|
15504
|
+
);
|
|
15250
15505
|
if (!usesGeneratedDetachedName && await pathExists(worktreePath)) {
|
|
15251
15506
|
if (options.force) {
|
|
15252
15507
|
if (!options.dryRun) {
|
|
15253
15508
|
try {
|
|
15254
|
-
await execFileAsync3(
|
|
15509
|
+
await execFileAsync3(
|
|
15510
|
+
"git",
|
|
15511
|
+
["worktree", "remove", "--force", worktreePath],
|
|
15512
|
+
{ cwd: repository.repoRoot }
|
|
15513
|
+
);
|
|
15255
15514
|
} catch (err) {
|
|
15256
15515
|
if (!isNotRegisteredWorktreeError(err)) {
|
|
15257
15516
|
const msg = `could not remove existing worktree at ${worktreePath}: ${toExecMessage(err)}`;
|
|
15258
15517
|
if (options.json) {
|
|
15259
|
-
options.stderr(
|
|
15260
|
-
|
|
15518
|
+
options.stderr(
|
|
15519
|
+
`${JSON.stringify({ warning: msg }, null, 2)}
|
|
15520
|
+
`
|
|
15521
|
+
);
|
|
15261
15522
|
} else {
|
|
15262
15523
|
options.stderr(`Warning: ${msg}
|
|
15263
15524
|
`);
|
|
@@ -15266,7 +15527,9 @@ function createNewCommand(dependencies = {}) {
|
|
|
15266
15527
|
}
|
|
15267
15528
|
if (!options.detached) {
|
|
15268
15529
|
try {
|
|
15269
|
-
await execFileAsync3("git", ["branch", "-D", worktreeName], {
|
|
15530
|
+
await execFileAsync3("git", ["branch", "-D", worktreeName], {
|
|
15531
|
+
cwd: repository.repoRoot
|
|
15532
|
+
});
|
|
15270
15533
|
} catch {
|
|
15271
15534
|
}
|
|
15272
15535
|
}
|
|
@@ -15277,12 +15540,18 @@ function createNewCommand(dependencies = {}) {
|
|
|
15277
15540
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}
|
|
15278
15541
|
`);
|
|
15279
15542
|
} else {
|
|
15280
|
-
options.stderr(
|
|
15281
|
-
`)
|
|
15282
|
-
|
|
15283
|
-
|
|
15284
|
-
options.stderr(
|
|
15285
|
-
`
|
|
15543
|
+
options.stderr(
|
|
15544
|
+
`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
15545
|
+
`
|
|
15546
|
+
);
|
|
15547
|
+
options.stderr(
|
|
15548
|
+
`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree
|
|
15549
|
+
`
|
|
15550
|
+
);
|
|
15551
|
+
options.stderr(
|
|
15552
|
+
`Hint: Use 'gji trigger-hook afterCreate' inside the worktree to re-run setup hooks
|
|
15553
|
+
`
|
|
15554
|
+
);
|
|
15286
15555
|
}
|
|
15287
15556
|
return 1;
|
|
15288
15557
|
} else {
|
|
@@ -15292,53 +15561,81 @@ function createNewCommand(dependencies = {}) {
|
|
|
15292
15561
|
await writeOutput(worktreePath, options.stdout);
|
|
15293
15562
|
return 0;
|
|
15294
15563
|
}
|
|
15295
|
-
options.stderr(
|
|
15296
|
-
`
|
|
15564
|
+
options.stderr(
|
|
15565
|
+
`Aborted because target worktree path already exists: ${worktreePath}
|
|
15566
|
+
`
|
|
15567
|
+
);
|
|
15297
15568
|
return 1;
|
|
15298
15569
|
}
|
|
15299
15570
|
}
|
|
15300
15571
|
if (options.dryRun) {
|
|
15301
15572
|
if (options.json) {
|
|
15302
|
-
options.stdout(
|
|
15303
|
-
|
|
15573
|
+
options.stdout(
|
|
15574
|
+
`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
|
|
15575
|
+
`
|
|
15576
|
+
);
|
|
15304
15577
|
} else {
|
|
15305
15578
|
const resolvedEditor = options.open ? options.editor ?? resolveConfigString(config, "editor") : void 0;
|
|
15306
15579
|
const openNote = resolvedEditor ? `, then open in ${resolvedEditor}` : "";
|
|
15307
|
-
options.stdout(
|
|
15308
|
-
`)
|
|
15580
|
+
options.stdout(
|
|
15581
|
+
`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
|
|
15582
|
+
`
|
|
15583
|
+
);
|
|
15309
15584
|
}
|
|
15310
15585
|
return 0;
|
|
15311
15586
|
}
|
|
15312
15587
|
await mkdir4(dirname5(worktreePath), { recursive: true });
|
|
15313
15588
|
const gitArgs = options.detached ? ["worktree", "add", "--detach", worktreePath] : await localBranchExists(repository.repoRoot, worktreeName) ? ["worktree", "add", worktreePath, worktreeName] : ["worktree", "add", "-b", worktreeName, worktreePath];
|
|
15314
15589
|
await execFileAsync3("git", gitArgs, { cwd: repository.repoRoot });
|
|
15315
|
-
const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
|
|
15590
|
+
const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
|
|
15591
|
+
(p2) => typeof p2 === "string"
|
|
15592
|
+
) : [];
|
|
15316
15593
|
for (const pattern of syncPatterns) {
|
|
15317
15594
|
try {
|
|
15318
15595
|
await syncFiles(repository.repoRoot, worktreePath, [pattern]);
|
|
15319
15596
|
} catch (error) {
|
|
15320
|
-
options.stderr(
|
|
15321
|
-
`)
|
|
15597
|
+
options.stderr(
|
|
15598
|
+
`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
|
|
15599
|
+
`
|
|
15600
|
+
);
|
|
15322
15601
|
}
|
|
15323
15602
|
}
|
|
15324
|
-
await maybeRunInstallPrompt(
|
|
15603
|
+
await maybeRunInstallPrompt(
|
|
15604
|
+
worktreePath,
|
|
15605
|
+
repository.repoRoot,
|
|
15606
|
+
config,
|
|
15607
|
+
options.stderr,
|
|
15608
|
+
dependencies,
|
|
15609
|
+
!!options.json
|
|
15610
|
+
);
|
|
15325
15611
|
const hooks = extractHooks(config);
|
|
15326
15612
|
await runHook(
|
|
15327
15613
|
hooks.afterCreate,
|
|
15328
15614
|
worktreePath,
|
|
15329
|
-
{
|
|
15615
|
+
{
|
|
15616
|
+
branch: worktreeName,
|
|
15617
|
+
path: worktreePath,
|
|
15618
|
+
repo: basename3(repository.repoRoot)
|
|
15619
|
+
},
|
|
15330
15620
|
options.stderr
|
|
15331
15621
|
);
|
|
15332
15622
|
if (options.json) {
|
|
15333
|
-
options.stdout(
|
|
15334
|
-
|
|
15623
|
+
options.stdout(
|
|
15624
|
+
`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}
|
|
15625
|
+
`
|
|
15626
|
+
);
|
|
15335
15627
|
} else {
|
|
15336
15628
|
await appendHistory(worktreePath, worktreeName);
|
|
15337
15629
|
await writeOutput(worktreePath, options.stdout);
|
|
15338
15630
|
}
|
|
15339
15631
|
if (options.open) {
|
|
15340
15632
|
const resolvedEditor = options.editor ?? resolveConfigString(config, "editor");
|
|
15341
|
-
await openWorktree(
|
|
15633
|
+
await openWorktree(
|
|
15634
|
+
worktreePath,
|
|
15635
|
+
resolvedEditor,
|
|
15636
|
+
spawnEditor,
|
|
15637
|
+
options.stderr
|
|
15638
|
+
);
|
|
15342
15639
|
}
|
|
15343
15640
|
return 0;
|
|
15344
15641
|
};
|
|
@@ -15399,7 +15696,11 @@ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName, basePath) {
|
|
|
15399
15696
|
let attempt = 1;
|
|
15400
15697
|
while (true) {
|
|
15401
15698
|
const candidateName = attempt === 1 ? baseName : `${baseName}-${attempt}`;
|
|
15402
|
-
const candidatePath = resolveWorktreePath(
|
|
15699
|
+
const candidatePath = resolveWorktreePath(
|
|
15700
|
+
repoRoot,
|
|
15701
|
+
candidateName,
|
|
15702
|
+
basePath
|
|
15703
|
+
);
|
|
15403
15704
|
if (!await pathExists(candidatePath)) {
|
|
15404
15705
|
return candidatePath;
|
|
15405
15706
|
}
|
|
@@ -15427,7 +15728,11 @@ function pickRandom(values, random) {
|
|
|
15427
15728
|
}
|
|
15428
15729
|
async function localBranchExists(repoRoot, branchName) {
|
|
15429
15730
|
try {
|
|
15430
|
-
await execFileAsync3(
|
|
15731
|
+
await execFileAsync3(
|
|
15732
|
+
"git",
|
|
15733
|
+
["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
|
|
15734
|
+
{ cwd: repoRoot }
|
|
15735
|
+
);
|
|
15431
15736
|
return true;
|
|
15432
15737
|
} catch {
|
|
15433
15738
|
return false;
|
|
@@ -15448,7 +15753,9 @@ function toExecMessage(error) {
|
|
|
15448
15753
|
}
|
|
15449
15754
|
async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
|
|
15450
15755
|
if (!editorCli) {
|
|
15451
|
-
stderr(
|
|
15756
|
+
stderr(
|
|
15757
|
+
"gji new: --open requires --editor <cli> or a saved editor in config\n"
|
|
15758
|
+
);
|
|
15452
15759
|
return;
|
|
15453
15760
|
}
|
|
15454
15761
|
const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
|
|
@@ -15467,7 +15774,7 @@ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
|
|
|
15467
15774
|
}
|
|
15468
15775
|
|
|
15469
15776
|
// src/repo-registry.ts
|
|
15470
|
-
import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
|
|
15777
|
+
import { mkdir as mkdir5, readFile as readFile3, realpath, writeFile as writeFile4 } from "node:fs/promises";
|
|
15471
15778
|
import { homedir as homedir4 } from "node:os";
|
|
15472
15779
|
import { basename as basename4, dirname as dirname6, join as join6, resolve as resolve4 } from "node:path";
|
|
15473
15780
|
var REGISTRY_FILE_NAME = "repos.json";
|
|
@@ -15490,21 +15797,46 @@ async function loadRegistry(home = homedir4()) {
|
|
|
15490
15797
|
return [];
|
|
15491
15798
|
}
|
|
15492
15799
|
}
|
|
15800
|
+
async function canonicalizeRepoPath(repoPath) {
|
|
15801
|
+
try {
|
|
15802
|
+
return await realpath(repoPath);
|
|
15803
|
+
} catch {
|
|
15804
|
+
return resolve4(repoPath);
|
|
15805
|
+
}
|
|
15806
|
+
}
|
|
15493
15807
|
async function registerRepo(repoPath, home = homedir4()) {
|
|
15494
15808
|
const registryPath = REGISTRY_FILE_PATH(home);
|
|
15495
|
-
const existing = await loadRegistry(home);
|
|
15496
|
-
|
|
15809
|
+
const existing = await normalizeRegistryForWrite(await loadRegistry(home));
|
|
15810
|
+
const canonicalRepoPath = await canonicalizeRepoPath(repoPath);
|
|
15811
|
+
if (existing.length > 0 && existing[0].path === canonicalRepoPath) return;
|
|
15497
15812
|
const entry = {
|
|
15498
15813
|
lastUsed: Date.now(),
|
|
15499
|
-
name: basename4(
|
|
15500
|
-
path:
|
|
15814
|
+
name: basename4(canonicalRepoPath),
|
|
15815
|
+
path: canonicalRepoPath
|
|
15501
15816
|
};
|
|
15502
|
-
const filtered = existing.filter((e2) => e2.path !==
|
|
15817
|
+
const filtered = existing.filter((e2) => e2.path !== canonicalRepoPath);
|
|
15503
15818
|
const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
|
|
15504
15819
|
await mkdir5(dirname6(registryPath), { recursive: true });
|
|
15505
15820
|
await writeFile4(registryPath, `${JSON.stringify(next, null, 2)}
|
|
15506
15821
|
`, "utf8");
|
|
15507
15822
|
}
|
|
15823
|
+
async function normalizeRegistryForWrite(entries) {
|
|
15824
|
+
const normalized = [];
|
|
15825
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
15826
|
+
for (const entry of entries) {
|
|
15827
|
+
const canonicalPath = await canonicalizeRepoPath(entry.path);
|
|
15828
|
+
if (seenPaths.has(canonicalPath)) {
|
|
15829
|
+
continue;
|
|
15830
|
+
}
|
|
15831
|
+
seenPaths.add(canonicalPath);
|
|
15832
|
+
normalized.push({
|
|
15833
|
+
...entry,
|
|
15834
|
+
name: basename4(canonicalPath),
|
|
15835
|
+
path: canonicalPath
|
|
15836
|
+
});
|
|
15837
|
+
}
|
|
15838
|
+
return normalized;
|
|
15839
|
+
}
|
|
15508
15840
|
function isRegistryEntry(value) {
|
|
15509
15841
|
return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string" && "name" in value && typeof value.name === "string" && "lastUsed" in value && typeof value.lastUsed === "number";
|
|
15510
15842
|
}
|
|
@@ -15537,11 +15869,17 @@ async function runWarpNavigate(options) {
|
|
|
15537
15869
|
}
|
|
15538
15870
|
return 1;
|
|
15539
15871
|
}
|
|
15540
|
-
const target = await resolveWarpTarget({
|
|
15872
|
+
const target = await resolveWarpTarget({
|
|
15873
|
+
...options,
|
|
15874
|
+
commandName: "gji warp",
|
|
15875
|
+
json: options.json
|
|
15876
|
+
});
|
|
15541
15877
|
if (!target) return 1;
|
|
15542
15878
|
if (options.json) {
|
|
15543
|
-
options.stdout(
|
|
15544
|
-
|
|
15879
|
+
options.stdout(
|
|
15880
|
+
`${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}
|
|
15881
|
+
`
|
|
15882
|
+
);
|
|
15545
15883
|
return 0;
|
|
15546
15884
|
}
|
|
15547
15885
|
appendHistory(target.path, target.branch).catch(() => void 0);
|
|
@@ -15549,9 +15887,10 @@ async function runWarpNavigate(options) {
|
|
|
15549
15887
|
return 0;
|
|
15550
15888
|
}
|
|
15551
15889
|
async function runWarpNew(options, registry) {
|
|
15890
|
+
const deduplicatedRegistry = await deduplicateRegistryForNew(registry);
|
|
15552
15891
|
let targetRepoRoot;
|
|
15553
|
-
if (
|
|
15554
|
-
targetRepoRoot =
|
|
15892
|
+
if (deduplicatedRegistry.length === 1) {
|
|
15893
|
+
targetRepoRoot = deduplicatedRegistry[0].path;
|
|
15555
15894
|
} else {
|
|
15556
15895
|
if (isHeadless()) {
|
|
15557
15896
|
options.stderr(
|
|
@@ -15561,7 +15900,7 @@ async function runWarpNew(options, registry) {
|
|
|
15561
15900
|
}
|
|
15562
15901
|
const choice = await ve({
|
|
15563
15902
|
message: "Create worktree in which repo?",
|
|
15564
|
-
options:
|
|
15903
|
+
options: deduplicatedRegistry.map((entry) => ({
|
|
15565
15904
|
value: entry.path,
|
|
15566
15905
|
label: entry.name,
|
|
15567
15906
|
hint: entry.path
|
|
@@ -15602,6 +15941,30 @@ async function runWarpNew(options, registry) {
|
|
|
15602
15941
|
await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
|
|
15603
15942
|
return 0;
|
|
15604
15943
|
}
|
|
15944
|
+
async function deduplicateRegistryForNew(registry) {
|
|
15945
|
+
const deduplicated = [];
|
|
15946
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
15947
|
+
for (const entry of registry) {
|
|
15948
|
+
const canonicalPath = await canonicalizeRepoPath2(entry.path);
|
|
15949
|
+
if (seenPaths.has(canonicalPath)) {
|
|
15950
|
+
continue;
|
|
15951
|
+
}
|
|
15952
|
+
seenPaths.add(canonicalPath);
|
|
15953
|
+
deduplicated.push({
|
|
15954
|
+
...entry,
|
|
15955
|
+
name: basename5(canonicalPath),
|
|
15956
|
+
path: canonicalPath
|
|
15957
|
+
});
|
|
15958
|
+
}
|
|
15959
|
+
return deduplicated;
|
|
15960
|
+
}
|
|
15961
|
+
async function canonicalizeRepoPath2(repoPath) {
|
|
15962
|
+
try {
|
|
15963
|
+
return await realpath2(repoPath);
|
|
15964
|
+
} catch {
|
|
15965
|
+
return resolve5(repoPath);
|
|
15966
|
+
}
|
|
15967
|
+
}
|
|
15605
15968
|
function findByQuery(items, query) {
|
|
15606
15969
|
const slashIdx = query.indexOf("/");
|
|
15607
15970
|
if (slashIdx !== -1) {
|
|
@@ -15717,14 +16080,19 @@ function createGoCommand(dependencies = {}) {
|
|
|
15717
16080
|
);
|
|
15718
16081
|
return 1;
|
|
15719
16082
|
}
|
|
15720
|
-
const target = await resolveWarpTarget({
|
|
16083
|
+
const target = await resolveWarpTarget({
|
|
16084
|
+
...options,
|
|
16085
|
+
commandName: "gji go"
|
|
16086
|
+
});
|
|
15721
16087
|
if (!target) return 1;
|
|
15722
16088
|
appendHistory(target.path, target.branch).catch(() => void 0);
|
|
15723
16089
|
await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
|
|
15724
16090
|
return 0;
|
|
15725
16091
|
}
|
|
15726
16092
|
if (!options.branch && isHeadless()) {
|
|
15727
|
-
options.stderr(
|
|
16093
|
+
options.stderr(
|
|
16094
|
+
"gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
|
|
16095
|
+
);
|
|
15728
16096
|
return 1;
|
|
15729
16097
|
}
|
|
15730
16098
|
const prompted = options.branch ? null : await prompt(sortByCurrentFirst(worktrees));
|
|
@@ -15740,204 +16108,83 @@ function createGoCommand(dependencies = {}) {
|
|
|
15740
16108
|
}
|
|
15741
16109
|
return 1;
|
|
15742
16110
|
}
|
|
15743
|
-
const chosenWorktree = worktrees.find((w2) => w2.path === resolvedPath);
|
|
15744
|
-
const config = await loadEffectiveConfig(
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
|
|
15750
|
-
|
|
15751
|
-
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
|
|
15764
|
-
options: worktrees.map((worktree, i) => {
|
|
15765
|
-
const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
|
|
15766
|
-
const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
|
|
15767
|
-
const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
|
|
15768
|
-
return {
|
|
15769
|
-
value: worktree.path,
|
|
15770
|
-
label: worktree.branch ?? "(detached)",
|
|
15771
|
-
hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
|
|
15772
|
-
};
|
|
15773
|
-
})
|
|
15774
|
-
});
|
|
15775
|
-
if (pD(choice)) {
|
|
15776
|
-
return null;
|
|
15777
|
-
}
|
|
15778
|
-
return choice;
|
|
15779
|
-
}
|
|
15780
|
-
function formatUpstreamHint(branch, health) {
|
|
15781
|
-
if (branch === null) return null;
|
|
15782
|
-
if (!health.hasUpstream) return "no upstream";
|
|
15783
|
-
if (health.upstreamGone) return "upstream gone";
|
|
15784
|
-
if (health.ahead === 0 && health.behind === 0) return "up to date";
|
|
15785
|
-
if (health.ahead === 0) return `behind ${health.behind}`;
|
|
15786
|
-
if (health.behind === 0) return `ahead ${health.ahead}`;
|
|
15787
|
-
return `ahead ${health.ahead}, behind ${health.behind}`;
|
|
15788
|
-
}
|
|
15789
|
-
|
|
15790
|
-
// src/open.ts
|
|
15791
|
-
import { execFile as execFile4 } from "node:child_process";
|
|
15792
|
-
import { access as access4, writeFile as writeFile5 } from "node:fs/promises";
|
|
15793
|
-
import { join as join7 } from "node:path";
|
|
15794
|
-
import { promisify as promisify5 } from "node:util";
|
|
15795
|
-
var execFileAsync4 = promisify5(execFile4);
|
|
15796
|
-
function createOpenCommand(dependencies = {}) {
|
|
15797
|
-
const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
|
|
15798
|
-
const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
|
|
15799
|
-
const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
|
|
15800
|
-
const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
|
|
15801
|
-
return async function runOpenCommand2(options) {
|
|
15802
|
-
const [worktrees, repository] = await Promise.all([
|
|
15803
|
-
listWorktrees(options.cwd),
|
|
15804
|
-
detectRepository(options.cwd)
|
|
15805
|
-
]);
|
|
15806
|
-
let targetPath;
|
|
15807
|
-
if (options.branch) {
|
|
15808
|
-
const entry = worktrees.find((w2) => w2.branch === options.branch);
|
|
15809
|
-
if (!entry) {
|
|
15810
|
-
options.stderr(`gji open: no worktree found for branch: ${options.branch}
|
|
15811
|
-
`);
|
|
15812
|
-
options.stderr(`Hint: Use 'gji ls' to see available worktrees
|
|
15813
|
-
`);
|
|
15814
|
-
return 1;
|
|
15815
|
-
}
|
|
15816
|
-
targetPath = entry.path;
|
|
15817
|
-
} else if (isHeadless()) {
|
|
15818
|
-
targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
|
|
15819
|
-
} else {
|
|
15820
|
-
const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
|
|
15821
|
-
if (!chosen) {
|
|
15822
|
-
options.stderr("Aborted\n");
|
|
15823
|
-
return 1;
|
|
15824
|
-
}
|
|
15825
|
-
targetPath = chosen;
|
|
15826
|
-
}
|
|
15827
|
-
const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
|
|
15828
|
-
const savedEditor = resolveConfigString(config, "editor");
|
|
15829
|
-
let editorCli;
|
|
15830
|
-
if (options.editor) {
|
|
15831
|
-
editorCli = options.editor;
|
|
15832
|
-
} else if (savedEditor) {
|
|
15833
|
-
editorCli = savedEditor;
|
|
15834
|
-
} else {
|
|
15835
|
-
const installed = await detectEditors();
|
|
15836
|
-
if (installed.length === 0) {
|
|
15837
|
-
options.stderr(
|
|
15838
|
-
"gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n"
|
|
15839
|
-
);
|
|
15840
|
-
return 1;
|
|
15841
|
-
}
|
|
15842
|
-
if (installed.length === 1 || isHeadless()) {
|
|
15843
|
-
editorCli = installed[0].cli;
|
|
15844
|
-
} else {
|
|
15845
|
-
const chosen = await promptForEditor(installed);
|
|
15846
|
-
if (!chosen) {
|
|
15847
|
-
options.stderr("Aborted\n");
|
|
15848
|
-
return 1;
|
|
15849
|
-
}
|
|
15850
|
-
editorCli = chosen;
|
|
15851
|
-
}
|
|
15852
|
-
}
|
|
15853
|
-
if (options.save && editorCli !== savedEditor) {
|
|
15854
|
-
await updateGlobalConfigKey("editor", editorCli);
|
|
15855
|
-
const displayName2 = EDITORS.find((e2) => e2.cli === editorCli)?.name ?? editorCli;
|
|
15856
|
-
options.stdout(`Saved editor "${displayName2}" to global config
|
|
15857
|
-
`);
|
|
15858
|
-
}
|
|
15859
|
-
const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
|
|
15860
|
-
let openTarget = targetPath;
|
|
15861
|
-
if (options.workspace) {
|
|
15862
|
-
if (editorDef?.supportsWorkspace) {
|
|
15863
|
-
openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
|
|
15864
|
-
} else {
|
|
15865
|
-
const displayName2 = editorDef?.name ?? editorCli;
|
|
15866
|
-
options.stderr(`gji open: --workspace is not supported for ${displayName2}, ignoring
|
|
15867
|
-
`);
|
|
15868
|
-
}
|
|
15869
|
-
}
|
|
15870
|
-
const args = [];
|
|
15871
|
-
if (editorDef?.newWindowFlag) {
|
|
15872
|
-
args.push(editorDef.newWindowFlag);
|
|
15873
|
-
}
|
|
15874
|
-
args.push(openTarget);
|
|
15875
|
-
try {
|
|
15876
|
-
await spawnEditor(editorCli, args);
|
|
15877
|
-
} catch (error) {
|
|
15878
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
15879
|
-
options.stderr(`gji open: failed to launch editor: ${message}
|
|
15880
|
-
`);
|
|
15881
|
-
return 1;
|
|
15882
|
-
}
|
|
15883
|
-
const displayName = editorDef?.name ?? editorCli;
|
|
15884
|
-
options.stdout(`Opened ${targetPath} in ${displayName}
|
|
15885
|
-
`);
|
|
16111
|
+
const chosenWorktree = worktrees.find((w2) => w2.path === resolvedPath);
|
|
16112
|
+
const config = await loadEffectiveConfig(
|
|
16113
|
+
repository.repoRoot,
|
|
16114
|
+
void 0,
|
|
16115
|
+
options.stderr
|
|
16116
|
+
);
|
|
16117
|
+
const hooks = extractHooks(config);
|
|
16118
|
+
await runHook(
|
|
16119
|
+
hooks.afterEnter,
|
|
16120
|
+
resolvedPath,
|
|
16121
|
+
{
|
|
16122
|
+
branch: chosenWorktree?.branch ?? void 0,
|
|
16123
|
+
path: resolvedPath,
|
|
16124
|
+
repo: basename6(repository.repoRoot)
|
|
16125
|
+
},
|
|
16126
|
+
options.stderr
|
|
16127
|
+
);
|
|
16128
|
+
appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(
|
|
16129
|
+
() => void 0
|
|
16130
|
+
);
|
|
16131
|
+
await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
|
|
15886
16132
|
return 0;
|
|
15887
16133
|
};
|
|
15888
16134
|
}
|
|
15889
|
-
var
|
|
15890
|
-
async function
|
|
15891
|
-
const
|
|
15892
|
-
|
|
16135
|
+
var runGoCommand = createGoCommand();
|
|
16136
|
+
async function promptForWorktree(worktrees) {
|
|
16137
|
+
const healthResults = await Promise.allSettled(
|
|
16138
|
+
worktrees.map((w2) => readWorktreeHealth(w2.path))
|
|
15893
16139
|
);
|
|
15894
|
-
return results.filter((r2) => r2.available).map((r2) => r2.editor);
|
|
15895
|
-
}
|
|
15896
|
-
async function isCommandAvailable(command) {
|
|
15897
|
-
try {
|
|
15898
|
-
await execFileAsync4("which", [command]);
|
|
15899
|
-
return true;
|
|
15900
|
-
} catch {
|
|
15901
|
-
return false;
|
|
15902
|
-
}
|
|
15903
|
-
}
|
|
15904
|
-
async function defaultPromptForWorktree(worktrees) {
|
|
15905
16140
|
const choice = await ve({
|
|
15906
|
-
message: "Choose a worktree
|
|
15907
|
-
options: worktrees.map((
|
|
15908
|
-
value:
|
|
15909
|
-
|
|
15910
|
-
|
|
15911
|
-
|
|
16141
|
+
message: "Choose a worktree",
|
|
16142
|
+
options: worktrees.map((worktree, i) => {
|
|
16143
|
+
const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
|
|
16144
|
+
const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
|
|
16145
|
+
const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
|
|
16146
|
+
return {
|
|
16147
|
+
value: worktree.path,
|
|
16148
|
+
label: worktree.branch ?? "(detached)",
|
|
16149
|
+
hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
|
|
16150
|
+
};
|
|
16151
|
+
})
|
|
15912
16152
|
});
|
|
15913
|
-
if (pD(choice))
|
|
16153
|
+
if (pD(choice)) {
|
|
16154
|
+
return null;
|
|
16155
|
+
}
|
|
15914
16156
|
return choice;
|
|
15915
16157
|
}
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
|
|
15919
|
-
|
|
15920
|
-
|
|
15921
|
-
if (
|
|
15922
|
-
return
|
|
16158
|
+
function formatUpstreamHint(branch, health) {
|
|
16159
|
+
if (branch === null) return null;
|
|
16160
|
+
if (!health.hasUpstream) return "no upstream";
|
|
16161
|
+
if (health.upstreamGone) return "upstream gone";
|
|
16162
|
+
if (health.ahead === 0 && health.behind === 0) return "up to date";
|
|
16163
|
+
if (health.ahead === 0) return `behind ${health.behind}`;
|
|
16164
|
+
if (health.behind === 0) return `ahead ${health.ahead}`;
|
|
16165
|
+
return `ahead ${health.ahead}, behind ${health.behind}`;
|
|
15923
16166
|
}
|
|
15924
|
-
|
|
15925
|
-
|
|
15926
|
-
|
|
15927
|
-
|
|
15928
|
-
|
|
15929
|
-
|
|
16167
|
+
|
|
16168
|
+
// src/history-command.ts
|
|
16169
|
+
async function runHistoryCommand(options) {
|
|
16170
|
+
const history = await loadHistory(options.home);
|
|
16171
|
+
if (options.json) {
|
|
16172
|
+
options.stdout(`${JSON.stringify(history, null, 2)}
|
|
16173
|
+
`);
|
|
16174
|
+
return 0;
|
|
15930
16175
|
}
|
|
15931
|
-
|
|
15932
|
-
|
|
15933
|
-
|
|
15934
|
-
|
|
16176
|
+
if (history.length === 0) {
|
|
16177
|
+
options.stdout("No navigation history.\n");
|
|
16178
|
+
return 0;
|
|
16179
|
+
}
|
|
16180
|
+
options.stdout(formatHistoryList(history, options.cwd));
|
|
16181
|
+
return 0;
|
|
15935
16182
|
}
|
|
15936
16183
|
|
|
15937
16184
|
// src/init.ts
|
|
15938
|
-
import { mkdir as mkdir6, readFile as readFile4, writeFile as
|
|
16185
|
+
import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
|
|
15939
16186
|
import { homedir as homedir5 } from "node:os";
|
|
15940
|
-
import { dirname as dirname7, join as
|
|
16187
|
+
import { dirname as dirname7, join as join7 } from "node:path";
|
|
15941
16188
|
var START_MARKER = "# >>> gji init >>>";
|
|
15942
16189
|
var END_MARKER = "# <<< gji init <<<";
|
|
15943
16190
|
var SHELL_WRAPPED_COMMANDS = [
|
|
@@ -16009,7 +16256,7 @@ async function runInitCommand(options) {
|
|
|
16009
16256
|
await mkdir6(dirname7(rcPath), { recursive: true });
|
|
16010
16257
|
const current = await readExistingConfig(rcPath);
|
|
16011
16258
|
const next = upsertShellIntegration(current, script);
|
|
16012
|
-
await
|
|
16259
|
+
await writeFile5(rcPath, next, "utf8");
|
|
16013
16260
|
options.stdout(`${rcPath}
|
|
16014
16261
|
`);
|
|
16015
16262
|
const { config: globalConfig } = await loadGlobalConfig(home);
|
|
@@ -16020,7 +16267,11 @@ async function runInitCommand(options) {
|
|
|
16020
16267
|
const prompt = options.promptForSetup ?? defaultPromptForSetup;
|
|
16021
16268
|
const result = await prompt();
|
|
16022
16269
|
if (result) {
|
|
16023
|
-
await updateGlobalConfigKey(
|
|
16270
|
+
await updateGlobalConfigKey(
|
|
16271
|
+
"installSaveTarget",
|
|
16272
|
+
result.installSaveTarget,
|
|
16273
|
+
home
|
|
16274
|
+
);
|
|
16024
16275
|
await saveWizardConfig(result, options.cwd, home);
|
|
16025
16276
|
}
|
|
16026
16277
|
}
|
|
@@ -16080,7 +16331,8 @@ async function saveWizardConfig(result, cwd, home) {
|
|
|
16080
16331
|
const hooks = {};
|
|
16081
16332
|
if (result.hooks?.afterCreate) hooks.afterCreate = result.hooks.afterCreate;
|
|
16082
16333
|
if (result.hooks?.afterEnter) hooks.afterEnter = result.hooks.afterEnter;
|
|
16083
|
-
if (result.hooks?.beforeRemove)
|
|
16334
|
+
if (result.hooks?.beforeRemove)
|
|
16335
|
+
hooks.beforeRemove = result.hooks.beforeRemove;
|
|
16084
16336
|
if (Object.keys(hooks).length > 0) values.hooks = hooks;
|
|
16085
16337
|
if (Object.keys(values).length === 0) return;
|
|
16086
16338
|
if (result.installSaveTarget === "local") {
|
|
@@ -16094,11 +16346,11 @@ async function saveWizardConfig(result, cwd, home) {
|
|
|
16094
16346
|
function resolveShellConfigPath(shell, home) {
|
|
16095
16347
|
switch (shell) {
|
|
16096
16348
|
case "bash":
|
|
16097
|
-
return
|
|
16349
|
+
return join7(home, ".bashrc");
|
|
16098
16350
|
case "fish":
|
|
16099
|
-
return
|
|
16351
|
+
return join7(home, ".config", "fish", "config.fish");
|
|
16100
16352
|
case "zsh":
|
|
16101
|
-
return
|
|
16353
|
+
return join7(home, ".zshrc");
|
|
16102
16354
|
}
|
|
16103
16355
|
}
|
|
16104
16356
|
async function readExistingConfig(path9) {
|
|
@@ -16124,7 +16376,9 @@ function isMissingFileError2(error) {
|
|
|
16124
16376
|
function renderFishWrapper(command) {
|
|
16125
16377
|
const nameTests = command.names.map((name) => `test $argv[1] = ${name}`);
|
|
16126
16378
|
const nameCondition = nameTests.length === 1 ? nameTests[0] : `begin; ${nameTests.join("; or ")}; end`;
|
|
16127
|
-
const bypassTests = command.bypassOptions.map(
|
|
16379
|
+
const bypassTests = command.bypassOptions.map(
|
|
16380
|
+
(opt) => `test $argv[1] = ${opt}`
|
|
16381
|
+
);
|
|
16128
16382
|
const bypassCondition = bypassTests.length === 1 ? bypassTests[0] : `begin; ${bypassTests.join("; or ")}; end`;
|
|
16129
16383
|
return `if test (count $argv) -gt 0; and ${nameCondition}
|
|
16130
16384
|
set -e argv[1]
|
|
@@ -16176,8 +16430,16 @@ async function defaultPromptForSetup() {
|
|
|
16176
16430
|
const installSaveTarget = await ve({
|
|
16177
16431
|
message: "Where should preferences be saved?",
|
|
16178
16432
|
options: [
|
|
16179
|
-
{
|
|
16180
|
-
|
|
16433
|
+
{
|
|
16434
|
+
value: "global",
|
|
16435
|
+
label: "~/.config/gji/config.json",
|
|
16436
|
+
hint: "personal \u2014 never committed"
|
|
16437
|
+
},
|
|
16438
|
+
{
|
|
16439
|
+
value: "local",
|
|
16440
|
+
label: ".gji.json",
|
|
16441
|
+
hint: "repo \u2014 committed with the project"
|
|
16442
|
+
}
|
|
16181
16443
|
]
|
|
16182
16444
|
});
|
|
16183
16445
|
if (pD(installSaveTarget)) {
|
|
@@ -16280,10 +16542,22 @@ function formatDetailedWorktreeTable(worktrees) {
|
|
|
16280
16542
|
status: worktree.status,
|
|
16281
16543
|
upstream: formatUpstreamState(worktree.upstream)
|
|
16282
16544
|
}));
|
|
16283
|
-
const branchWidth = Math.max(
|
|
16284
|
-
|
|
16285
|
-
|
|
16286
|
-
|
|
16545
|
+
const branchWidth = Math.max(
|
|
16546
|
+
"BRANCH".length,
|
|
16547
|
+
...rows.map((row) => row.branch.length)
|
|
16548
|
+
);
|
|
16549
|
+
const statusWidth = Math.max(
|
|
16550
|
+
"STATUS".length,
|
|
16551
|
+
...rows.map((row) => row.status.length)
|
|
16552
|
+
);
|
|
16553
|
+
const upstreamWidth = Math.max(
|
|
16554
|
+
"UPSTREAM".length,
|
|
16555
|
+
...rows.map((row) => row.upstream.length)
|
|
16556
|
+
);
|
|
16557
|
+
const lastCommitWidth = Math.max(
|
|
16558
|
+
"LAST".length,
|
|
16559
|
+
...rows.map((row) => row.lastCommit.length)
|
|
16560
|
+
);
|
|
16287
16561
|
const lines = [
|
|
16288
16562
|
" " + "BRANCH".padEnd(branchWidth, " ") + " " + "STATUS".padEnd(statusWidth, " ") + " " + "UPSTREAM".padEnd(upstreamWidth, " ") + " " + "LAST".padEnd(lastCommitWidth, " ") + " PATH"
|
|
16289
16563
|
];
|
|
@@ -16306,7 +16580,9 @@ function formatWorktreeTable(worktrees) {
|
|
|
16306
16580
|
);
|
|
16307
16581
|
const lines = [" " + "BRANCH".padEnd(branchWidth, " ") + " PATH"];
|
|
16308
16582
|
for (const row of rows) {
|
|
16309
|
-
lines.push(
|
|
16583
|
+
lines.push(
|
|
16584
|
+
`${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`
|
|
16585
|
+
);
|
|
16310
16586
|
}
|
|
16311
16587
|
return lines.join("\n");
|
|
16312
16588
|
}
|
|
@@ -16318,10 +16594,172 @@ function sortWorktrees(worktrees) {
|
|
|
16318
16594
|
});
|
|
16319
16595
|
}
|
|
16320
16596
|
|
|
16597
|
+
// src/open.ts
|
|
16598
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
16599
|
+
import { access as access4, writeFile as writeFile6 } from "node:fs/promises";
|
|
16600
|
+
import { join as join8 } from "node:path";
|
|
16601
|
+
import { promisify as promisify5 } from "node:util";
|
|
16602
|
+
var execFileAsync4 = promisify5(execFile4);
|
|
16603
|
+
function createOpenCommand(dependencies = {}) {
|
|
16604
|
+
const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
|
|
16605
|
+
const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
|
|
16606
|
+
const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
|
|
16607
|
+
const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
|
|
16608
|
+
return async function runOpenCommand2(options) {
|
|
16609
|
+
const [worktrees, repository] = await Promise.all([
|
|
16610
|
+
listWorktrees(options.cwd),
|
|
16611
|
+
detectRepository(options.cwd)
|
|
16612
|
+
]);
|
|
16613
|
+
let targetPath;
|
|
16614
|
+
if (options.branch) {
|
|
16615
|
+
const entry = worktrees.find((w2) => w2.branch === options.branch);
|
|
16616
|
+
if (!entry) {
|
|
16617
|
+
options.stderr(
|
|
16618
|
+
`gji open: no worktree found for branch: ${options.branch}
|
|
16619
|
+
`
|
|
16620
|
+
);
|
|
16621
|
+
options.stderr(`Hint: Use 'gji ls' to see available worktrees
|
|
16622
|
+
`);
|
|
16623
|
+
return 1;
|
|
16624
|
+
}
|
|
16625
|
+
targetPath = entry.path;
|
|
16626
|
+
} else if (isHeadless()) {
|
|
16627
|
+
targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
|
|
16628
|
+
} else {
|
|
16629
|
+
const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
|
|
16630
|
+
if (!chosen) {
|
|
16631
|
+
options.stderr("Aborted\n");
|
|
16632
|
+
return 1;
|
|
16633
|
+
}
|
|
16634
|
+
targetPath = chosen;
|
|
16635
|
+
}
|
|
16636
|
+
const config = await loadEffectiveConfig(
|
|
16637
|
+
repository.repoRoot,
|
|
16638
|
+
void 0,
|
|
16639
|
+
options.stderr
|
|
16640
|
+
);
|
|
16641
|
+
const savedEditor = resolveConfigString(config, "editor");
|
|
16642
|
+
let editorCli;
|
|
16643
|
+
if (options.editor) {
|
|
16644
|
+
editorCli = options.editor;
|
|
16645
|
+
} else if (savedEditor) {
|
|
16646
|
+
editorCli = savedEditor;
|
|
16647
|
+
} else {
|
|
16648
|
+
const installed = await detectEditors();
|
|
16649
|
+
if (installed.length === 0) {
|
|
16650
|
+
options.stderr(
|
|
16651
|
+
"gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n"
|
|
16652
|
+
);
|
|
16653
|
+
return 1;
|
|
16654
|
+
}
|
|
16655
|
+
if (installed.length === 1 || isHeadless()) {
|
|
16656
|
+
editorCli = installed[0].cli;
|
|
16657
|
+
} else {
|
|
16658
|
+
const chosen = await promptForEditor(installed);
|
|
16659
|
+
if (!chosen) {
|
|
16660
|
+
options.stderr("Aborted\n");
|
|
16661
|
+
return 1;
|
|
16662
|
+
}
|
|
16663
|
+
editorCli = chosen;
|
|
16664
|
+
}
|
|
16665
|
+
}
|
|
16666
|
+
if (options.save && editorCli !== savedEditor) {
|
|
16667
|
+
await updateGlobalConfigKey("editor", editorCli);
|
|
16668
|
+
const displayName2 = EDITORS.find((e2) => e2.cli === editorCli)?.name ?? editorCli;
|
|
16669
|
+
options.stdout(`Saved editor "${displayName2}" to global config
|
|
16670
|
+
`);
|
|
16671
|
+
}
|
|
16672
|
+
const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
|
|
16673
|
+
let openTarget = targetPath;
|
|
16674
|
+
if (options.workspace) {
|
|
16675
|
+
if (editorDef?.supportsWorkspace) {
|
|
16676
|
+
openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
|
|
16677
|
+
} else {
|
|
16678
|
+
const displayName2 = editorDef?.name ?? editorCli;
|
|
16679
|
+
options.stderr(
|
|
16680
|
+
`gji open: --workspace is not supported for ${displayName2}, ignoring
|
|
16681
|
+
`
|
|
16682
|
+
);
|
|
16683
|
+
}
|
|
16684
|
+
}
|
|
16685
|
+
const args = [];
|
|
16686
|
+
if (editorDef?.newWindowFlag) {
|
|
16687
|
+
args.push(editorDef.newWindowFlag);
|
|
16688
|
+
}
|
|
16689
|
+
args.push(openTarget);
|
|
16690
|
+
try {
|
|
16691
|
+
await spawnEditor(editorCli, args);
|
|
16692
|
+
} catch (error) {
|
|
16693
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16694
|
+
options.stderr(`gji open: failed to launch editor: ${message}
|
|
16695
|
+
`);
|
|
16696
|
+
return 1;
|
|
16697
|
+
}
|
|
16698
|
+
const displayName = editorDef?.name ?? editorCli;
|
|
16699
|
+
options.stdout(`Opened ${targetPath} in ${displayName}
|
|
16700
|
+
`);
|
|
16701
|
+
return 0;
|
|
16702
|
+
};
|
|
16703
|
+
}
|
|
16704
|
+
var runOpenCommand = createOpenCommand();
|
|
16705
|
+
async function detectInstalledEditors() {
|
|
16706
|
+
const results = await Promise.all(
|
|
16707
|
+
EDITORS.map(async (editor) => ({
|
|
16708
|
+
editor,
|
|
16709
|
+
available: await isCommandAvailable(editor.cli)
|
|
16710
|
+
}))
|
|
16711
|
+
);
|
|
16712
|
+
return results.filter((r2) => r2.available).map((r2) => r2.editor);
|
|
16713
|
+
}
|
|
16714
|
+
async function isCommandAvailable(command) {
|
|
16715
|
+
try {
|
|
16716
|
+
await execFileAsync4("which", [command]);
|
|
16717
|
+
return true;
|
|
16718
|
+
} catch {
|
|
16719
|
+
return false;
|
|
16720
|
+
}
|
|
16721
|
+
}
|
|
16722
|
+
async function defaultPromptForWorktree(worktrees) {
|
|
16723
|
+
const choice = await ve({
|
|
16724
|
+
message: "Choose a worktree to open",
|
|
16725
|
+
options: worktrees.map((w2) => ({
|
|
16726
|
+
value: w2.path,
|
|
16727
|
+
label: w2.branch ?? "(detached)",
|
|
16728
|
+
hint: w2.isCurrent ? `${w2.path} (current)` : w2.path
|
|
16729
|
+
}))
|
|
16730
|
+
});
|
|
16731
|
+
if (pD(choice)) return null;
|
|
16732
|
+
return choice;
|
|
16733
|
+
}
|
|
16734
|
+
async function defaultPromptForEditor(editors) {
|
|
16735
|
+
const choice = await ve({
|
|
16736
|
+
message: "Choose an editor",
|
|
16737
|
+
options: editors.map((e2) => ({ value: e2.cli, label: e2.name }))
|
|
16738
|
+
});
|
|
16739
|
+
if (pD(choice)) return null;
|
|
16740
|
+
return choice;
|
|
16741
|
+
}
|
|
16742
|
+
async function ensureWorkspaceFile(worktreePath, repoName) {
|
|
16743
|
+
const workspacePath = join8(worktreePath, `${repoName}.code-workspace`);
|
|
16744
|
+
try {
|
|
16745
|
+
await access4(workspacePath);
|
|
16746
|
+
return workspacePath;
|
|
16747
|
+
} catch {
|
|
16748
|
+
}
|
|
16749
|
+
const workspace = { folders: [{ path: "." }], settings: {} };
|
|
16750
|
+
await writeFile6(
|
|
16751
|
+
workspacePath,
|
|
16752
|
+
`${JSON.stringify(workspace, null, 2)}
|
|
16753
|
+
`,
|
|
16754
|
+
"utf8"
|
|
16755
|
+
);
|
|
16756
|
+
return workspacePath;
|
|
16757
|
+
}
|
|
16758
|
+
|
|
16321
16759
|
// src/pr.ts
|
|
16322
|
-
import { mkdir as mkdir7 } from "node:fs/promises";
|
|
16323
|
-
import { basename as basename6, dirname as dirname8 } from "node:path";
|
|
16324
16760
|
import { execFile as execFile5 } from "node:child_process";
|
|
16761
|
+
import { mkdir as mkdir7 } from "node:fs/promises";
|
|
16762
|
+
import { basename as basename7, dirname as dirname8 } from "node:path";
|
|
16325
16763
|
import { promisify as promisify6 } from "node:util";
|
|
16326
16764
|
var execFileAsync5 = promisify6(execFile5);
|
|
16327
16765
|
var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
|
|
@@ -16329,7 +16767,9 @@ function parsePrInput(input) {
|
|
|
16329
16767
|
if (/^\d+$/.test(input)) return input;
|
|
16330
16768
|
const hashMatch = input.match(/^#(\d+)$/);
|
|
16331
16769
|
if (hashMatch) return hashMatch[1];
|
|
16332
|
-
const urlMatch = input.match(
|
|
16770
|
+
const urlMatch = input.match(
|
|
16771
|
+
/\/(?:pull|pull-requests|merge_requests)\/(\d+)/
|
|
16772
|
+
);
|
|
16333
16773
|
if (urlMatch) return urlMatch[1];
|
|
16334
16774
|
return null;
|
|
16335
16775
|
}
|
|
@@ -16349,12 +16789,20 @@ function createPrCommand(dependencies = {}) {
|
|
|
16349
16789
|
return 1;
|
|
16350
16790
|
}
|
|
16351
16791
|
const repository = await detectRepository(options.cwd);
|
|
16352
|
-
const config = await loadEffectiveConfig(
|
|
16792
|
+
const config = await loadEffectiveConfig(
|
|
16793
|
+
repository.repoRoot,
|
|
16794
|
+
void 0,
|
|
16795
|
+
options.stderr
|
|
16796
|
+
);
|
|
16353
16797
|
const branchName = `pr/${prNumber}`;
|
|
16354
16798
|
const remoteRef = `refs/remotes/origin/pull/${prNumber}/head`;
|
|
16355
16799
|
const rawBasePath = resolveConfigString(config, "worktreePath");
|
|
16356
16800
|
const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
|
|
16357
|
-
const worktreePath = resolveWorktreePath(
|
|
16801
|
+
const worktreePath = resolveWorktreePath(
|
|
16802
|
+
repository.repoRoot,
|
|
16803
|
+
branchName,
|
|
16804
|
+
configuredBasePath
|
|
16805
|
+
);
|
|
16358
16806
|
if (await pathExists(worktreePath)) {
|
|
16359
16807
|
if (options.json || isHeadless()) {
|
|
16360
16808
|
const message = `target worktree path already exists: ${worktreePath}`;
|
|
@@ -16362,10 +16810,14 @@ function createPrCommand(dependencies = {}) {
|
|
|
16362
16810
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}
|
|
16363
16811
|
`);
|
|
16364
16812
|
} else {
|
|
16365
|
-
options.stderr(
|
|
16366
|
-
`)
|
|
16367
|
-
|
|
16368
|
-
|
|
16813
|
+
options.stderr(
|
|
16814
|
+
`gji pr: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
16815
|
+
`
|
|
16816
|
+
);
|
|
16817
|
+
options.stderr(
|
|
16818
|
+
`Hint: Use 'gji remove pr/${prNumber}' or 'gji clean' to remove the existing worktree
|
|
16819
|
+
`
|
|
16820
|
+
);
|
|
16369
16821
|
}
|
|
16370
16822
|
return 1;
|
|
16371
16823
|
}
|
|
@@ -16375,22 +16827,33 @@ function createPrCommand(dependencies = {}) {
|
|
|
16375
16827
|
await writeOutput2(worktreePath, options.stdout);
|
|
16376
16828
|
return 0;
|
|
16377
16829
|
}
|
|
16378
|
-
options.stderr(
|
|
16379
|
-
`
|
|
16830
|
+
options.stderr(
|
|
16831
|
+
`Aborted because target worktree path already exists: ${worktreePath}
|
|
16832
|
+
`
|
|
16833
|
+
);
|
|
16380
16834
|
return 1;
|
|
16381
16835
|
}
|
|
16382
16836
|
if (options.dryRun) {
|
|
16383
16837
|
if (options.json) {
|
|
16384
|
-
options.stdout(
|
|
16385
|
-
|
|
16838
|
+
options.stdout(
|
|
16839
|
+
`${JSON.stringify({ branch: branchName, path: worktreePath, dryRun: true }, null, 2)}
|
|
16840
|
+
`
|
|
16841
|
+
);
|
|
16386
16842
|
} else {
|
|
16387
|
-
options.stdout(
|
|
16388
|
-
`)
|
|
16843
|
+
options.stdout(
|
|
16844
|
+
`Would create worktree at ${worktreePath} (branch: ${branchName})
|
|
16845
|
+
`
|
|
16846
|
+
);
|
|
16389
16847
|
}
|
|
16390
16848
|
return 0;
|
|
16391
16849
|
}
|
|
16392
16850
|
try {
|
|
16393
|
-
await fetchPullRequestRef(
|
|
16851
|
+
await fetchPullRequestRef(
|
|
16852
|
+
repository.repoRoot,
|
|
16853
|
+
options.number,
|
|
16854
|
+
prNumber,
|
|
16855
|
+
remoteRef
|
|
16856
|
+
);
|
|
16394
16857
|
} catch {
|
|
16395
16858
|
const message = `Failed to fetch PR #${prNumber} from origin`;
|
|
16396
16859
|
if (options.json) {
|
|
@@ -16399,35 +16862,57 @@ function createPrCommand(dependencies = {}) {
|
|
|
16399
16862
|
} else {
|
|
16400
16863
|
options.stderr(`${message}
|
|
16401
16864
|
`);
|
|
16402
|
-
options.stderr(
|
|
16403
|
-
`
|
|
16865
|
+
options.stderr(
|
|
16866
|
+
`Hint: Verify the remote is reachable: git fetch origin
|
|
16867
|
+
`
|
|
16868
|
+
);
|
|
16404
16869
|
}
|
|
16405
16870
|
return 1;
|
|
16406
16871
|
}
|
|
16407
16872
|
await mkdir7(dirname8(worktreePath), { recursive: true });
|
|
16408
|
-
const branchAlreadyExists = await localBranchExists2(
|
|
16873
|
+
const branchAlreadyExists = await localBranchExists2(
|
|
16874
|
+
repository.repoRoot,
|
|
16875
|
+
branchName
|
|
16876
|
+
);
|
|
16409
16877
|
const worktreeArgs = branchAlreadyExists ? ["worktree", "add", worktreePath, branchName] : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
|
|
16410
16878
|
await execFileAsync5("git", worktreeArgs, { cwd: repository.repoRoot });
|
|
16411
|
-
const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
|
|
16879
|
+
const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
|
|
16880
|
+
(p2) => typeof p2 === "string"
|
|
16881
|
+
) : [];
|
|
16412
16882
|
for (const pattern of syncPatterns) {
|
|
16413
16883
|
try {
|
|
16414
16884
|
await syncFiles(repository.repoRoot, worktreePath, [pattern]);
|
|
16415
16885
|
} catch (error) {
|
|
16416
|
-
options.stderr(
|
|
16417
|
-
`)
|
|
16886
|
+
options.stderr(
|
|
16887
|
+
`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
|
|
16888
|
+
`
|
|
16889
|
+
);
|
|
16418
16890
|
}
|
|
16419
16891
|
}
|
|
16420
|
-
await maybeRunInstallPrompt(
|
|
16892
|
+
await maybeRunInstallPrompt(
|
|
16893
|
+
worktreePath,
|
|
16894
|
+
repository.repoRoot,
|
|
16895
|
+
config,
|
|
16896
|
+
options.stderr,
|
|
16897
|
+
dependencies,
|
|
16898
|
+
!!options.json
|
|
16899
|
+
);
|
|
16421
16900
|
const hooks = extractHooks(config);
|
|
16422
16901
|
await runHook(
|
|
16423
16902
|
hooks.afterCreate,
|
|
16424
16903
|
worktreePath,
|
|
16425
|
-
{
|
|
16904
|
+
{
|
|
16905
|
+
branch: branchName,
|
|
16906
|
+
path: worktreePath,
|
|
16907
|
+
repo: basename7(repository.repoRoot)
|
|
16908
|
+
},
|
|
16426
16909
|
options.stderr
|
|
16427
16910
|
);
|
|
16428
16911
|
if (options.json) {
|
|
16429
|
-
options.stdout(
|
|
16430
|
-
|
|
16912
|
+
options.stdout(
|
|
16913
|
+
`${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}
|
|
16914
|
+
`
|
|
16915
|
+
);
|
|
16431
16916
|
} else {
|
|
16432
16917
|
await appendHistory(worktreePath, branchName);
|
|
16433
16918
|
await writeOutput2(worktreePath, options.stdout);
|
|
@@ -16463,9 +16948,16 @@ async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
|
|
|
16463
16948
|
throw new Error(`No pull request ref found for #${prNumber}`);
|
|
16464
16949
|
}
|
|
16465
16950
|
function listPullRequestSourceRefs(input, prNumber) {
|
|
16466
|
-
const allForges = [
|
|
16951
|
+
const allForges = [
|
|
16952
|
+
"github",
|
|
16953
|
+
"gitlab",
|
|
16954
|
+
"bitbucket"
|
|
16955
|
+
];
|
|
16467
16956
|
const preferredForge = detectPullRequestForge(input);
|
|
16468
|
-
const orderedForges = preferredForge === "unknown" ? allForges : [
|
|
16957
|
+
const orderedForges = preferredForge === "unknown" ? allForges : [
|
|
16958
|
+
preferredForge,
|
|
16959
|
+
...allForges.filter((forge) => forge !== preferredForge)
|
|
16960
|
+
];
|
|
16469
16961
|
return orderedForges.map((forge) => sourceRefForForge(forge, prNumber));
|
|
16470
16962
|
}
|
|
16471
16963
|
function detectPullRequestForge(input) {
|
|
@@ -16495,7 +16987,7 @@ async function writeOutput2(worktreePath, stdout) {
|
|
|
16495
16987
|
}
|
|
16496
16988
|
|
|
16497
16989
|
// src/remove.ts
|
|
16498
|
-
import { basename as
|
|
16990
|
+
import { basename as basename8 } from "node:path";
|
|
16499
16991
|
var REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
|
|
16500
16992
|
function createRemoveCommand(dependencies = {}) {
|
|
16501
16993
|
const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree2;
|
|
@@ -16503,7 +16995,9 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16503
16995
|
const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
|
|
16504
16996
|
const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
|
|
16505
16997
|
return async function runRemoveCommand2(options) {
|
|
16506
|
-
const { linkedWorktrees, repository } = await loadLinkedWorktrees(
|
|
16998
|
+
const { linkedWorktrees, repository } = await loadLinkedWorktrees(
|
|
16999
|
+
options.cwd
|
|
17000
|
+
);
|
|
16507
17001
|
if (linkedWorktrees.length === 0) {
|
|
16508
17002
|
emitError2(options, "No linked worktrees to finish");
|
|
16509
17003
|
return 1;
|
|
@@ -16513,8 +17007,10 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16513
17007
|
if (options.json) {
|
|
16514
17008
|
emitError2(options, message);
|
|
16515
17009
|
} else {
|
|
16516
|
-
options.stderr(
|
|
16517
|
-
`)
|
|
17010
|
+
options.stderr(
|
|
17011
|
+
`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
17012
|
+
`
|
|
17013
|
+
);
|
|
16518
17014
|
}
|
|
16519
17015
|
return 1;
|
|
16520
17016
|
}
|
|
@@ -16535,8 +17031,10 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16535
17031
|
if (options.json) {
|
|
16536
17032
|
emitError2(options, message);
|
|
16537
17033
|
} else {
|
|
16538
|
-
options.stderr(
|
|
16539
|
-
`)
|
|
17034
|
+
options.stderr(
|
|
17035
|
+
`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
|
|
17036
|
+
`
|
|
17037
|
+
);
|
|
16540
17038
|
}
|
|
16541
17039
|
return 1;
|
|
16542
17040
|
}
|
|
@@ -16546,8 +17044,10 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16546
17044
|
}
|
|
16547
17045
|
if (options.dryRun) {
|
|
16548
17046
|
if (options.json) {
|
|
16549
|
-
options.stdout(
|
|
16550
|
-
|
|
17047
|
+
options.stdout(
|
|
17048
|
+
`${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}
|
|
17049
|
+
`
|
|
17050
|
+
);
|
|
16551
17051
|
} else {
|
|
16552
17052
|
const desc = worktree.branch ? `branch: ${worktree.branch}` : "detached";
|
|
16553
17053
|
options.stdout(`Would remove worktree at ${worktree.path} (${desc})
|
|
@@ -16555,12 +17055,20 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16555
17055
|
}
|
|
16556
17056
|
return 0;
|
|
16557
17057
|
}
|
|
16558
|
-
const config = await loadEffectiveConfig(
|
|
17058
|
+
const config = await loadEffectiveConfig(
|
|
17059
|
+
repository.repoRoot,
|
|
17060
|
+
void 0,
|
|
17061
|
+
options.stderr
|
|
17062
|
+
);
|
|
16559
17063
|
const hooks = extractHooks(config);
|
|
16560
17064
|
await runHook(
|
|
16561
17065
|
hooks.beforeRemove,
|
|
16562
17066
|
worktree.path,
|
|
16563
|
-
{
|
|
17067
|
+
{
|
|
17068
|
+
branch: worktree.branch ?? void 0,
|
|
17069
|
+
path: worktree.path,
|
|
17070
|
+
repo: basename8(repository.repoRoot)
|
|
17071
|
+
},
|
|
16564
17072
|
options.stderr
|
|
16565
17073
|
);
|
|
16566
17074
|
try {
|
|
@@ -16576,7 +17084,10 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16576
17084
|
try {
|
|
16577
17085
|
await forceRemoveWorktree(repository.repoRoot, worktree.path);
|
|
16578
17086
|
} catch (forceError) {
|
|
16579
|
-
emitError2(
|
|
17087
|
+
emitError2(
|
|
17088
|
+
options,
|
|
17089
|
+
`Failed to remove worktree at ${worktree.path}: ${toMessage2(forceError)}`
|
|
17090
|
+
);
|
|
16580
17091
|
return 1;
|
|
16581
17092
|
}
|
|
16582
17093
|
}
|
|
@@ -16591,18 +17102,24 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16591
17102
|
try {
|
|
16592
17103
|
await forceDeleteBranch(repository.repoRoot, worktree.branch);
|
|
16593
17104
|
} catch (forceError) {
|
|
16594
|
-
options.stderr(
|
|
16595
|
-
`)
|
|
17105
|
+
options.stderr(
|
|
17106
|
+
`Failed to delete branch ${worktree.branch}: ${toMessage2(forceError)}
|
|
17107
|
+
`
|
|
17108
|
+
);
|
|
16596
17109
|
}
|
|
16597
17110
|
} else {
|
|
16598
|
-
options.stderr(
|
|
16599
|
-
`)
|
|
17111
|
+
options.stderr(
|
|
17112
|
+
`Branch ${worktree.branch} was not deleted (has unmerged commits)
|
|
17113
|
+
`
|
|
17114
|
+
);
|
|
16600
17115
|
}
|
|
16601
17116
|
}
|
|
16602
17117
|
}
|
|
16603
17118
|
if (options.json) {
|
|
16604
|
-
options.stdout(
|
|
16605
|
-
|
|
17119
|
+
options.stdout(
|
|
17120
|
+
`${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}
|
|
17121
|
+
`
|
|
17122
|
+
);
|
|
16606
17123
|
} else {
|
|
16607
17124
|
await writeOutput3(repository.repoRoot, options.stdout);
|
|
16608
17125
|
}
|
|
@@ -16651,7 +17168,11 @@ var ROOT_OUTPUT_FILE_ENV = "GJI_ROOT_OUTPUT_FILE";
|
|
|
16651
17168
|
async function runRootCommand(options) {
|
|
16652
17169
|
const repository = await detectRepository(options.cwd);
|
|
16653
17170
|
if (!options.print && process.env[ROOT_OUTPUT_FILE_ENV]) {
|
|
16654
|
-
await writeShellOutput(
|
|
17171
|
+
await writeShellOutput(
|
|
17172
|
+
ROOT_OUTPUT_FILE_ENV,
|
|
17173
|
+
repository.repoRoot,
|
|
17174
|
+
options.stdout
|
|
17175
|
+
);
|
|
16655
17176
|
return 0;
|
|
16656
17177
|
}
|
|
16657
17178
|
options.stdout(`${repository.repoRoot}
|
|
@@ -16667,18 +17188,31 @@ async function runStatusCommand(options) {
|
|
|
16667
17188
|
worktrees.map(async (worktree) => buildStatusRow(worktree))
|
|
16668
17189
|
);
|
|
16669
17190
|
if (options.json) {
|
|
16670
|
-
options.stdout(
|
|
16671
|
-
|
|
17191
|
+
options.stdout(
|
|
17192
|
+
`${JSON.stringify(formatStatusJson(repository.repoRoot, repository.currentRoot, rows), null, 2)}
|
|
17193
|
+
`
|
|
17194
|
+
);
|
|
16672
17195
|
return 0;
|
|
16673
17196
|
}
|
|
16674
|
-
options.stdout(
|
|
16675
|
-
|
|
17197
|
+
options.stdout(
|
|
17198
|
+
`${formatStatusOutput(repository.repoRoot, repository.currentRoot, rows)}
|
|
17199
|
+
`
|
|
17200
|
+
);
|
|
16676
17201
|
return 0;
|
|
16677
17202
|
}
|
|
16678
17203
|
function formatStatusOutput(repoRoot, currentRoot, rows) {
|
|
16679
|
-
const currentWidth = Math.max(
|
|
16680
|
-
|
|
16681
|
-
|
|
17204
|
+
const currentWidth = Math.max(
|
|
17205
|
+
"CURRENT".length,
|
|
17206
|
+
...rows.map((row) => row.current ? 1 : 0)
|
|
17207
|
+
);
|
|
17208
|
+
const branchWidth = Math.max(
|
|
17209
|
+
"BRANCH".length,
|
|
17210
|
+
...rows.map((row) => formatBranch(row.branch).length)
|
|
17211
|
+
);
|
|
17212
|
+
const statusWidth = Math.max(
|
|
17213
|
+
"STATUS".length,
|
|
17214
|
+
...rows.map((row) => row.status.length)
|
|
17215
|
+
);
|
|
16682
17216
|
const upstreamWidth = Math.max(
|
|
16683
17217
|
"UPSTREAM".length,
|
|
16684
17218
|
...rows.map((row) => formatUpstreamState2(row.upstream).length)
|
|
@@ -16714,7 +17248,9 @@ async function buildStatusRow(worktree) {
|
|
|
16714
17248
|
};
|
|
16715
17249
|
}
|
|
16716
17250
|
function sortWorktreesByPath(worktrees) {
|
|
16717
|
-
return [...worktrees].sort(
|
|
17251
|
+
return [...worktrees].sort(
|
|
17252
|
+
(left, right) => comparePaths(left.path, right.path)
|
|
17253
|
+
);
|
|
16718
17254
|
}
|
|
16719
17255
|
function formatBranch(branch) {
|
|
16720
17256
|
return branch ?? "(detached)";
|
|
@@ -16760,7 +17296,11 @@ function formatUpstreamState2(upstream) {
|
|
|
16760
17296
|
// src/sync.ts
|
|
16761
17297
|
async function runSyncCommand(options) {
|
|
16762
17298
|
const repository = await detectRepository(options.cwd);
|
|
16763
|
-
const config = await loadEffectiveConfig(
|
|
17299
|
+
const config = await loadEffectiveConfig(
|
|
17300
|
+
repository.repoRoot,
|
|
17301
|
+
void 0,
|
|
17302
|
+
options.stderr
|
|
17303
|
+
);
|
|
16764
17304
|
const worktrees = await listWorktrees(options.cwd);
|
|
16765
17305
|
const remote = resolveConfiguredString2(config.syncRemote) ?? "origin";
|
|
16766
17306
|
let defaultBranch;
|
|
@@ -16769,22 +17309,33 @@ async function runSyncCommand(options) {
|
|
|
16769
17309
|
} catch {
|
|
16770
17310
|
emitError3(options, `Unable to reach remote '${remote}'`);
|
|
16771
17311
|
if (!options.json) {
|
|
16772
|
-
options.stderr(
|
|
16773
|
-
`
|
|
17312
|
+
options.stderr(
|
|
17313
|
+
`Hint: Add the remote with: git remote add ${remote} <url>
|
|
17314
|
+
`
|
|
17315
|
+
);
|
|
16774
17316
|
}
|
|
16775
17317
|
return 1;
|
|
16776
17318
|
}
|
|
16777
17319
|
if (!defaultBranch) {
|
|
16778
17320
|
emitError3(options, "Unable to determine the default branch for sync.");
|
|
16779
17321
|
if (!options.json) {
|
|
16780
|
-
options.stderr(
|
|
16781
|
-
`
|
|
17322
|
+
options.stderr(
|
|
17323
|
+
`Hint: Add the remote with: git remote add ${remote} <url>
|
|
17324
|
+
`
|
|
17325
|
+
);
|
|
16782
17326
|
}
|
|
16783
17327
|
return 1;
|
|
16784
17328
|
}
|
|
16785
|
-
const targetWorktrees = selectTargetWorktrees(
|
|
17329
|
+
const targetWorktrees = selectTargetWorktrees(
|
|
17330
|
+
worktrees,
|
|
17331
|
+
repository.currentRoot,
|
|
17332
|
+
options.all
|
|
17333
|
+
);
|
|
16786
17334
|
if (targetWorktrees === "detached") {
|
|
16787
|
-
emitError3(
|
|
17335
|
+
emitError3(
|
|
17336
|
+
options,
|
|
17337
|
+
`Cannot sync detached worktree: ${repository.currentRoot}`
|
|
17338
|
+
);
|
|
16788
17339
|
return 1;
|
|
16789
17340
|
}
|
|
16790
17341
|
for (const worktree of targetWorktrees) {
|
|
@@ -16798,15 +17349,21 @@ async function runSyncCommand(options) {
|
|
|
16798
17349
|
} catch {
|
|
16799
17350
|
emitError3(options, `Failed to fetch from remote '${remote}'`);
|
|
16800
17351
|
if (!options.json) {
|
|
16801
|
-
options.stderr(
|
|
16802
|
-
`
|
|
17352
|
+
options.stderr(
|
|
17353
|
+
`Hint: Add the remote with: git remote add ${remote} <url>
|
|
17354
|
+
`
|
|
17355
|
+
);
|
|
16803
17356
|
}
|
|
16804
17357
|
return 1;
|
|
16805
17358
|
}
|
|
16806
17359
|
const updatedWorktrees = [];
|
|
16807
17360
|
for (const worktree of targetWorktrees) {
|
|
16808
17361
|
if (worktree.branch === defaultBranch) {
|
|
16809
|
-
await runGit(worktree.path, [
|
|
17362
|
+
await runGit(worktree.path, [
|
|
17363
|
+
"merge",
|
|
17364
|
+
"--ff-only",
|
|
17365
|
+
`${remote}/${defaultBranch}`
|
|
17366
|
+
]);
|
|
16810
17367
|
} else {
|
|
16811
17368
|
await runGit(worktree.path, ["rebase", `${remote}/${defaultBranch}`]);
|
|
16812
17369
|
}
|
|
@@ -16817,7 +17374,10 @@ async function runSyncCommand(options) {
|
|
|
16817
17374
|
}
|
|
16818
17375
|
}
|
|
16819
17376
|
if (options.json) {
|
|
16820
|
-
const updated = updatedWorktrees.map((w2) => ({
|
|
17377
|
+
const updated = updatedWorktrees.map((w2) => ({
|
|
17378
|
+
branch: w2.branch,
|
|
17379
|
+
path: w2.path
|
|
17380
|
+
}));
|
|
16821
17381
|
options.stdout(`${JSON.stringify({ updated }, null, 2)}
|
|
16822
17382
|
`);
|
|
16823
17383
|
}
|
|
@@ -16836,7 +17396,9 @@ function selectTargetWorktrees(worktrees, currentRoot, all) {
|
|
|
16836
17396
|
if (all) {
|
|
16837
17397
|
return worktrees.filter((worktree) => worktree.branch !== null).sort((left, right) => comparePaths(left.path, right.path));
|
|
16838
17398
|
}
|
|
16839
|
-
const currentWorktree = worktrees.find(
|
|
17399
|
+
const currentWorktree = worktrees.find(
|
|
17400
|
+
(worktree) => worktree.path === currentRoot
|
|
17401
|
+
);
|
|
16840
17402
|
if (!currentWorktree) {
|
|
16841
17403
|
return [];
|
|
16842
17404
|
}
|
|
@@ -16849,8 +17411,153 @@ function resolveConfiguredString2(value) {
|
|
|
16849
17411
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
16850
17412
|
}
|
|
16851
17413
|
|
|
17414
|
+
// src/sync-files-command.ts
|
|
17415
|
+
import { homedir as homedir6 } from "node:os";
|
|
17416
|
+
import { join as join9 } from "node:path";
|
|
17417
|
+
async function runSyncFilesCommand(options) {
|
|
17418
|
+
const repository = await detectRepository(options.cwd);
|
|
17419
|
+
const home = options.home ?? homedir6();
|
|
17420
|
+
const loaded = await loadGlobalConfig(home);
|
|
17421
|
+
const repoEntry = findRepoConfigEntry(
|
|
17422
|
+
loaded.config,
|
|
17423
|
+
repository.repoRoot,
|
|
17424
|
+
home
|
|
17425
|
+
);
|
|
17426
|
+
const repoConfig = repoEntry?.config ?? {};
|
|
17427
|
+
switch (options.action) {
|
|
17428
|
+
case void 0:
|
|
17429
|
+
case "list": {
|
|
17430
|
+
writeSyncFiles(options.stdout, readSyncFiles(repoConfig), !!options.json);
|
|
17431
|
+
return 0;
|
|
17432
|
+
}
|
|
17433
|
+
case "add": {
|
|
17434
|
+
const paths = validatePaths(options.paths ?? [], options);
|
|
17435
|
+
if (!paths) return 1;
|
|
17436
|
+
const nextFiles = mergeSyncFiles(readSyncFiles(repoConfig), paths);
|
|
17437
|
+
await saveRepoSyncFiles(
|
|
17438
|
+
loaded.config,
|
|
17439
|
+
repoEntry?.key ?? repository.repoRoot,
|
|
17440
|
+
nextFiles,
|
|
17441
|
+
home
|
|
17442
|
+
);
|
|
17443
|
+
writeSyncFiles(options.stdout, nextFiles, !!options.json);
|
|
17444
|
+
return 0;
|
|
17445
|
+
}
|
|
17446
|
+
case "remove": {
|
|
17447
|
+
const paths = validatePaths(options.paths ?? [], options);
|
|
17448
|
+
if (!paths) return 1;
|
|
17449
|
+
const existingFiles = readSyncFiles(repoConfig);
|
|
17450
|
+
const nextFiles = removeSyncFiles(existingFiles, paths);
|
|
17451
|
+
if (repoEntry && nextFiles.length !== existingFiles.length) {
|
|
17452
|
+
await saveRepoSyncFiles(loaded.config, repoEntry.key, nextFiles, home);
|
|
17453
|
+
}
|
|
17454
|
+
writeSyncFiles(options.stdout, nextFiles, !!options.json);
|
|
17455
|
+
return 0;
|
|
17456
|
+
}
|
|
17457
|
+
}
|
|
17458
|
+
writeError(options, `unknown action: ${options.action}`);
|
|
17459
|
+
return 1;
|
|
17460
|
+
}
|
|
17461
|
+
function findRepoConfigEntry(config, repoRoot, home) {
|
|
17462
|
+
const repos = config.repos;
|
|
17463
|
+
if (!isPlainObject3(repos)) return null;
|
|
17464
|
+
for (const [key, value] of Object.entries(repos)) {
|
|
17465
|
+
if (expandTilde2(key, home) === repoRoot && isPlainObject3(value)) {
|
|
17466
|
+
return { config: value, key };
|
|
17467
|
+
}
|
|
17468
|
+
}
|
|
17469
|
+
return null;
|
|
17470
|
+
}
|
|
17471
|
+
function readSyncFiles(config) {
|
|
17472
|
+
const syncFiles2 = config.syncFiles;
|
|
17473
|
+
if (!Array.isArray(syncFiles2)) return [];
|
|
17474
|
+
return syncFiles2.filter((item) => typeof item === "string");
|
|
17475
|
+
}
|
|
17476
|
+
function writeSyncFiles(stdout, files, json) {
|
|
17477
|
+
if (json) {
|
|
17478
|
+
stdout(`${JSON.stringify(files, null, 2)}
|
|
17479
|
+
`);
|
|
17480
|
+
return;
|
|
17481
|
+
}
|
|
17482
|
+
if (files.length === 0) {
|
|
17483
|
+
stdout("No sync files configured for this repo.\n");
|
|
17484
|
+
return;
|
|
17485
|
+
}
|
|
17486
|
+
stdout(`${files.join("\n")}
|
|
17487
|
+
`);
|
|
17488
|
+
}
|
|
17489
|
+
function validatePaths(paths, options) {
|
|
17490
|
+
if (paths.length === 0) {
|
|
17491
|
+
writeError(options, "at least one path is required");
|
|
17492
|
+
return null;
|
|
17493
|
+
}
|
|
17494
|
+
const validatedPaths = [];
|
|
17495
|
+
for (const path9 of paths) {
|
|
17496
|
+
try {
|
|
17497
|
+
validatedPaths.push(validateSyncFilePattern(path9));
|
|
17498
|
+
} catch (error) {
|
|
17499
|
+
writeError(
|
|
17500
|
+
options,
|
|
17501
|
+
error instanceof Error ? error.message : String(error)
|
|
17502
|
+
);
|
|
17503
|
+
return null;
|
|
17504
|
+
}
|
|
17505
|
+
}
|
|
17506
|
+
return validatedPaths;
|
|
17507
|
+
}
|
|
17508
|
+
function mergeSyncFiles(existing, additions) {
|
|
17509
|
+
const nextFiles = [...existing];
|
|
17510
|
+
for (const path9 of additions) {
|
|
17511
|
+
if (!nextFiles.includes(path9)) {
|
|
17512
|
+
nextFiles.push(path9);
|
|
17513
|
+
}
|
|
17514
|
+
}
|
|
17515
|
+
return nextFiles;
|
|
17516
|
+
}
|
|
17517
|
+
function removeSyncFiles(existing, removals) {
|
|
17518
|
+
const removalSet = new Set(removals);
|
|
17519
|
+
return existing.filter((path9) => !removalSet.has(path9));
|
|
17520
|
+
}
|
|
17521
|
+
async function saveRepoSyncFiles(config, repoKey, syncFiles2, home) {
|
|
17522
|
+
const repos = isPlainObject3(config.repos) ? { ...config.repos } : {};
|
|
17523
|
+
const repoConfig = isPlainObject3(repos[repoKey]) ? repos[repoKey] : {};
|
|
17524
|
+
const nextRepoConfig = { ...repoConfig };
|
|
17525
|
+
if (syncFiles2.length > 0) {
|
|
17526
|
+
nextRepoConfig.syncFiles = syncFiles2;
|
|
17527
|
+
} else {
|
|
17528
|
+
delete nextRepoConfig.syncFiles;
|
|
17529
|
+
}
|
|
17530
|
+
if (Object.keys(nextRepoConfig).length > 0) {
|
|
17531
|
+
repos[repoKey] = nextRepoConfig;
|
|
17532
|
+
} else {
|
|
17533
|
+
delete repos[repoKey];
|
|
17534
|
+
}
|
|
17535
|
+
await saveGlobalConfig({ ...config, repos }, home);
|
|
17536
|
+
}
|
|
17537
|
+
function isPlainObject3(value) {
|
|
17538
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17539
|
+
}
|
|
17540
|
+
function writeError(options, message) {
|
|
17541
|
+
if (options.json) {
|
|
17542
|
+
options.stderr(`${JSON.stringify({ error: message }, null, 2)}
|
|
17543
|
+
`);
|
|
17544
|
+
return;
|
|
17545
|
+
}
|
|
17546
|
+
options.stderr(`gji sync-files: ${message}
|
|
17547
|
+
`);
|
|
17548
|
+
}
|
|
17549
|
+
function expandTilde2(value, home) {
|
|
17550
|
+
if (value === "~") return home;
|
|
17551
|
+
if (value.startsWith("~/")) return join9(home, value.slice(2));
|
|
17552
|
+
return value;
|
|
17553
|
+
}
|
|
17554
|
+
|
|
16852
17555
|
// src/trigger-hook.ts
|
|
16853
|
-
var VALID_HOOKS = [
|
|
17556
|
+
var VALID_HOOKS = [
|
|
17557
|
+
"afterCreate",
|
|
17558
|
+
"afterEnter",
|
|
17559
|
+
"beforeRemove"
|
|
17560
|
+
];
|
|
16854
17561
|
function isValidHook(hook) {
|
|
16855
17562
|
return VALID_HOOKS.includes(hook);
|
|
16856
17563
|
}
|
|
@@ -16864,10 +17571,16 @@ async function runTriggerHookCommand(options) {
|
|
|
16864
17571
|
}
|
|
16865
17572
|
const hookName = options.hook;
|
|
16866
17573
|
const repository = await detectRepository(options.cwd);
|
|
16867
|
-
const config = await loadEffectiveConfig(
|
|
17574
|
+
const config = await loadEffectiveConfig(
|
|
17575
|
+
repository.repoRoot,
|
|
17576
|
+
void 0,
|
|
17577
|
+
options.stderr
|
|
17578
|
+
);
|
|
16868
17579
|
const hooks = extractHooks(config);
|
|
16869
17580
|
const worktrees = await listWorktrees(options.cwd);
|
|
16870
|
-
const currentWorktree = worktrees.find(
|
|
17581
|
+
const currentWorktree = worktrees.find(
|
|
17582
|
+
(w2) => w2.path === repository.currentRoot
|
|
17583
|
+
);
|
|
16871
17584
|
await runHook(
|
|
16872
17585
|
hooks[hookName],
|
|
16873
17586
|
repository.currentRoot,
|
|
@@ -16947,46 +17660,117 @@ function maybeRegisterCurrentRepo(cwd) {
|
|
|
16947
17660
|
detectRepository(cwd).then(({ repoRoot }) => registerRepo(repoRoot)).catch(() => void 0);
|
|
16948
17661
|
}
|
|
16949
17662
|
function registerCommands(program2) {
|
|
16950
|
-
program2.command("new [branch]").description("create a new branch or detached linked worktree").option(
|
|
17663
|
+
program2.command("new [branch]").description("create a new branch or detached linked worktree").option(
|
|
17664
|
+
"-f, --force",
|
|
17665
|
+
"remove and recreate the worktree if the target path already exists"
|
|
17666
|
+
).option("--detached", "create a detached worktree without a branch").option("--open", "open the new worktree in an editor after creation").option(
|
|
17667
|
+
"--editor <cli>",
|
|
17668
|
+
"editor CLI to use with --open (code, cursor, zed, \u2026)"
|
|
17669
|
+
).option(
|
|
17670
|
+
"--dry-run",
|
|
17671
|
+
"show what would be created without executing any git commands or writing files"
|
|
17672
|
+
).option(
|
|
17673
|
+
"--json",
|
|
17674
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17675
|
+
).action(notImplemented("new"));
|
|
16951
17676
|
program2.command("init [shell]").description("print or install shell integration").option("--write", "write the integration to the shell config file").action(notImplemented("init"));
|
|
16952
17677
|
program2.command("completion [shell]").description("print shell completion definitions").action(notImplemented("completion"));
|
|
16953
|
-
program2.command("pr <ref>").description(
|
|
16954
|
-
|
|
17678
|
+
program2.command("pr <ref>").description(
|
|
17679
|
+
"fetch a pull request by number, #number, or URL into a linked worktree"
|
|
17680
|
+
).option(
|
|
17681
|
+
"--dry-run",
|
|
17682
|
+
"show what would be created without executing any git commands or writing files"
|
|
17683
|
+
).option(
|
|
17684
|
+
"--json",
|
|
17685
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17686
|
+
).action(notImplemented("pr"));
|
|
17687
|
+
program2.command("back [n]").description(
|
|
17688
|
+
"navigate to the previously visited worktree, optionally N steps back"
|
|
17689
|
+
).option("--print", "print the resolved worktree path explicitly").action(notImplemented("back"));
|
|
16955
17690
|
program2.command("history").description("show navigation history").option("--json", "print history as JSON").action(notImplemented("history"));
|
|
16956
|
-
program2.command("open [branch]").description("open the worktree in an editor").option(
|
|
17691
|
+
program2.command("open [branch]").description("open the worktree in an editor").option(
|
|
17692
|
+
"--editor <cli>",
|
|
17693
|
+
"editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)"
|
|
17694
|
+
).option("--save", "save the chosen editor to global config").option(
|
|
17695
|
+
"--workspace",
|
|
17696
|
+
"generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)"
|
|
17697
|
+
).action(notImplemented("open"));
|
|
16957
17698
|
program2.command("go [branch]").alias("jump").description("print or select a worktree path").option("--print", "print the resolved worktree path explicitly").action(notImplemented("go"));
|
|
16958
17699
|
program2.command("root").description("print the main repository root path").option("--print", "print the resolved repository root path explicitly").action(notImplemented("root"));
|
|
16959
17700
|
program2.command("status").description("summarize repository and worktree health").option("--json", "print repository and worktree health as JSON").action(notImplemented("status"));
|
|
16960
|
-
program2.command("sync").description("fetch and update one or all worktrees").option("--all", "sync every worktree in the repository").option(
|
|
17701
|
+
program2.command("sync").description("fetch and update one or all worktrees").option("--all", "sync every worktree in the repository").option(
|
|
17702
|
+
"--json",
|
|
17703
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17704
|
+
).action(notImplemented("sync"));
|
|
17705
|
+
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"));
|
|
17706
|
+
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"));
|
|
17707
|
+
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"));
|
|
17708
|
+
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"));
|
|
16961
17709
|
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"));
|
|
16962
|
-
program2.command("clean").description("interactively prune linked worktrees").option(
|
|
16963
|
-
|
|
16964
|
-
|
|
16965
|
-
|
|
17710
|
+
program2.command("clean").description("interactively prune linked worktrees").option(
|
|
17711
|
+
"-f, --force",
|
|
17712
|
+
"bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches"
|
|
17713
|
+
).option(
|
|
17714
|
+
"--stale",
|
|
17715
|
+
"only target clean worktrees whose upstream is gone and branch is merged into the default branch"
|
|
17716
|
+
).option("--dry-run", "show what would be deleted without removing anything").option(
|
|
17717
|
+
"--json",
|
|
17718
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17719
|
+
).action(notImplemented("clean"));
|
|
17720
|
+
program2.command("remove [branch]").alias("rm").description("remove a linked worktree and delete its branch when present").option(
|
|
17721
|
+
"-f, --force",
|
|
17722
|
+
"bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch"
|
|
17723
|
+
).option("--dry-run", "show what would be deleted without removing anything").option(
|
|
17724
|
+
"--json",
|
|
17725
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17726
|
+
).action(notImplemented("remove"));
|
|
17727
|
+
program2.command("trigger-hook <hook>").description(
|
|
17728
|
+
"run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree"
|
|
17729
|
+
).action(notImplemented("trigger-hook"));
|
|
17730
|
+
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(
|
|
17731
|
+
"--print",
|
|
17732
|
+
"print the resolved worktree path without changing directory"
|
|
17733
|
+
).option(
|
|
17734
|
+
"--json",
|
|
17735
|
+
"emit JSON on success or error instead of human-readable output"
|
|
17736
|
+
).action(notImplemented("warp"));
|
|
16966
17737
|
const configCommand = program2.command("config").description("manage global config defaults").action(notImplemented("config"));
|
|
16967
17738
|
configCommand.command("get [key]").description("print the global config or a single key").action(notImplemented("config get"));
|
|
16968
17739
|
configCommand.command("set <key> <value>").description("set a global config value").action(notImplemented("config set"));
|
|
16969
17740
|
configCommand.command("unset <key>").description("remove a global config value").action(notImplemented("config unset"));
|
|
16970
17741
|
}
|
|
16971
17742
|
function attachCommandActions(program2, options) {
|
|
16972
|
-
program2.commands.find((command) => command.name() === "new")?.action(
|
|
16973
|
-
|
|
16974
|
-
|
|
16975
|
-
|
|
17743
|
+
program2.commands.find((command) => command.name() === "new")?.action(
|
|
17744
|
+
async (branch, commandOptions) => {
|
|
17745
|
+
const exitCode = await runNewCommand({
|
|
17746
|
+
...options,
|
|
17747
|
+
branch,
|
|
17748
|
+
detached: commandOptions.detached,
|
|
17749
|
+
dryRun: commandOptions.dryRun,
|
|
17750
|
+
editor: commandOptions.editor,
|
|
17751
|
+
force: commandOptions.force,
|
|
17752
|
+
json: commandOptions.json,
|
|
17753
|
+
open: commandOptions.open
|
|
17754
|
+
});
|
|
17755
|
+
if (exitCode !== 0) {
|
|
17756
|
+
throw commanderExit(exitCode);
|
|
17757
|
+
}
|
|
16976
17758
|
}
|
|
16977
|
-
|
|
16978
|
-
program2.commands.find((command) => command.name() === "init")?.action(
|
|
16979
|
-
|
|
16980
|
-
|
|
16981
|
-
|
|
16982
|
-
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
16986
|
-
|
|
16987
|
-
|
|
17759
|
+
);
|
|
17760
|
+
program2.commands.find((command) => command.name() === "init")?.action(
|
|
17761
|
+
async (shell, commandOptions) => {
|
|
17762
|
+
const exitCode = await runInitCommand({
|
|
17763
|
+
cwd: options.cwd,
|
|
17764
|
+
shell,
|
|
17765
|
+
stderr: options.stderr,
|
|
17766
|
+
stdout: options.stdout,
|
|
17767
|
+
write: commandOptions.write
|
|
17768
|
+
});
|
|
17769
|
+
if (exitCode !== 0) {
|
|
17770
|
+
throw commanderExit(exitCode);
|
|
17771
|
+
}
|
|
16988
17772
|
}
|
|
16989
|
-
|
|
17773
|
+
);
|
|
16990
17774
|
program2.commands.find((command) => command.name() === "completion")?.action(async (shell) => {
|
|
16991
17775
|
const exitCode = await runCompletionCommand({
|
|
16992
17776
|
shell,
|
|
@@ -16997,32 +17781,93 @@ function attachCommandActions(program2, options) {
|
|
|
16997
17781
|
throw commanderExit(exitCode);
|
|
16998
17782
|
}
|
|
16999
17783
|
});
|
|
17000
|
-
program2.commands.find((command) => command.name() === "pr")?.action(
|
|
17001
|
-
|
|
17784
|
+
program2.commands.find((command) => command.name() === "pr")?.action(
|
|
17785
|
+
async (number, commandOptions) => {
|
|
17786
|
+
const exitCode = await runPrCommand({
|
|
17787
|
+
cwd: options.cwd,
|
|
17788
|
+
dryRun: commandOptions.dryRun,
|
|
17789
|
+
json: commandOptions.json,
|
|
17790
|
+
number,
|
|
17791
|
+
stderr: options.stderr,
|
|
17792
|
+
stdout: options.stdout
|
|
17793
|
+
});
|
|
17794
|
+
if (exitCode !== 0) {
|
|
17795
|
+
throw commanderExit(exitCode);
|
|
17796
|
+
}
|
|
17797
|
+
}
|
|
17798
|
+
);
|
|
17799
|
+
program2.commands.find((command) => command.name() === "back")?.action(
|
|
17800
|
+
async (n, commandOptions) => {
|
|
17801
|
+
if (n !== void 0 && !/^\d+$/.test(n)) {
|
|
17802
|
+
options.stderr(`gji back: invalid step count: ${n}
|
|
17803
|
+
`);
|
|
17804
|
+
throw commanderExit(1);
|
|
17805
|
+
}
|
|
17806
|
+
const steps = n !== void 0 ? parseInt(n, 10) : void 0;
|
|
17807
|
+
const exitCode = await runBackCommand({
|
|
17808
|
+
cwd: options.cwd,
|
|
17809
|
+
n: steps,
|
|
17810
|
+
print: commandOptions.print,
|
|
17811
|
+
stderr: options.stderr,
|
|
17812
|
+
stdout: options.stdout
|
|
17813
|
+
});
|
|
17814
|
+
if (exitCode !== 0) {
|
|
17815
|
+
throw commanderExit(exitCode);
|
|
17816
|
+
}
|
|
17817
|
+
}
|
|
17818
|
+
);
|
|
17819
|
+
program2.commands.find((command) => command.name() === "history")?.action(async (commandOptions) => {
|
|
17820
|
+
const exitCode = await runHistoryCommand({
|
|
17821
|
+
cwd: options.cwd,
|
|
17822
|
+
json: commandOptions.json,
|
|
17823
|
+
stdout: options.stdout
|
|
17824
|
+
});
|
|
17002
17825
|
if (exitCode !== 0) {
|
|
17003
17826
|
throw commanderExit(exitCode);
|
|
17004
17827
|
}
|
|
17005
17828
|
});
|
|
17006
|
-
program2.commands.find((command) => command.name() === "
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17829
|
+
program2.commands.find((command) => command.name() === "open")?.action(
|
|
17830
|
+
async (branch, commandOptions) => {
|
|
17831
|
+
const exitCode = await runOpenCommand({
|
|
17832
|
+
branch,
|
|
17833
|
+
cwd: options.cwd,
|
|
17834
|
+
editor: commandOptions.editor,
|
|
17835
|
+
save: commandOptions.save,
|
|
17836
|
+
stderr: options.stderr,
|
|
17837
|
+
stdout: options.stdout,
|
|
17838
|
+
workspace: commandOptions.workspace
|
|
17839
|
+
});
|
|
17840
|
+
if (exitCode !== 0) {
|
|
17841
|
+
throw commanderExit(exitCode);
|
|
17842
|
+
}
|
|
17843
|
+
}
|
|
17844
|
+
);
|
|
17845
|
+
program2.commands.find((command) => command.name() === "go")?.action(
|
|
17846
|
+
async (branch, commandOptions) => {
|
|
17847
|
+
const exitCode = await runGoCommand({
|
|
17848
|
+
branch,
|
|
17849
|
+
cwd: options.cwd,
|
|
17850
|
+
print: commandOptions.print,
|
|
17851
|
+
stderr: options.stderr,
|
|
17852
|
+
stdout: options.stdout
|
|
17853
|
+
});
|
|
17854
|
+
if (exitCode !== 0) {
|
|
17855
|
+
throw commanderExit(exitCode);
|
|
17856
|
+
}
|
|
17011
17857
|
}
|
|
17012
|
-
|
|
17013
|
-
|
|
17858
|
+
);
|
|
17859
|
+
program2.commands.find((command) => command.name() === "root")?.action(async (commandOptions) => {
|
|
17860
|
+
const exitCode = await runRootCommand({
|
|
17014
17861
|
cwd: options.cwd,
|
|
17015
|
-
n: steps,
|
|
17016
17862
|
print: commandOptions.print,
|
|
17017
|
-
stderr: options.stderr,
|
|
17018
17863
|
stdout: options.stdout
|
|
17019
17864
|
});
|
|
17020
17865
|
if (exitCode !== 0) {
|
|
17021
17866
|
throw commanderExit(exitCode);
|
|
17022
17867
|
}
|
|
17023
17868
|
});
|
|
17024
|
-
program2.commands.find((command) => command.name() === "
|
|
17025
|
-
const exitCode = await
|
|
17869
|
+
program2.commands.find((command) => command.name() === "status")?.action(async (commandOptions) => {
|
|
17870
|
+
const exitCode = await runStatusCommand({
|
|
17026
17871
|
cwd: options.cwd,
|
|
17027
17872
|
json: commandOptions.json,
|
|
17028
17873
|
stdout: options.stdout
|
|
@@ -17031,25 +17876,26 @@ function attachCommandActions(program2, options) {
|
|
|
17031
17876
|
throw commanderExit(exitCode);
|
|
17032
17877
|
}
|
|
17033
17878
|
});
|
|
17034
|
-
program2.commands.find((command) => command.name() === "
|
|
17035
|
-
const exitCode = await
|
|
17036
|
-
|
|
17879
|
+
program2.commands.find((command) => command.name() === "sync")?.action(async (commandOptions) => {
|
|
17880
|
+
const exitCode = await runSyncCommand({
|
|
17881
|
+
all: commandOptions.all,
|
|
17037
17882
|
cwd: options.cwd,
|
|
17038
|
-
|
|
17039
|
-
save: commandOptions.save,
|
|
17883
|
+
json: commandOptions.json,
|
|
17040
17884
|
stderr: options.stderr,
|
|
17041
|
-
stdout: options.stdout
|
|
17042
|
-
workspace: commandOptions.workspace
|
|
17885
|
+
stdout: options.stdout
|
|
17043
17886
|
});
|
|
17044
17887
|
if (exitCode !== 0) {
|
|
17045
17888
|
throw commanderExit(exitCode);
|
|
17046
17889
|
}
|
|
17047
17890
|
});
|
|
17048
|
-
program2.commands.find(
|
|
17049
|
-
|
|
17050
|
-
|
|
17891
|
+
const syncFilesCommand = program2.commands.find(
|
|
17892
|
+
(command) => command.name() === "sync-files"
|
|
17893
|
+
);
|
|
17894
|
+
syncFilesCommand?.action(async (commandOptions) => {
|
|
17895
|
+
const exitCode = await runSyncFilesCommand({
|
|
17896
|
+
action: "list",
|
|
17051
17897
|
cwd: options.cwd,
|
|
17052
|
-
|
|
17898
|
+
json: commandOptions.json,
|
|
17053
17899
|
stderr: options.stderr,
|
|
17054
17900
|
stdout: options.stdout
|
|
17055
17901
|
});
|
|
@@ -17057,38 +17903,45 @@ function attachCommandActions(program2, options) {
|
|
|
17057
17903
|
throw commanderExit(exitCode);
|
|
17058
17904
|
}
|
|
17059
17905
|
});
|
|
17060
|
-
|
|
17061
|
-
const exitCode = await
|
|
17906
|
+
syncFilesCommand?.commands.find((command) => command.name() === "list")?.action(async (commandOptions) => {
|
|
17907
|
+
const exitCode = await runSyncFilesCommand({
|
|
17908
|
+
action: "list",
|
|
17062
17909
|
cwd: options.cwd,
|
|
17063
|
-
|
|
17910
|
+
json: commandOptions.json || syncFilesCommand?.opts().json,
|
|
17911
|
+
stderr: options.stderr,
|
|
17064
17912
|
stdout: options.stdout
|
|
17065
17913
|
});
|
|
17066
17914
|
if (exitCode !== 0) {
|
|
17067
17915
|
throw commanderExit(exitCode);
|
|
17068
17916
|
}
|
|
17069
17917
|
});
|
|
17070
|
-
|
|
17071
|
-
const exitCode = await
|
|
17918
|
+
syncFilesCommand?.commands.find((command) => command.name() === "add")?.action(async (paths, commandOptions) => {
|
|
17919
|
+
const exitCode = await runSyncFilesCommand({
|
|
17920
|
+
action: "add",
|
|
17072
17921
|
cwd: options.cwd,
|
|
17073
|
-
json: commandOptions.json,
|
|
17922
|
+
json: commandOptions.json || syncFilesCommand?.opts().json,
|
|
17923
|
+
paths,
|
|
17924
|
+
stderr: options.stderr,
|
|
17074
17925
|
stdout: options.stdout
|
|
17075
17926
|
});
|
|
17076
17927
|
if (exitCode !== 0) {
|
|
17077
17928
|
throw commanderExit(exitCode);
|
|
17078
17929
|
}
|
|
17079
17930
|
});
|
|
17080
|
-
|
|
17081
|
-
const exitCode = await
|
|
17082
|
-
|
|
17931
|
+
const runSyncFilesRemoveCommand = async (paths, commandOptions) => {
|
|
17932
|
+
const exitCode = await runSyncFilesCommand({
|
|
17933
|
+
action: "remove",
|
|
17083
17934
|
cwd: options.cwd,
|
|
17084
|
-
json: commandOptions.json,
|
|
17935
|
+
json: commandOptions.json || syncFilesCommand?.opts().json,
|
|
17936
|
+
paths,
|
|
17085
17937
|
stderr: options.stderr,
|
|
17086
17938
|
stdout: options.stdout
|
|
17087
17939
|
});
|
|
17088
17940
|
if (exitCode !== 0) {
|
|
17089
17941
|
throw commanderExit(exitCode);
|
|
17090
17942
|
}
|
|
17091
|
-
}
|
|
17943
|
+
};
|
|
17944
|
+
syncFilesCommand?.commands.find((command) => command.name() === "remove")?.action(runSyncFilesRemoveCommand);
|
|
17092
17945
|
program2.commands.find((command) => command.name() === "ls")?.action(async (commandOptions) => {
|
|
17093
17946
|
const exitCode = await runLsCommand({
|
|
17094
17947
|
compact: commandOptions.compact,
|
|
@@ -17100,20 +17953,22 @@ function attachCommandActions(program2, options) {
|
|
|
17100
17953
|
throw commanderExit(exitCode);
|
|
17101
17954
|
}
|
|
17102
17955
|
});
|
|
17103
|
-
program2.commands.find((command) => command.name() === "clean")?.action(
|
|
17104
|
-
|
|
17105
|
-
|
|
17106
|
-
|
|
17107
|
-
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17956
|
+
program2.commands.find((command) => command.name() === "clean")?.action(
|
|
17957
|
+
async (commandOptions) => {
|
|
17958
|
+
const exitCode = await runCleanCommand({
|
|
17959
|
+
cwd: options.cwd,
|
|
17960
|
+
dryRun: commandOptions.dryRun,
|
|
17961
|
+
force: commandOptions.force,
|
|
17962
|
+
json: commandOptions.json,
|
|
17963
|
+
stale: commandOptions.stale,
|
|
17964
|
+
stderr: options.stderr,
|
|
17965
|
+
stdout: options.stdout
|
|
17966
|
+
});
|
|
17967
|
+
if (exitCode !== 0) {
|
|
17968
|
+
throw commanderExit(exitCode);
|
|
17969
|
+
}
|
|
17115
17970
|
}
|
|
17116
|
-
|
|
17971
|
+
);
|
|
17117
17972
|
const runRemovalCommand = async (branch, commandOptions = {}) => {
|
|
17118
17973
|
const exitCode = await runRemoveCommand({
|
|
17119
17974
|
branch,
|
|
@@ -17139,23 +17994,27 @@ function attachCommandActions(program2, options) {
|
|
|
17139
17994
|
throw commanderExit(exitCode);
|
|
17140
17995
|
}
|
|
17141
17996
|
});
|
|
17142
|
-
program2.commands.find((command) => command.name() === "warp")?.action(
|
|
17143
|
-
|
|
17144
|
-
|
|
17145
|
-
|
|
17146
|
-
|
|
17147
|
-
|
|
17148
|
-
|
|
17149
|
-
|
|
17150
|
-
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
|
|
17154
|
-
|
|
17155
|
-
|
|
17997
|
+
program2.commands.find((command) => command.name() === "warp")?.action(
|
|
17998
|
+
async (branch, commandOptions) => {
|
|
17999
|
+
const newFlag = commandOptions.new;
|
|
18000
|
+
const newWorktree = newFlag !== void 0 && newFlag !== false;
|
|
18001
|
+
const newBranch = typeof newFlag === "string" ? newFlag : void 0;
|
|
18002
|
+
const exitCode = await runWarpCommand({
|
|
18003
|
+
branch: newWorktree ? newBranch ?? branch : branch,
|
|
18004
|
+
cwd: options.cwd,
|
|
18005
|
+
json: commandOptions.json,
|
|
18006
|
+
newWorktree,
|
|
18007
|
+
stderr: options.stderr,
|
|
18008
|
+
stdout: options.stdout
|
|
18009
|
+
});
|
|
18010
|
+
if (exitCode !== 0) {
|
|
18011
|
+
throw commanderExit(exitCode);
|
|
18012
|
+
}
|
|
17156
18013
|
}
|
|
17157
|
-
|
|
17158
|
-
const configCommand = program2.commands.find(
|
|
18014
|
+
);
|
|
18015
|
+
const configCommand = program2.commands.find(
|
|
18016
|
+
(command) => command.name() === "config"
|
|
18017
|
+
);
|
|
17159
18018
|
configCommand?.action(async () => {
|
|
17160
18019
|
const exitCode = await runConfigCommand({
|
|
17161
18020
|
cwd: options.cwd,
|
|
@@ -17237,7 +18096,7 @@ async function main() {
|
|
|
17237
18096
|
}
|
|
17238
18097
|
async function warnIfMissingShellIntegration() {
|
|
17239
18098
|
try {
|
|
17240
|
-
const { config } = await loadGlobalConfig(
|
|
18099
|
+
const { config } = await loadGlobalConfig(homedir7());
|
|
17241
18100
|
if (!config.shellIntegration) {
|
|
17242
18101
|
const shellBin = (process.env.SHELL ?? "").split("/").at(-1);
|
|
17243
18102
|
const shellArg = shellBin && ["bash", "zsh", "fish"].includes(shellBin) ? ` ${shellBin}` : "";
|