sync-worktrees 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +257 -114
- package/dist/index.js.map +4 -4
- package/dist/mcp-server.js +198 -88
- package/dist/mcp-server.js.map +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import * as
|
|
4
|
+
import * as path13 from "path";
|
|
5
5
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
6
6
|
import * as cron3 from "node-cron";
|
|
7
7
|
import pLimit3 from "p-limit";
|
|
@@ -25,7 +25,8 @@ var GIT_CONSTANTS = {
|
|
|
25
25
|
REMOTES: "refs/remotes/origin",
|
|
26
26
|
REMOTES_ORIGIN: "refs/remotes/origin/*"
|
|
27
27
|
},
|
|
28
|
-
FETCH_CONFIG: "+refs/heads/*:refs/remotes/origin/*"
|
|
28
|
+
FETCH_CONFIG: "+refs/heads/*:refs/remotes/origin/*",
|
|
29
|
+
PROGRESS_BUCKET_PERCENT: 25
|
|
29
30
|
};
|
|
30
31
|
var GIT_OPERATIONS = {
|
|
31
32
|
MERGE_HEAD: "MERGE_HEAD",
|
|
@@ -219,9 +220,9 @@ var WorktreeError = class extends SyncWorktreesError {
|
|
|
219
220
|
}
|
|
220
221
|
};
|
|
221
222
|
var WorktreeNotCleanError = class extends WorktreeError {
|
|
222
|
-
constructor(
|
|
223
|
-
super(`Worktree at '${
|
|
224
|
-
this.path =
|
|
223
|
+
constructor(path14, reasons) {
|
|
224
|
+
super(`Worktree at '${path14}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
|
|
225
|
+
this.path = path14;
|
|
225
226
|
this.reasons = reasons;
|
|
226
227
|
}
|
|
227
228
|
};
|
|
@@ -673,7 +674,7 @@ var ConfigLoaderService = class {
|
|
|
673
674
|
|
|
674
675
|
// src/services/InteractiveUIService.tsx
|
|
675
676
|
import React8 from "react";
|
|
676
|
-
import * as
|
|
677
|
+
import * as path10 from "path";
|
|
677
678
|
import { render } from "ink";
|
|
678
679
|
import * as cron2 from "node-cron";
|
|
679
680
|
import pLimit2 from "p-limit";
|
|
@@ -1949,7 +1950,7 @@ var App_default = App;
|
|
|
1949
1950
|
|
|
1950
1951
|
// src/services/worktree-sync.service.ts
|
|
1951
1952
|
import * as fs6 from "fs/promises";
|
|
1952
|
-
import * as
|
|
1953
|
+
import * as path8 from "path";
|
|
1953
1954
|
import pLimit from "p-limit";
|
|
1954
1955
|
import * as lockfile from "proper-lockfile";
|
|
1955
1956
|
|
|
@@ -2179,7 +2180,7 @@ function formatTimingTable(totalDuration, phaseResults, repoName) {
|
|
|
2179
2180
|
|
|
2180
2181
|
// src/services/git.service.ts
|
|
2181
2182
|
import * as fs4 from "fs/promises";
|
|
2182
|
-
import * as
|
|
2183
|
+
import * as path6 from "path";
|
|
2183
2184
|
import simpleGit4 from "simple-git";
|
|
2184
2185
|
|
|
2185
2186
|
// src/utils/worktree-list-parser.ts
|
|
@@ -2323,11 +2324,13 @@ function defaultConsoleOutput(msg, level) {
|
|
|
2323
2324
|
}
|
|
2324
2325
|
|
|
2325
2326
|
// src/services/sparse-checkout.service.ts
|
|
2327
|
+
import * as path3 from "path";
|
|
2326
2328
|
import simpleGit from "simple-git";
|
|
2327
2329
|
var SparseCheckoutService = class {
|
|
2328
2330
|
logger;
|
|
2329
2331
|
gitFactory;
|
|
2330
2332
|
warnedConfigs = /* @__PURE__ */ new WeakSet();
|
|
2333
|
+
matcherCache = /* @__PURE__ */ new WeakMap();
|
|
2331
2334
|
constructor(logger, gitFactory) {
|
|
2332
2335
|
this.logger = logger ?? Logger.createDefault();
|
|
2333
2336
|
this.gitFactory = gitFactory ?? ((p) => simpleGit(p));
|
|
@@ -2411,11 +2414,66 @@ var SparseCheckoutService = class {
|
|
|
2411
2414
|
const bt = b.map((x) => x.trim());
|
|
2412
2415
|
return at.every((v, i) => v === bt[i]);
|
|
2413
2416
|
}
|
|
2417
|
+
/**
|
|
2418
|
+
* Decide whether a list of changed file paths intersects the sparse-checkout
|
|
2419
|
+
* set defined by `cfg`. Used to skip fast-forward updates when upstream
|
|
2420
|
+
* commits only touch files outside the materialized worktree.
|
|
2421
|
+
*
|
|
2422
|
+
* Cone mode materializes:
|
|
2423
|
+
* - all files at the repository root,
|
|
2424
|
+
* - all files directly inside every ancestor of an included directory
|
|
2425
|
+
* (e.g. include `tools/build` keeps `tools/foo.txt` checked out too),
|
|
2426
|
+
* - everything inside an included directory.
|
|
2427
|
+
* We mirror those rules here. Missing the ancestor-files case would let
|
|
2428
|
+
* stale files linger when only those parent files change upstream.
|
|
2429
|
+
*
|
|
2430
|
+
* No-cone mode: gitignore-style matching with negation is non-trivial and
|
|
2431
|
+
* not implemented here yet. We return `true` so the caller falls back to
|
|
2432
|
+
* the safe behavior of always running the update.
|
|
2433
|
+
*
|
|
2434
|
+
* The matcher derived from `cfg` is cached on the cfg object identity
|
|
2435
|
+
* (WeakMap), so callers should reuse the same `cfg` reference across
|
|
2436
|
+
* invocations to benefit from the cache.
|
|
2437
|
+
*/
|
|
2438
|
+
pathsTouchSparse(changedPaths, cfg) {
|
|
2439
|
+
if (changedPaths.length === 0) return false;
|
|
2440
|
+
const matcher = this.getMatcher(cfg);
|
|
2441
|
+
if (matcher.mode === "no-cone") return true;
|
|
2442
|
+
if (matcher.patterns.length === 0) return true;
|
|
2443
|
+
return changedPaths.some((p) => {
|
|
2444
|
+
if (!p.includes("/")) return true;
|
|
2445
|
+
for (const pat of matcher.patterns) {
|
|
2446
|
+
if (p === pat || p.startsWith(pat + "/")) return true;
|
|
2447
|
+
}
|
|
2448
|
+
return matcher.ancestorDirs.has(path3.posix.dirname(p));
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
getMatcher(cfg) {
|
|
2452
|
+
const cached = this.matcherCache.get(cfg);
|
|
2453
|
+
if (cached) return cached;
|
|
2454
|
+
const mode = this.resolveMode(cfg);
|
|
2455
|
+
if (mode === "no-cone") {
|
|
2456
|
+
const matcher2 = { mode, patterns: [], ancestorDirs: /* @__PURE__ */ new Set() };
|
|
2457
|
+
this.matcherCache.set(cfg, matcher2);
|
|
2458
|
+
return matcher2;
|
|
2459
|
+
}
|
|
2460
|
+
const patterns = cfg.include.map((p) => p.trim()).filter((p) => p.length > 0).map((p) => p.endsWith("/") ? p.slice(0, -1) : p);
|
|
2461
|
+
const ancestorDirs = /* @__PURE__ */ new Set();
|
|
2462
|
+
for (const pat of patterns) {
|
|
2463
|
+
const parts = pat.split("/");
|
|
2464
|
+
for (let i = 1; i < parts.length; i++) {
|
|
2465
|
+
ancestorDirs.add(parts.slice(0, i).join("/"));
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
const matcher = { mode, patterns, ancestorDirs };
|
|
2469
|
+
this.matcherCache.set(cfg, matcher);
|
|
2470
|
+
return matcher;
|
|
2471
|
+
}
|
|
2414
2472
|
};
|
|
2415
2473
|
|
|
2416
2474
|
// src/services/worktree-metadata.service.ts
|
|
2417
2475
|
import * as fs2 from "fs/promises";
|
|
2418
|
-
import * as
|
|
2476
|
+
import * as path4 from "path";
|
|
2419
2477
|
import simpleGit2 from "simple-git";
|
|
2420
2478
|
var WorktreeMetadataService = class {
|
|
2421
2479
|
logger;
|
|
@@ -2428,7 +2486,7 @@ var WorktreeMetadataService = class {
|
|
|
2428
2486
|
* For example: /worktrees/fix/test-branch -> test-branch (not fix/test-branch)
|
|
2429
2487
|
*/
|
|
2430
2488
|
getWorktreeDirectoryName(worktreePath) {
|
|
2431
|
-
return
|
|
2489
|
+
return path4.basename(worktreePath);
|
|
2432
2490
|
}
|
|
2433
2491
|
async getMetadataPath(bareRepoPath, worktreeName) {
|
|
2434
2492
|
if (worktreeName.includes("/") || worktreeName.includes("\\")) {
|
|
@@ -2436,7 +2494,7 @@ var WorktreeMetadataService = class {
|
|
|
2436
2494
|
`getMetadataPath requires a filesystem-safe worktree directory name, got '${worktreeName}'. Use getMetadataPathFromWorktreePath when starting from a raw branch name.`
|
|
2437
2495
|
);
|
|
2438
2496
|
}
|
|
2439
|
-
return
|
|
2497
|
+
return path4.join(
|
|
2440
2498
|
bareRepoPath,
|
|
2441
2499
|
METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
|
|
2442
2500
|
worktreeName,
|
|
@@ -2449,7 +2507,7 @@ var WorktreeMetadataService = class {
|
|
|
2449
2507
|
}
|
|
2450
2508
|
async saveMetadata(bareRepoPath, worktreeName, metadata) {
|
|
2451
2509
|
const metadataPath = await this.getMetadataPath(bareRepoPath, worktreeName);
|
|
2452
|
-
await fs2.mkdir(
|
|
2510
|
+
await fs2.mkdir(path4.dirname(metadataPath), { recursive: true });
|
|
2453
2511
|
const tmpPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`;
|
|
2454
2512
|
let renamed = false;
|
|
2455
2513
|
try {
|
|
@@ -2642,7 +2700,7 @@ var WorktreeMetadataService = class {
|
|
|
2642
2700
|
|
|
2643
2701
|
// src/services/worktree-status.service.ts
|
|
2644
2702
|
import * as fs3 from "fs/promises";
|
|
2645
|
-
import * as
|
|
2703
|
+
import * as path5 from "path";
|
|
2646
2704
|
import simpleGit3 from "simple-git";
|
|
2647
2705
|
var OPERATION_FILES = [
|
|
2648
2706
|
{ file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
|
|
@@ -2845,7 +2903,7 @@ var WorktreeStatusService = class {
|
|
|
2845
2903
|
async detectOperationFile(gitDir) {
|
|
2846
2904
|
const results = await Promise.all(
|
|
2847
2905
|
OPERATION_FILES.map(
|
|
2848
|
-
({ file }) => fs3.access(
|
|
2906
|
+
({ file }) => fs3.access(path5.join(gitDir, file)).then(
|
|
2849
2907
|
() => true,
|
|
2850
2908
|
() => false
|
|
2851
2909
|
)
|
|
@@ -2966,14 +3024,14 @@ var WorktreeStatusService = class {
|
|
|
2966
3024
|
}
|
|
2967
3025
|
}
|
|
2968
3026
|
async resolveGitDir(worktreePath) {
|
|
2969
|
-
const gitPath =
|
|
3027
|
+
const gitPath = path5.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
|
|
2970
3028
|
try {
|
|
2971
3029
|
const stat3 = await fs3.stat(gitPath);
|
|
2972
3030
|
if (stat3.isFile()) {
|
|
2973
3031
|
const content = await fs3.readFile(gitPath, "utf-8");
|
|
2974
3032
|
const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
|
|
2975
3033
|
if (gitdirMatch) {
|
|
2976
|
-
return
|
|
3034
|
+
return path5.resolve(worktreePath, gitdirMatch[1].trim());
|
|
2977
3035
|
}
|
|
2978
3036
|
throw new GitOperationError("resolve-git-dir", `Failed to parse gitdir from .git file at ${gitPath}`);
|
|
2979
3037
|
}
|
|
@@ -2987,7 +3045,7 @@ var WorktreeStatusService = class {
|
|
|
2987
3045
|
}
|
|
2988
3046
|
}
|
|
2989
3047
|
createGitInstance(worktreePath) {
|
|
2990
|
-
const key = `${
|
|
3048
|
+
const key = `${path5.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
|
|
2991
3049
|
let git = this.gitInstances.get(key);
|
|
2992
3050
|
if (!git) {
|
|
2993
3051
|
git = this.config.skipLfs ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
@@ -3010,7 +3068,7 @@ var GitService = class {
|
|
|
3010
3068
|
this.config = config;
|
|
3011
3069
|
this.logger = logger ?? Logger.createDefault(void 0, config.debug);
|
|
3012
3070
|
this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
|
|
3013
|
-
this.mainWorktreePath =
|
|
3071
|
+
this.mainWorktreePath = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
|
|
3014
3072
|
this.metadataService = new WorktreeMetadataService(this.logger);
|
|
3015
3073
|
this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
|
|
3016
3074
|
this.sparseCheckoutService = new SparseCheckoutService(this.logger);
|
|
@@ -3038,16 +3096,36 @@ var GitService = class {
|
|
|
3038
3096
|
return this.config.cloneTimeoutMs ?? DEFAULT_CONFIG.CLONE_TIMEOUT_MS;
|
|
3039
3097
|
}
|
|
3040
3098
|
getCachedGit(dirPath, useLfsSkip = false) {
|
|
3041
|
-
const key = `${
|
|
3099
|
+
const key = `${path6.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
|
|
3042
3100
|
let git = this.gitInstances.get(key);
|
|
3043
3101
|
if (!git) {
|
|
3044
|
-
const
|
|
3045
|
-
const base = block > 0 ? simpleGit4(dirPath, { timeout: { block } }) : simpleGit4(dirPath);
|
|
3102
|
+
const base = simpleGit4(dirPath, this.buildSimpleGitOptions(this.getFetchTimeoutMs()));
|
|
3046
3103
|
git = useLfsSkip ? base.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : base;
|
|
3047
3104
|
this.gitInstances.set(key, git);
|
|
3048
3105
|
}
|
|
3049
3106
|
return git;
|
|
3050
3107
|
}
|
|
3108
|
+
buildSimpleGitOptions(blockMs) {
|
|
3109
|
+
const options = { progress: this.makeProgressHandler() };
|
|
3110
|
+
if (blockMs > 0) options.timeout = { block: blockMs };
|
|
3111
|
+
return options;
|
|
3112
|
+
}
|
|
3113
|
+
makeProgressHandler() {
|
|
3114
|
+
const lastBucket = /* @__PURE__ */ new Map();
|
|
3115
|
+
return (event) => {
|
|
3116
|
+
if (event.method !== "fetch" && event.method !== "clone" && event.method !== "pull") return;
|
|
3117
|
+
const key = `${event.method}:${event.stage}`;
|
|
3118
|
+
const bucket = Math.floor(event.progress / GIT_CONSTANTS.PROGRESS_BUCKET_PERCENT);
|
|
3119
|
+
let last = lastBucket.get(key) ?? -1;
|
|
3120
|
+
if (bucket < last) {
|
|
3121
|
+
last = -1;
|
|
3122
|
+
}
|
|
3123
|
+
if (bucket <= last && event.progress < 100) return;
|
|
3124
|
+
lastBucket.set(key, bucket);
|
|
3125
|
+
const total = event.total > 0 ? `${event.processed}/${event.total}` : `${event.processed}`;
|
|
3126
|
+
this.logger.info(` \u21B3 ${event.method} ${event.stage}: ${event.progress}% (${total})`);
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3051
3129
|
updateLogger(logger) {
|
|
3052
3130
|
this.logger = logger;
|
|
3053
3131
|
this.sparseCheckoutService.updateLogger(logger);
|
|
@@ -3055,14 +3133,13 @@ var GitService = class {
|
|
|
3055
3133
|
async initialize() {
|
|
3056
3134
|
const { repoUrl } = this.config;
|
|
3057
3135
|
try {
|
|
3058
|
-
await fs4.access(
|
|
3136
|
+
await fs4.access(path6.join(this.bareRepoPath, "HEAD"));
|
|
3059
3137
|
} catch {
|
|
3060
3138
|
this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
|
|
3061
|
-
await fs4.mkdir(
|
|
3062
|
-
const
|
|
3063
|
-
const cloneBase = cloneBlock > 0 ? simpleGit4({ timeout: { block: cloneBlock } }) : simpleGit4();
|
|
3139
|
+
await fs4.mkdir(path6.dirname(this.bareRepoPath), { recursive: true });
|
|
3140
|
+
const cloneBase = simpleGit4(this.buildSimpleGitOptions(this.getCloneTimeoutMs()));
|
|
3064
3141
|
const cloneGit = this.isLfsSkipEnabled() ? cloneBase.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : cloneBase;
|
|
3065
|
-
await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
|
|
3142
|
+
await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare", "--progress"]);
|
|
3066
3143
|
this.logger.info("\u2705 Clone successful.");
|
|
3067
3144
|
}
|
|
3068
3145
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
@@ -3076,19 +3153,19 @@ var GitService = class {
|
|
|
3076
3153
|
await bareGit.addConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*");
|
|
3077
3154
|
}
|
|
3078
3155
|
this.logger.info("Fetching remote branches...");
|
|
3079
|
-
await bareGit.fetch(["--all"]);
|
|
3156
|
+
await bareGit.fetch(["--all", "--progress"]);
|
|
3080
3157
|
this.defaultBranch = await this.detectDefaultBranch(bareGit);
|
|
3081
|
-
this.mainWorktreePath =
|
|
3158
|
+
this.mainWorktreePath = path6.join(this.config.worktreeDir, this.defaultBranch);
|
|
3082
3159
|
let needsMainWorktree = true;
|
|
3083
3160
|
try {
|
|
3084
3161
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
3085
|
-
needsMainWorktree = !worktrees.some((w) =>
|
|
3162
|
+
needsMainWorktree = !worktrees.some((w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath));
|
|
3086
3163
|
} catch {
|
|
3087
3164
|
}
|
|
3088
3165
|
if (needsMainWorktree) {
|
|
3089
3166
|
this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
|
|
3090
3167
|
await fs4.mkdir(this.config.worktreeDir, { recursive: true });
|
|
3091
|
-
const absoluteWorktreePath =
|
|
3168
|
+
const absoluteWorktreePath = path6.resolve(this.mainWorktreePath);
|
|
3092
3169
|
const branches = await bareGit.branch();
|
|
3093
3170
|
const defaultBranchExists = branches.all.includes(this.defaultBranch);
|
|
3094
3171
|
const useNoCheckoutMain = !!this.config.sparseCheckout;
|
|
@@ -3124,7 +3201,7 @@ var GitService = class {
|
|
|
3124
3201
|
}
|
|
3125
3202
|
const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
|
|
3126
3203
|
const mainWorktreeRegistered = updatedWorktrees.some(
|
|
3127
|
-
(w) =>
|
|
3204
|
+
(w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath)
|
|
3128
3205
|
);
|
|
3129
3206
|
if (!mainWorktreeRegistered) {
|
|
3130
3207
|
if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
|
|
@@ -3154,12 +3231,12 @@ var GitService = class {
|
|
|
3154
3231
|
this.assertInitialized();
|
|
3155
3232
|
this.logger.info("Fetching latest data from remote...");
|
|
3156
3233
|
const git = this.getCachedGit(this.mainWorktreePath, this.isLfsSkipEnabled());
|
|
3157
|
-
await git.fetch(["--all", "--prune"]);
|
|
3234
|
+
await git.fetch(["--all", "--prune", "--progress"]);
|
|
3158
3235
|
}
|
|
3159
3236
|
async fetchBranch(branchName) {
|
|
3160
3237
|
this.assertInitialized();
|
|
3161
3238
|
const git = this.getCachedGit(this.mainWorktreePath, this.isLfsSkipEnabled());
|
|
3162
|
-
await git.fetch(["origin", branchName, "--prune"]);
|
|
3239
|
+
await git.fetch(["origin", branchName, "--prune", "--progress"]);
|
|
3163
3240
|
}
|
|
3164
3241
|
assertInitialized() {
|
|
3165
3242
|
if (!this.git) {
|
|
@@ -3207,7 +3284,7 @@ var GitService = class {
|
|
|
3207
3284
|
const existence = await Promise.all(
|
|
3208
3285
|
lfsFileList.map(async (f) => {
|
|
3209
3286
|
try {
|
|
3210
|
-
await fs4.access(
|
|
3287
|
+
await fs4.access(path6.join(worktreePath, f));
|
|
3211
3288
|
return f;
|
|
3212
3289
|
} catch {
|
|
3213
3290
|
return null;
|
|
@@ -3235,7 +3312,7 @@ var GitService = class {
|
|
|
3235
3312
|
let allDownloaded = true;
|
|
3236
3313
|
const notDownloaded = [];
|
|
3237
3314
|
for (const file of samplesToCheck) {
|
|
3238
|
-
const filePath =
|
|
3315
|
+
const filePath = path6.join(worktreePath, file);
|
|
3239
3316
|
try {
|
|
3240
3317
|
const handle = await fs4.open(filePath, "r");
|
|
3241
3318
|
try {
|
|
@@ -3324,12 +3401,12 @@ var GitService = class {
|
|
|
3324
3401
|
}
|
|
3325
3402
|
async addWorktree(branchName, worktreePath) {
|
|
3326
3403
|
const bareGit = this.getCachedGit(this.bareRepoPath, this.isLfsSkipEnabled());
|
|
3327
|
-
const absoluteWorktreePath =
|
|
3328
|
-
await fs4.mkdir(
|
|
3404
|
+
const absoluteWorktreePath = path6.resolve(worktreePath);
|
|
3405
|
+
await fs4.mkdir(path6.dirname(absoluteWorktreePath), { recursive: true });
|
|
3329
3406
|
try {
|
|
3330
3407
|
await fs4.access(absoluteWorktreePath);
|
|
3331
3408
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
3332
|
-
const isValidWorktree = worktrees.some((w) =>
|
|
3409
|
+
const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
3333
3410
|
if (isValidWorktree) {
|
|
3334
3411
|
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
3335
3412
|
return;
|
|
@@ -3374,7 +3451,7 @@ var GitService = class {
|
|
|
3374
3451
|
}
|
|
3375
3452
|
if (errorMessage.includes("already registered worktree")) {
|
|
3376
3453
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
3377
|
-
const existingWorktree = worktrees.find((w) =>
|
|
3454
|
+
const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
3378
3455
|
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
3379
3456
|
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
|
|
3380
3457
|
return;
|
|
@@ -3420,7 +3497,7 @@ var GitService = class {
|
|
|
3420
3497
|
try {
|
|
3421
3498
|
await fs4.access(absoluteWorktreePath);
|
|
3422
3499
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
3423
|
-
const isValidWorktree = worktrees.some((w) =>
|
|
3500
|
+
const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
3424
3501
|
if (isValidWorktree) {
|
|
3425
3502
|
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
3426
3503
|
return;
|
|
@@ -3450,7 +3527,7 @@ var GitService = class {
|
|
|
3450
3527
|
const fallbackErrorMessage = getErrorMessage(fallbackError);
|
|
3451
3528
|
if (fallbackErrorMessage.includes("already registered worktree")) {
|
|
3452
3529
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
3453
|
-
const existingWorktree = worktrees.find((w) =>
|
|
3530
|
+
const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
3454
3531
|
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
3455
3532
|
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
|
|
3456
3533
|
return;
|
|
@@ -3673,6 +3750,23 @@ var GitService = class {
|
|
|
3673
3750
|
return false;
|
|
3674
3751
|
}
|
|
3675
3752
|
}
|
|
3753
|
+
async getChangedPathsInRange(worktreePath, fromRef, toRef) {
|
|
3754
|
+
const worktreeGit = this.getCachedGit(worktreePath);
|
|
3755
|
+
try {
|
|
3756
|
+
const out = await worktreeGit.raw([
|
|
3757
|
+
"-c",
|
|
3758
|
+
"core.quotePath=false",
|
|
3759
|
+
"diff",
|
|
3760
|
+
"--name-only",
|
|
3761
|
+
"--no-renames",
|
|
3762
|
+
`${fromRef}..${toRef}`
|
|
3763
|
+
]);
|
|
3764
|
+
return out.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0);
|
|
3765
|
+
} catch (error) {
|
|
3766
|
+
this.logger.warn(`Failed to compute diff ${fromRef}..${toRef} in ${worktreePath}: ${getErrorMessage(error)}`);
|
|
3767
|
+
return null;
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3676
3770
|
async compareTreeContent(worktreePath, branch) {
|
|
3677
3771
|
const worktreeGit = this.getCachedGit(worktreePath);
|
|
3678
3772
|
try {
|
|
@@ -3757,7 +3851,7 @@ var GitService = class {
|
|
|
3757
3851
|
// src/services/path-resolution.service.ts
|
|
3758
3852
|
import { createHash } from "crypto";
|
|
3759
3853
|
import * as fs5 from "fs";
|
|
3760
|
-
import * as
|
|
3854
|
+
import * as path7 from "path";
|
|
3761
3855
|
var BRANCH_STEM_MAX = 80;
|
|
3762
3856
|
var BRANCH_HASH_LEN = 8;
|
|
3763
3857
|
var PathResolutionService = class {
|
|
@@ -3767,22 +3861,22 @@ var PathResolutionService = class {
|
|
|
3767
3861
|
return `${stem}-${hash}`;
|
|
3768
3862
|
}
|
|
3769
3863
|
getBranchWorktreePath(worktreeDir, branchName) {
|
|
3770
|
-
return
|
|
3864
|
+
return path7.join(worktreeDir, this.sanitizeBranchName(branchName));
|
|
3771
3865
|
}
|
|
3772
3866
|
resolveRealPath(inputPath) {
|
|
3773
|
-
const absolute =
|
|
3867
|
+
const absolute = path7.resolve(inputPath);
|
|
3774
3868
|
const missing = [];
|
|
3775
3869
|
let current = absolute;
|
|
3776
3870
|
while (!fs5.existsSync(current)) {
|
|
3777
|
-
const parent =
|
|
3871
|
+
const parent = path7.dirname(current);
|
|
3778
3872
|
if (parent === current) {
|
|
3779
3873
|
return absolute;
|
|
3780
3874
|
}
|
|
3781
|
-
missing.unshift(
|
|
3875
|
+
missing.unshift(path7.basename(current));
|
|
3782
3876
|
current = parent;
|
|
3783
3877
|
}
|
|
3784
3878
|
try {
|
|
3785
|
-
return
|
|
3879
|
+
return path7.join(fs5.realpathSync(current), ...missing);
|
|
3786
3880
|
} catch {
|
|
3787
3881
|
return absolute;
|
|
3788
3882
|
}
|
|
@@ -3792,7 +3886,7 @@ var PathResolutionService = class {
|
|
|
3792
3886
|
const a = fold(resolved);
|
|
3793
3887
|
const b = fold(resolvedBase);
|
|
3794
3888
|
if (a === b) return true;
|
|
3795
|
-
return a.length > b.length && a.charAt(b.length) ===
|
|
3889
|
+
return a.length > b.length && a.charAt(b.length) === path7.sep && a.startsWith(b);
|
|
3796
3890
|
}
|
|
3797
3891
|
normalizeWorktreePath(worktreePath, worktreeBaseDir) {
|
|
3798
3892
|
const resolved = this.resolveRealPath(worktreePath);
|
|
@@ -3800,7 +3894,7 @@ var PathResolutionService = class {
|
|
|
3800
3894
|
if (!this.isResolvedPathInsideBase(resolved, resolvedBase)) {
|
|
3801
3895
|
throw new Error(`Worktree path '${worktreePath}' is outside base directory '${worktreeBaseDir}'`);
|
|
3802
3896
|
}
|
|
3803
|
-
return
|
|
3897
|
+
return path7.relative(resolvedBase, resolved);
|
|
3804
3898
|
}
|
|
3805
3899
|
isPathInsideBaseDir(targetPath, baseDir) {
|
|
3806
3900
|
const resolved = this.resolveRealPath(targetPath);
|
|
@@ -3906,7 +4000,7 @@ var WorktreeSyncService = class {
|
|
|
3906
4000
|
};
|
|
3907
4001
|
}
|
|
3908
4002
|
const barePath = this.gitService.getBareRepoPath();
|
|
3909
|
-
const lockTarget =
|
|
4003
|
+
const lockTarget = path8.join(barePath, "HEAD");
|
|
3910
4004
|
try {
|
|
3911
4005
|
await fs6.access(lockTarget);
|
|
3912
4006
|
} catch {
|
|
@@ -4104,12 +4198,12 @@ var WorktreeSyncService = class {
|
|
|
4104
4198
|
}
|
|
4105
4199
|
const reservedPaths = /* @__PURE__ */ new Map();
|
|
4106
4200
|
for (const w of worktrees) {
|
|
4107
|
-
reservedPaths.set(
|
|
4201
|
+
reservedPaths.set(path8.resolve(w.path), w.branch);
|
|
4108
4202
|
}
|
|
4109
4203
|
const plan = [];
|
|
4110
4204
|
for (const branchName of newBranches) {
|
|
4111
4205
|
const worktreePath = this.pathResolution.getBranchWorktreePath(this.config.worktreeDir, branchName);
|
|
4112
|
-
const resolved =
|
|
4206
|
+
const resolved = path8.resolve(worktreePath);
|
|
4113
4207
|
const conflict = reservedPaths.get(resolved);
|
|
4114
4208
|
if (conflict && conflict !== branchName) {
|
|
4115
4209
|
this.logger.error(
|
|
@@ -4314,12 +4408,12 @@ var WorktreeSyncService = class {
|
|
|
4314
4408
|
}
|
|
4315
4409
|
async updateExistingWorktrees(worktrees, remoteBranches) {
|
|
4316
4410
|
this.logger.info("Step 4: Checking for worktrees that need updates...");
|
|
4317
|
-
const divergedDir =
|
|
4411
|
+
const divergedDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
4318
4412
|
try {
|
|
4319
4413
|
const diverged = await fs6.readdir(divergedDir);
|
|
4320
4414
|
if (diverged.length > 0) {
|
|
4321
4415
|
this.logger.info(
|
|
4322
|
-
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${
|
|
4416
|
+
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path8.relative(process.cwd(), divergedDir)}`
|
|
4323
4417
|
);
|
|
4324
4418
|
}
|
|
4325
4419
|
} catch {
|
|
@@ -4349,7 +4443,23 @@ var WorktreeSyncService = class {
|
|
|
4349
4443
|
return { action: "diverged", worktree };
|
|
4350
4444
|
}
|
|
4351
4445
|
const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
|
|
4352
|
-
|
|
4446
|
+
if (!isBehind) return null;
|
|
4447
|
+
const sparseCfg = this.config.sparseCheckout;
|
|
4448
|
+
if (sparseCfg && sparseCfg.skipUpdateWhenOutsideSparse !== false) {
|
|
4449
|
+
const sparseService = this.gitService.getSparseCheckoutService();
|
|
4450
|
+
if (sparseService.resolveMode(sparseCfg) === "cone") {
|
|
4451
|
+
const diff = await this.gitService.getChangedPathsInRange(
|
|
4452
|
+
worktree.path,
|
|
4453
|
+
"HEAD",
|
|
4454
|
+
`origin/${worktree.branch}`
|
|
4455
|
+
);
|
|
4456
|
+
if (diff !== null && !sparseService.pathsTouchSparse(diff, sparseCfg)) {
|
|
4457
|
+
this.logger.info(`\u23ED\uFE0F Skipping '${worktree.branch}' - upstream changes outside sparse paths`);
|
|
4458
|
+
return null;
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
return { action: "update", worktree };
|
|
4353
4463
|
})
|
|
4354
4464
|
)
|
|
4355
4465
|
);
|
|
@@ -4427,13 +4537,13 @@ var WorktreeSyncService = class {
|
|
|
4427
4537
|
}
|
|
4428
4538
|
async cleanupOrphanedDirectories(worktrees) {
|
|
4429
4539
|
try {
|
|
4430
|
-
const worktreeRelativePaths = worktrees.map((w) =>
|
|
4540
|
+
const worktreeRelativePaths = worktrees.map((w) => path8.relative(this.config.worktreeDir, w.path));
|
|
4431
4541
|
const allDirs = await fs6.readdir(this.config.worktreeDir);
|
|
4432
4542
|
const regularDirs = allDirs.filter((dir) => !dir.startsWith("."));
|
|
4433
4543
|
const orphanedDirs = [];
|
|
4434
4544
|
for (const dir of regularDirs) {
|
|
4435
4545
|
const isPartOfWorktree = worktreeRelativePaths.some((worktreePath) => {
|
|
4436
|
-
return worktreePath === dir || worktreePath.startsWith(dir +
|
|
4546
|
+
return worktreePath === dir || worktreePath.startsWith(dir + path8.sep);
|
|
4437
4547
|
});
|
|
4438
4548
|
if (!isPartOfWorktree) {
|
|
4439
4549
|
orphanedDirs.push(dir);
|
|
@@ -4442,7 +4552,7 @@ var WorktreeSyncService = class {
|
|
|
4442
4552
|
if (orphanedDirs.length > 0) {
|
|
4443
4553
|
this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
|
|
4444
4554
|
for (const dir of orphanedDirs) {
|
|
4445
|
-
const dirPath =
|
|
4555
|
+
const dirPath = path8.join(this.config.worktreeDir, dir);
|
|
4446
4556
|
try {
|
|
4447
4557
|
const stat3 = await fs6.stat(dirPath);
|
|
4448
4558
|
if (stat3.isDirectory()) {
|
|
@@ -4476,7 +4586,7 @@ var WorktreeSyncService = class {
|
|
|
4476
4586
|
} else {
|
|
4477
4587
|
this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
|
|
4478
4588
|
const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
|
|
4479
|
-
const relativePath =
|
|
4589
|
+
const relativePath = path8.relative(process.cwd(), divergedPath);
|
|
4480
4590
|
this.logger.info(` Moved to: ${relativePath}`);
|
|
4481
4591
|
this.logger.info(` Your local changes are preserved. To review:`);
|
|
4482
4592
|
this.logger.info(` cd ${relativePath}`);
|
|
@@ -4500,12 +4610,12 @@ var WorktreeSyncService = class {
|
|
|
4500
4610
|
}
|
|
4501
4611
|
}
|
|
4502
4612
|
async divergeWorktree(worktreePath, branchName) {
|
|
4503
|
-
const divergedBaseDir =
|
|
4613
|
+
const divergedBaseDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
4504
4614
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
4505
4615
|
const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
4506
4616
|
const safeBranchName = this.pathResolution.sanitizeBranchName(branchName);
|
|
4507
4617
|
const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
|
|
4508
|
-
const divergedPath =
|
|
4618
|
+
const divergedPath = path8.join(divergedBaseDir, divergedName);
|
|
4509
4619
|
await fs6.mkdir(divergedBaseDir, { recursive: true });
|
|
4510
4620
|
try {
|
|
4511
4621
|
await fs6.rename(worktreePath, divergedPath);
|
|
@@ -4532,7 +4642,7 @@ var WorktreeSyncService = class {
|
|
|
4532
4642
|
Original worktree location: ${worktreePath}`
|
|
4533
4643
|
};
|
|
4534
4644
|
await fs6.writeFile(
|
|
4535
|
-
|
|
4645
|
+
path8.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
|
|
4536
4646
|
JSON.stringify(metadata, null, 2)
|
|
4537
4647
|
);
|
|
4538
4648
|
return divergedPath;
|
|
@@ -4541,7 +4651,7 @@ var WorktreeSyncService = class {
|
|
|
4541
4651
|
|
|
4542
4652
|
// src/services/file-copy.service.ts
|
|
4543
4653
|
import * as fs7 from "fs/promises";
|
|
4544
|
-
import * as
|
|
4654
|
+
import * as path9 from "path";
|
|
4545
4655
|
import { glob } from "glob";
|
|
4546
4656
|
var DEFAULT_IGNORE_PATTERNS = [
|
|
4547
4657
|
"**/node_modules/**",
|
|
@@ -4568,8 +4678,8 @@ var FileCopyService = class {
|
|
|
4568
4678
|
}
|
|
4569
4679
|
const filesToCopy = await this.expandPatterns(sourceDir, patterns);
|
|
4570
4680
|
for (const relativePath of filesToCopy) {
|
|
4571
|
-
const sourcePath =
|
|
4572
|
-
const destPath =
|
|
4681
|
+
const sourcePath = path9.join(sourceDir, relativePath);
|
|
4682
|
+
const destPath = path9.join(destDir, relativePath);
|
|
4573
4683
|
try {
|
|
4574
4684
|
const copied = await this.copyFile(sourcePath, destPath);
|
|
4575
4685
|
if (copied) {
|
|
@@ -4610,7 +4720,7 @@ var FileCopyService = class {
|
|
|
4610
4720
|
return false;
|
|
4611
4721
|
} catch {
|
|
4612
4722
|
}
|
|
4613
|
-
const destDir =
|
|
4723
|
+
const destDir = path9.dirname(destPath);
|
|
4614
4724
|
await fs7.mkdir(destDir, { recursive: true });
|
|
4615
4725
|
await fs7.copyFile(sourcePath, destPath);
|
|
4616
4726
|
return true;
|
|
@@ -4828,6 +4938,8 @@ var AppEventEmitter = class {
|
|
|
4828
4938
|
|
|
4829
4939
|
// src/services/InteractiveUIService.tsx
|
|
4830
4940
|
import * as fs8 from "fs/promises";
|
|
4941
|
+
var WAIT_SYNC_FAST_TIMEOUT_MS = 2e3;
|
|
4942
|
+
var WAIT_SYNC_DEFAULT_TIMEOUT_MS = 3e4;
|
|
4831
4943
|
var InteractiveUIService = class {
|
|
4832
4944
|
app = null;
|
|
4833
4945
|
syncServices;
|
|
@@ -4935,6 +5047,9 @@ var InteractiveUIService = class {
|
|
|
4935
5047
|
}
|
|
4936
5048
|
this.cronJobs = [];
|
|
4937
5049
|
}
|
|
5050
|
+
registerCronJob(job) {
|
|
5051
|
+
this.cronJobs.push(job);
|
|
5052
|
+
}
|
|
4938
5053
|
renderUI() {
|
|
4939
5054
|
if (this.app) {
|
|
4940
5055
|
this.app.unmount();
|
|
@@ -4958,8 +5073,8 @@ var InteractiveUIService = class {
|
|
|
4958
5073
|
getWorktreeStatusForRepo: (index) => this.getWorktreeStatusForRepo(index),
|
|
4959
5074
|
getDivergedDirectoriesForRepo: (index) => this.getDivergedDirectoriesForRepo(index),
|
|
4960
5075
|
deleteDivergedDirectory: (repoIndex, name) => this.deleteDivergedDirectory(repoIndex, name),
|
|
4961
|
-
openEditorInWorktree: (
|
|
4962
|
-
openTerminalInWorktree: (repoIndex,
|
|
5076
|
+
openEditorInWorktree: (path14) => this.openEditorInWorktree(path14),
|
|
5077
|
+
openTerminalInWorktree: (repoIndex, path14, branchName) => this.openTerminalInWorktree(repoIndex, path14, branchName),
|
|
4963
5078
|
copyBranchFiles: (repoIndex, baseBranch, targetBranch) => this.copyBranchFiles(repoIndex, baseBranch, targetBranch),
|
|
4964
5079
|
createWorktreeForBranch: (repoIndex, branchName) => this.createWorktreeForBranch(repoIndex, branchName),
|
|
4965
5080
|
executeOnBranchCreatedHooks: (repoIndex, context) => this.executeOnBranchCreatedHooks(repoIndex, context)
|
|
@@ -5049,18 +5164,17 @@ var InteractiveUIService = class {
|
|
|
5049
5164
|
await this.destroy();
|
|
5050
5165
|
process.exit(0);
|
|
5051
5166
|
}
|
|
5052
|
-
async waitForInProgressSyncs() {
|
|
5167
|
+
async waitForInProgressSyncs(timeoutMs = WAIT_SYNC_DEFAULT_TIMEOUT_MS) {
|
|
5053
5168
|
const inProgressServices = this.syncServices.filter((s) => s.isSyncInProgress());
|
|
5054
5169
|
if (inProgressServices.length === 0) {
|
|
5055
5170
|
return;
|
|
5056
5171
|
}
|
|
5057
5172
|
this.addLog(`Waiting for ${inProgressServices.length} in-progress sync(s) to finish...`, "info");
|
|
5058
5173
|
const syncChecks = inProgressServices.map(async (service) => {
|
|
5059
|
-
const timeout = 3e4;
|
|
5060
5174
|
const checkInterval = 500;
|
|
5061
5175
|
const startTime = Date.now();
|
|
5062
5176
|
while (service.isSyncInProgress()) {
|
|
5063
|
-
if (Date.now() - startTime >
|
|
5177
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
5064
5178
|
throw new Error("Timeout waiting for sync operations to complete");
|
|
5065
5179
|
}
|
|
5066
5180
|
await new Promise((resolve9) => setTimeout(resolve9, checkInterval));
|
|
@@ -5070,7 +5184,7 @@ var InteractiveUIService = class {
|
|
|
5070
5184
|
await Promise.all(syncChecks);
|
|
5071
5185
|
} catch {
|
|
5072
5186
|
this.addLog(
|
|
5073
|
-
|
|
5187
|
+
`Warning: Timeout waiting for sync operations to complete after ${formatDuration2(timeoutMs)}. Proceeding with potential data loss risk.`,
|
|
5074
5188
|
"warn"
|
|
5075
5189
|
);
|
|
5076
5190
|
}
|
|
@@ -5196,7 +5310,7 @@ var InteractiveUIService = class {
|
|
|
5196
5310
|
}
|
|
5197
5311
|
const service = this.syncServices[repoIndex];
|
|
5198
5312
|
const worktreeDir = service.config.worktreeDir;
|
|
5199
|
-
const divergedDir =
|
|
5313
|
+
const divergedDir = path10.join(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
5200
5314
|
let dirEntries;
|
|
5201
5315
|
try {
|
|
5202
5316
|
dirEntries = await fs8.readdir(divergedDir, { withFileTypes: true, encoding: "utf-8" });
|
|
@@ -5206,8 +5320,8 @@ var InteractiveUIService = class {
|
|
|
5206
5320
|
const subdirs = dirEntries.filter((e) => e.isDirectory());
|
|
5207
5321
|
const results = await Promise.allSettled(
|
|
5208
5322
|
subdirs.map(async (entry) => {
|
|
5209
|
-
const fullPath =
|
|
5210
|
-
const infoFilePath =
|
|
5323
|
+
const fullPath = path10.join(divergedDir, entry.name);
|
|
5324
|
+
const infoFilePath = path10.join(fullPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE);
|
|
5211
5325
|
let originalBranch = entry.name;
|
|
5212
5326
|
let divergedAt = "";
|
|
5213
5327
|
try {
|
|
@@ -5242,11 +5356,11 @@ var InteractiveUIService = class {
|
|
|
5242
5356
|
}
|
|
5243
5357
|
const service = this.syncServices[repoIndex];
|
|
5244
5358
|
const worktreeDir = service.config.worktreeDir;
|
|
5245
|
-
const divergedBase =
|
|
5359
|
+
const divergedBase = path10.resolve(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
5246
5360
|
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
|
|
5247
5361
|
throw new Error(`Invalid diverged directory name: "${name}"`);
|
|
5248
5362
|
}
|
|
5249
|
-
const targetPath =
|
|
5363
|
+
const targetPath = path10.join(divergedBase, name);
|
|
5250
5364
|
if (!this.pathResolution.isPathInsideBaseDir(targetPath, divergedBase)) {
|
|
5251
5365
|
throw new Error(`Path traversal rejected: "${name}" resolves outside the diverged directory`);
|
|
5252
5366
|
}
|
|
@@ -5485,11 +5599,11 @@ var InteractiveUIService = class {
|
|
|
5485
5599
|
this.addLog(`Failed to copy files to new branch: ${error}`, "error");
|
|
5486
5600
|
}
|
|
5487
5601
|
}
|
|
5488
|
-
async destroy() {
|
|
5602
|
+
async destroy(fast = false) {
|
|
5489
5603
|
this.isDestroyed = true;
|
|
5490
5604
|
this.cancelCronJobs();
|
|
5491
5605
|
try {
|
|
5492
|
-
await this.waitForInProgressSyncs();
|
|
5606
|
+
await this.waitForInProgressSyncs(fast ? WAIT_SYNC_FAST_TIMEOUT_MS : WAIT_SYNC_DEFAULT_TIMEOUT_MS);
|
|
5493
5607
|
} catch {
|
|
5494
5608
|
}
|
|
5495
5609
|
this.hookExecutionService.cleanup();
|
|
@@ -5635,7 +5749,7 @@ function reconstructCliCommand(config) {
|
|
|
5635
5749
|
|
|
5636
5750
|
// src/utils/config-generator.ts
|
|
5637
5751
|
import * as fs9 from "fs/promises";
|
|
5638
|
-
import * as
|
|
5752
|
+
import * as path11 from "path";
|
|
5639
5753
|
function serializeToESM(obj, indent = 0) {
|
|
5640
5754
|
const spaces = " ".repeat(indent);
|
|
5641
5755
|
const innerSpaces = " ".repeat(indent + 2);
|
|
@@ -5665,9 +5779,9 @@ ${spaces}}`;
|
|
|
5665
5779
|
return String(obj);
|
|
5666
5780
|
}
|
|
5667
5781
|
async function generateConfigFile(config, configPath) {
|
|
5668
|
-
const configDir =
|
|
5782
|
+
const configDir = path11.dirname(configPath);
|
|
5669
5783
|
await fs9.mkdir(configDir, { recursive: true });
|
|
5670
|
-
const worktreeDirRelative =
|
|
5784
|
+
const worktreeDirRelative = path11.relative(configDir, config.worktreeDir);
|
|
5671
5785
|
const useRelativeWorktree = !worktreeDirRelative.startsWith("../../../");
|
|
5672
5786
|
const repoName = extractRepoNameFromUrl(config.repoUrl);
|
|
5673
5787
|
const repository = {
|
|
@@ -5676,7 +5790,7 @@ async function generateConfigFile(config, configPath) {
|
|
|
5676
5790
|
worktreeDir: useRelativeWorktree ? `./${worktreeDirRelative}` : config.worktreeDir
|
|
5677
5791
|
};
|
|
5678
5792
|
if (config.bareRepoDir) {
|
|
5679
|
-
const bareRepoDirRelative =
|
|
5793
|
+
const bareRepoDirRelative = path11.relative(configDir, config.bareRepoDir);
|
|
5680
5794
|
const useRelativeBare = !bareRepoDirRelative.startsWith("../../../");
|
|
5681
5795
|
repository.bareRepoDir = useRelativeBare ? `./${bareRepoDirRelative}` : config.bareRepoDir;
|
|
5682
5796
|
}
|
|
@@ -5697,11 +5811,11 @@ export default ${serializeToESM(configObject)};
|
|
|
5697
5811
|
await fs9.writeFile(configPath, configContent, "utf-8");
|
|
5698
5812
|
}
|
|
5699
5813
|
function getDefaultConfigPath() {
|
|
5700
|
-
return
|
|
5814
|
+
return path11.join(process.cwd(), "sync-worktrees.config.js");
|
|
5701
5815
|
}
|
|
5702
5816
|
async function findConfigInCwd(cwd = process.cwd()) {
|
|
5703
5817
|
for (const name of CONFIG_FILE_NAMES) {
|
|
5704
|
-
const full =
|
|
5818
|
+
const full = path11.join(cwd, name);
|
|
5705
5819
|
try {
|
|
5706
5820
|
await fs9.access(full);
|
|
5707
5821
|
return full;
|
|
@@ -5712,7 +5826,7 @@ async function findConfigInCwd(cwd = process.cwd()) {
|
|
|
5712
5826
|
}
|
|
5713
5827
|
|
|
5714
5828
|
// src/utils/interactive.ts
|
|
5715
|
-
import * as
|
|
5829
|
+
import * as path12 from "path";
|
|
5716
5830
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
5717
5831
|
async function promptForConfig(partialConfig) {
|
|
5718
5832
|
console.log("\u{1F527} Welcome to sync-worktrees interactive setup!\n");
|
|
@@ -5752,8 +5866,8 @@ async function promptForConfig(partialConfig) {
|
|
|
5752
5866
|
if (!worktreeDir.trim() && defaultWorktreeDir) {
|
|
5753
5867
|
worktreeDir = defaultWorktreeDir;
|
|
5754
5868
|
}
|
|
5755
|
-
if (!
|
|
5756
|
-
worktreeDir =
|
|
5869
|
+
if (!path12.isAbsolute(worktreeDir)) {
|
|
5870
|
+
worktreeDir = path12.resolve(worktreeDir);
|
|
5757
5871
|
}
|
|
5758
5872
|
}
|
|
5759
5873
|
let bareRepoDir = partialConfig.bareRepoDir;
|
|
@@ -5772,8 +5886,8 @@ async function promptForConfig(partialConfig) {
|
|
|
5772
5886
|
return true;
|
|
5773
5887
|
}
|
|
5774
5888
|
});
|
|
5775
|
-
if (!
|
|
5776
|
-
bareRepoDir =
|
|
5889
|
+
if (!path12.isAbsolute(bareRepoDir)) {
|
|
5890
|
+
bareRepoDir = path12.resolve(bareRepoDir);
|
|
5777
5891
|
}
|
|
5778
5892
|
}
|
|
5779
5893
|
let runOnce = partialConfig.runOnce;
|
|
@@ -5845,8 +5959,8 @@ async function promptForConfig(partialConfig) {
|
|
|
5845
5959
|
return true;
|
|
5846
5960
|
}
|
|
5847
5961
|
});
|
|
5848
|
-
if (!
|
|
5849
|
-
configPath =
|
|
5962
|
+
if (!path12.isAbsolute(configPath)) {
|
|
5963
|
+
configPath = path12.resolve(configPath);
|
|
5850
5964
|
}
|
|
5851
5965
|
try {
|
|
5852
5966
|
await generateConfigFile(finalConfig, configPath);
|
|
@@ -5864,26 +5978,55 @@ async function promptForConfig(partialConfig) {
|
|
|
5864
5978
|
return { config: finalConfig, savedConfigPath };
|
|
5865
5979
|
}
|
|
5866
5980
|
|
|
5867
|
-
// src/
|
|
5868
|
-
var
|
|
5869
|
-
function setupSignalHandlers() {
|
|
5870
|
-
|
|
5871
|
-
const
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5981
|
+
// src/utils/signal-handlers.ts
|
|
5982
|
+
var DEFAULT_FORCE_EXIT_MS = 3e3;
|
|
5983
|
+
function setupSignalHandlers(options = {}) {
|
|
5984
|
+
const forceExitMs = options.forceExitMs ?? DEFAULT_FORCE_EXIT_MS;
|
|
5985
|
+
const log = options.log ?? ((msg) => console.log(msg));
|
|
5986
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
5987
|
+
const target = options.process ?? process;
|
|
5988
|
+
const cleanupFns = [];
|
|
5989
|
+
let signalCount = 0;
|
|
5990
|
+
const handler = (signal) => {
|
|
5991
|
+
signalCount += 1;
|
|
5992
|
+
if (signalCount >= 2) {
|
|
5993
|
+
log(`
|
|
5994
|
+
Received second ${signal}, forcing exit.`);
|
|
5995
|
+
exit(130);
|
|
5996
|
+
return;
|
|
5997
|
+
}
|
|
5998
|
+
log(`
|
|
5999
|
+
Received ${signal}, shutting down (Ctrl+C again to force exit)...`);
|
|
6000
|
+
const watchdog = setTimeout(() => {
|
|
6001
|
+
log(`
|
|
6002
|
+
Shutdown took longer than ${forceExitMs}ms, forcing exit.`);
|
|
6003
|
+
exit(130);
|
|
6004
|
+
}, forceExitMs);
|
|
6005
|
+
if (typeof watchdog.unref === "function") {
|
|
6006
|
+
watchdog.unref();
|
|
6007
|
+
}
|
|
6008
|
+
void Promise.allSettled(cleanupFns.map((fn) => Promise.resolve().then(() => fn(true)))).then(() => {
|
|
6009
|
+
clearTimeout(watchdog);
|
|
6010
|
+
exit(0);
|
|
6011
|
+
});
|
|
6012
|
+
};
|
|
6013
|
+
const sigintListener = () => handler("SIGINT");
|
|
6014
|
+
const sigtermListener = () => handler("SIGTERM");
|
|
6015
|
+
target.on("SIGINT", sigintListener);
|
|
6016
|
+
target.on("SIGTERM", sigtermListener);
|
|
6017
|
+
return {
|
|
6018
|
+
register: (fn) => {
|
|
6019
|
+
cleanupFns.push(fn);
|
|
6020
|
+
},
|
|
6021
|
+
dispose: () => {
|
|
6022
|
+
target.removeListener("SIGINT", sigintListener);
|
|
6023
|
+
target.removeListener("SIGTERM", sigtermListener);
|
|
5881
6024
|
}
|
|
5882
|
-
process.exit(0);
|
|
5883
6025
|
};
|
|
5884
|
-
process.on("SIGINT", () => void handler("SIGINT"));
|
|
5885
|
-
process.on("SIGTERM", () => void handler("SIGTERM"));
|
|
5886
6026
|
}
|
|
6027
|
+
|
|
6028
|
+
// src/index.ts
|
|
6029
|
+
var signalHandle = setupSignalHandlers();
|
|
5887
6030
|
async function runSingleRepository(config) {
|
|
5888
6031
|
const logger = Logger.createDefault(void 0, config.debug);
|
|
5889
6032
|
logger.info("\n\u{1F4CB} CLI Command (for future reference):");
|
|
@@ -5900,11 +6043,11 @@ async function runSingleRepository(config) {
|
|
|
5900
6043
|
await syncService.sync();
|
|
5901
6044
|
} else {
|
|
5902
6045
|
const uiService = new InteractiveUIService([syncService], void 0, config.cronSchedule);
|
|
5903
|
-
|
|
6046
|
+
signalHandle.register((fast) => uiService.destroy(fast));
|
|
5904
6047
|
await syncService.sync();
|
|
5905
6048
|
uiService.updateLastSyncTime();
|
|
5906
6049
|
void uiService.calculateAndUpdateDiskSpace();
|
|
5907
|
-
cron3.schedule(config.cronSchedule, async () => {
|
|
6050
|
+
const job = cron3.schedule(config.cronSchedule, async () => {
|
|
5908
6051
|
try {
|
|
5909
6052
|
uiService.setStatus("syncing");
|
|
5910
6053
|
await syncService.sync();
|
|
@@ -5915,6 +6058,7 @@ async function runSingleRepository(config) {
|
|
|
5915
6058
|
uiService.setStatus("idle");
|
|
5916
6059
|
}
|
|
5917
6060
|
});
|
|
6061
|
+
uiService.registerCronJob(job);
|
|
5918
6062
|
}
|
|
5919
6063
|
} catch (error) {
|
|
5920
6064
|
logger.error("\u274C Fatal Error during initialization:", error);
|
|
@@ -5981,7 +6125,7 @@ async function runMultipleRepositories(repositories, runOnce, configPath, maxPar
|
|
|
5981
6125
|
const displaySchedule = uniqueSchedules.length === 1 ? uniqueSchedules[0] : void 0;
|
|
5982
6126
|
const allServices = Array.from(services.values());
|
|
5983
6127
|
const uiService = new InteractiveUIService(allServices, configPath, displaySchedule, maxParallel, reloadOptions);
|
|
5984
|
-
|
|
6128
|
+
signalHandle.register((fast) => uiService.destroy(fast));
|
|
5985
6129
|
void uiService.calculateAndUpdateDiskSpace();
|
|
5986
6130
|
uiService.setupCronJobs();
|
|
5987
6131
|
uiService.addLog(`\u{1F4CB} ${repositories.length} repositories configured`);
|
|
@@ -6076,13 +6220,12 @@ async function runInteractive(partial, options) {
|
|
|
6076
6220
|
await runSingleRepository(config);
|
|
6077
6221
|
}
|
|
6078
6222
|
async function main() {
|
|
6079
|
-
setupSignalHandlers();
|
|
6080
6223
|
const options = parseArguments();
|
|
6081
6224
|
if (!options.config && !options.repoUrl && !options.worktreeDir) {
|
|
6082
6225
|
const discovered = await findConfigInCwd();
|
|
6083
6226
|
if (discovered) {
|
|
6084
6227
|
options.config = discovered;
|
|
6085
|
-
console.log(`\u{1F4C4} Using config: ${
|
|
6228
|
+
console.log(`\u{1F4C4} Using config: ${path13.relative(process.cwd(), discovered)}`);
|
|
6086
6229
|
}
|
|
6087
6230
|
}
|
|
6088
6231
|
if (options.config) {
|