sync-worktrees 3.4.0 → 3.5.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 +228 -105
- package/dist/index.js.map +4 -4
- package/dist/mcp-server.js +169 -79
- 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";
|
|
@@ -219,9 +219,9 @@ var WorktreeError = class extends SyncWorktreesError {
|
|
|
219
219
|
}
|
|
220
220
|
};
|
|
221
221
|
var WorktreeNotCleanError = class extends WorktreeError {
|
|
222
|
-
constructor(
|
|
223
|
-
super(`Worktree at '${
|
|
224
|
-
this.path =
|
|
222
|
+
constructor(path14, reasons) {
|
|
223
|
+
super(`Worktree at '${path14}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
|
|
224
|
+
this.path = path14;
|
|
225
225
|
this.reasons = reasons;
|
|
226
226
|
}
|
|
227
227
|
};
|
|
@@ -673,7 +673,7 @@ var ConfigLoaderService = class {
|
|
|
673
673
|
|
|
674
674
|
// src/services/InteractiveUIService.tsx
|
|
675
675
|
import React8 from "react";
|
|
676
|
-
import * as
|
|
676
|
+
import * as path10 from "path";
|
|
677
677
|
import { render } from "ink";
|
|
678
678
|
import * as cron2 from "node-cron";
|
|
679
679
|
import pLimit2 from "p-limit";
|
|
@@ -1949,7 +1949,7 @@ var App_default = App;
|
|
|
1949
1949
|
|
|
1950
1950
|
// src/services/worktree-sync.service.ts
|
|
1951
1951
|
import * as fs6 from "fs/promises";
|
|
1952
|
-
import * as
|
|
1952
|
+
import * as path8 from "path";
|
|
1953
1953
|
import pLimit from "p-limit";
|
|
1954
1954
|
import * as lockfile from "proper-lockfile";
|
|
1955
1955
|
|
|
@@ -2179,7 +2179,7 @@ function formatTimingTable(totalDuration, phaseResults, repoName) {
|
|
|
2179
2179
|
|
|
2180
2180
|
// src/services/git.service.ts
|
|
2181
2181
|
import * as fs4 from "fs/promises";
|
|
2182
|
-
import * as
|
|
2182
|
+
import * as path6 from "path";
|
|
2183
2183
|
import simpleGit4 from "simple-git";
|
|
2184
2184
|
|
|
2185
2185
|
// src/utils/worktree-list-parser.ts
|
|
@@ -2323,11 +2323,13 @@ function defaultConsoleOutput(msg, level) {
|
|
|
2323
2323
|
}
|
|
2324
2324
|
|
|
2325
2325
|
// src/services/sparse-checkout.service.ts
|
|
2326
|
+
import * as path3 from "path";
|
|
2326
2327
|
import simpleGit from "simple-git";
|
|
2327
2328
|
var SparseCheckoutService = class {
|
|
2328
2329
|
logger;
|
|
2329
2330
|
gitFactory;
|
|
2330
2331
|
warnedConfigs = /* @__PURE__ */ new WeakSet();
|
|
2332
|
+
matcherCache = /* @__PURE__ */ new WeakMap();
|
|
2331
2333
|
constructor(logger, gitFactory) {
|
|
2332
2334
|
this.logger = logger ?? Logger.createDefault();
|
|
2333
2335
|
this.gitFactory = gitFactory ?? ((p) => simpleGit(p));
|
|
@@ -2411,11 +2413,66 @@ var SparseCheckoutService = class {
|
|
|
2411
2413
|
const bt = b.map((x) => x.trim());
|
|
2412
2414
|
return at.every((v, i) => v === bt[i]);
|
|
2413
2415
|
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Decide whether a list of changed file paths intersects the sparse-checkout
|
|
2418
|
+
* set defined by `cfg`. Used to skip fast-forward updates when upstream
|
|
2419
|
+
* commits only touch files outside the materialized worktree.
|
|
2420
|
+
*
|
|
2421
|
+
* Cone mode materializes:
|
|
2422
|
+
* - all files at the repository root,
|
|
2423
|
+
* - all files directly inside every ancestor of an included directory
|
|
2424
|
+
* (e.g. include `tools/build` keeps `tools/foo.txt` checked out too),
|
|
2425
|
+
* - everything inside an included directory.
|
|
2426
|
+
* We mirror those rules here. Missing the ancestor-files case would let
|
|
2427
|
+
* stale files linger when only those parent files change upstream.
|
|
2428
|
+
*
|
|
2429
|
+
* No-cone mode: gitignore-style matching with negation is non-trivial and
|
|
2430
|
+
* not implemented here yet. We return `true` so the caller falls back to
|
|
2431
|
+
* the safe behavior of always running the update.
|
|
2432
|
+
*
|
|
2433
|
+
* The matcher derived from `cfg` is cached on the cfg object identity
|
|
2434
|
+
* (WeakMap), so callers should reuse the same `cfg` reference across
|
|
2435
|
+
* invocations to benefit from the cache.
|
|
2436
|
+
*/
|
|
2437
|
+
pathsTouchSparse(changedPaths, cfg) {
|
|
2438
|
+
if (changedPaths.length === 0) return false;
|
|
2439
|
+
const matcher = this.getMatcher(cfg);
|
|
2440
|
+
if (matcher.mode === "no-cone") return true;
|
|
2441
|
+
if (matcher.patterns.length === 0) return true;
|
|
2442
|
+
return changedPaths.some((p) => {
|
|
2443
|
+
if (!p.includes("/")) return true;
|
|
2444
|
+
for (const pat of matcher.patterns) {
|
|
2445
|
+
if (p === pat || p.startsWith(pat + "/")) return true;
|
|
2446
|
+
}
|
|
2447
|
+
return matcher.ancestorDirs.has(path3.posix.dirname(p));
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
getMatcher(cfg) {
|
|
2451
|
+
const cached = this.matcherCache.get(cfg);
|
|
2452
|
+
if (cached) return cached;
|
|
2453
|
+
const mode = this.resolveMode(cfg);
|
|
2454
|
+
if (mode === "no-cone") {
|
|
2455
|
+
const matcher2 = { mode, patterns: [], ancestorDirs: /* @__PURE__ */ new Set() };
|
|
2456
|
+
this.matcherCache.set(cfg, matcher2);
|
|
2457
|
+
return matcher2;
|
|
2458
|
+
}
|
|
2459
|
+
const patterns = cfg.include.map((p) => p.trim()).filter((p) => p.length > 0).map((p) => p.endsWith("/") ? p.slice(0, -1) : p);
|
|
2460
|
+
const ancestorDirs = /* @__PURE__ */ new Set();
|
|
2461
|
+
for (const pat of patterns) {
|
|
2462
|
+
const parts = pat.split("/");
|
|
2463
|
+
for (let i = 1; i < parts.length; i++) {
|
|
2464
|
+
ancestorDirs.add(parts.slice(0, i).join("/"));
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
const matcher = { mode, patterns, ancestorDirs };
|
|
2468
|
+
this.matcherCache.set(cfg, matcher);
|
|
2469
|
+
return matcher;
|
|
2470
|
+
}
|
|
2414
2471
|
};
|
|
2415
2472
|
|
|
2416
2473
|
// src/services/worktree-metadata.service.ts
|
|
2417
2474
|
import * as fs2 from "fs/promises";
|
|
2418
|
-
import * as
|
|
2475
|
+
import * as path4 from "path";
|
|
2419
2476
|
import simpleGit2 from "simple-git";
|
|
2420
2477
|
var WorktreeMetadataService = class {
|
|
2421
2478
|
logger;
|
|
@@ -2428,7 +2485,7 @@ var WorktreeMetadataService = class {
|
|
|
2428
2485
|
* For example: /worktrees/fix/test-branch -> test-branch (not fix/test-branch)
|
|
2429
2486
|
*/
|
|
2430
2487
|
getWorktreeDirectoryName(worktreePath) {
|
|
2431
|
-
return
|
|
2488
|
+
return path4.basename(worktreePath);
|
|
2432
2489
|
}
|
|
2433
2490
|
async getMetadataPath(bareRepoPath, worktreeName) {
|
|
2434
2491
|
if (worktreeName.includes("/") || worktreeName.includes("\\")) {
|
|
@@ -2436,7 +2493,7 @@ var WorktreeMetadataService = class {
|
|
|
2436
2493
|
`getMetadataPath requires a filesystem-safe worktree directory name, got '${worktreeName}'. Use getMetadataPathFromWorktreePath when starting from a raw branch name.`
|
|
2437
2494
|
);
|
|
2438
2495
|
}
|
|
2439
|
-
return
|
|
2496
|
+
return path4.join(
|
|
2440
2497
|
bareRepoPath,
|
|
2441
2498
|
METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
|
|
2442
2499
|
worktreeName,
|
|
@@ -2449,7 +2506,7 @@ var WorktreeMetadataService = class {
|
|
|
2449
2506
|
}
|
|
2450
2507
|
async saveMetadata(bareRepoPath, worktreeName, metadata) {
|
|
2451
2508
|
const metadataPath = await this.getMetadataPath(bareRepoPath, worktreeName);
|
|
2452
|
-
await fs2.mkdir(
|
|
2509
|
+
await fs2.mkdir(path4.dirname(metadataPath), { recursive: true });
|
|
2453
2510
|
const tmpPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`;
|
|
2454
2511
|
let renamed = false;
|
|
2455
2512
|
try {
|
|
@@ -2642,7 +2699,7 @@ var WorktreeMetadataService = class {
|
|
|
2642
2699
|
|
|
2643
2700
|
// src/services/worktree-status.service.ts
|
|
2644
2701
|
import * as fs3 from "fs/promises";
|
|
2645
|
-
import * as
|
|
2702
|
+
import * as path5 from "path";
|
|
2646
2703
|
import simpleGit3 from "simple-git";
|
|
2647
2704
|
var OPERATION_FILES = [
|
|
2648
2705
|
{ file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
|
|
@@ -2845,7 +2902,7 @@ var WorktreeStatusService = class {
|
|
|
2845
2902
|
async detectOperationFile(gitDir) {
|
|
2846
2903
|
const results = await Promise.all(
|
|
2847
2904
|
OPERATION_FILES.map(
|
|
2848
|
-
({ file }) => fs3.access(
|
|
2905
|
+
({ file }) => fs3.access(path5.join(gitDir, file)).then(
|
|
2849
2906
|
() => true,
|
|
2850
2907
|
() => false
|
|
2851
2908
|
)
|
|
@@ -2966,14 +3023,14 @@ var WorktreeStatusService = class {
|
|
|
2966
3023
|
}
|
|
2967
3024
|
}
|
|
2968
3025
|
async resolveGitDir(worktreePath) {
|
|
2969
|
-
const gitPath =
|
|
3026
|
+
const gitPath = path5.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
|
|
2970
3027
|
try {
|
|
2971
3028
|
const stat3 = await fs3.stat(gitPath);
|
|
2972
3029
|
if (stat3.isFile()) {
|
|
2973
3030
|
const content = await fs3.readFile(gitPath, "utf-8");
|
|
2974
3031
|
const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
|
|
2975
3032
|
if (gitdirMatch) {
|
|
2976
|
-
return
|
|
3033
|
+
return path5.resolve(worktreePath, gitdirMatch[1].trim());
|
|
2977
3034
|
}
|
|
2978
3035
|
throw new GitOperationError("resolve-git-dir", `Failed to parse gitdir from .git file at ${gitPath}`);
|
|
2979
3036
|
}
|
|
@@ -2987,7 +3044,7 @@ var WorktreeStatusService = class {
|
|
|
2987
3044
|
}
|
|
2988
3045
|
}
|
|
2989
3046
|
createGitInstance(worktreePath) {
|
|
2990
|
-
const key = `${
|
|
3047
|
+
const key = `${path5.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
|
|
2991
3048
|
let git = this.gitInstances.get(key);
|
|
2992
3049
|
if (!git) {
|
|
2993
3050
|
git = this.config.skipLfs ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
@@ -3010,7 +3067,7 @@ var GitService = class {
|
|
|
3010
3067
|
this.config = config;
|
|
3011
3068
|
this.logger = logger ?? Logger.createDefault(void 0, config.debug);
|
|
3012
3069
|
this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
|
|
3013
|
-
this.mainWorktreePath =
|
|
3070
|
+
this.mainWorktreePath = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
|
|
3014
3071
|
this.metadataService = new WorktreeMetadataService(this.logger);
|
|
3015
3072
|
this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
|
|
3016
3073
|
this.sparseCheckoutService = new SparseCheckoutService(this.logger);
|
|
@@ -3038,7 +3095,7 @@ var GitService = class {
|
|
|
3038
3095
|
return this.config.cloneTimeoutMs ?? DEFAULT_CONFIG.CLONE_TIMEOUT_MS;
|
|
3039
3096
|
}
|
|
3040
3097
|
getCachedGit(dirPath, useLfsSkip = false) {
|
|
3041
|
-
const key = `${
|
|
3098
|
+
const key = `${path6.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
|
|
3042
3099
|
let git = this.gitInstances.get(key);
|
|
3043
3100
|
if (!git) {
|
|
3044
3101
|
const block = this.getFetchTimeoutMs();
|
|
@@ -3055,10 +3112,10 @@ var GitService = class {
|
|
|
3055
3112
|
async initialize() {
|
|
3056
3113
|
const { repoUrl } = this.config;
|
|
3057
3114
|
try {
|
|
3058
|
-
await fs4.access(
|
|
3115
|
+
await fs4.access(path6.join(this.bareRepoPath, "HEAD"));
|
|
3059
3116
|
} catch {
|
|
3060
3117
|
this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
|
|
3061
|
-
await fs4.mkdir(
|
|
3118
|
+
await fs4.mkdir(path6.dirname(this.bareRepoPath), { recursive: true });
|
|
3062
3119
|
const cloneBlock = this.getCloneTimeoutMs();
|
|
3063
3120
|
const cloneBase = cloneBlock > 0 ? simpleGit4({ timeout: { block: cloneBlock } }) : simpleGit4();
|
|
3064
3121
|
const cloneGit = this.isLfsSkipEnabled() ? cloneBase.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : cloneBase;
|
|
@@ -3078,17 +3135,17 @@ var GitService = class {
|
|
|
3078
3135
|
this.logger.info("Fetching remote branches...");
|
|
3079
3136
|
await bareGit.fetch(["--all"]);
|
|
3080
3137
|
this.defaultBranch = await this.detectDefaultBranch(bareGit);
|
|
3081
|
-
this.mainWorktreePath =
|
|
3138
|
+
this.mainWorktreePath = path6.join(this.config.worktreeDir, this.defaultBranch);
|
|
3082
3139
|
let needsMainWorktree = true;
|
|
3083
3140
|
try {
|
|
3084
3141
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
3085
|
-
needsMainWorktree = !worktrees.some((w) =>
|
|
3142
|
+
needsMainWorktree = !worktrees.some((w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath));
|
|
3086
3143
|
} catch {
|
|
3087
3144
|
}
|
|
3088
3145
|
if (needsMainWorktree) {
|
|
3089
3146
|
this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
|
|
3090
3147
|
await fs4.mkdir(this.config.worktreeDir, { recursive: true });
|
|
3091
|
-
const absoluteWorktreePath =
|
|
3148
|
+
const absoluteWorktreePath = path6.resolve(this.mainWorktreePath);
|
|
3092
3149
|
const branches = await bareGit.branch();
|
|
3093
3150
|
const defaultBranchExists = branches.all.includes(this.defaultBranch);
|
|
3094
3151
|
const useNoCheckoutMain = !!this.config.sparseCheckout;
|
|
@@ -3124,7 +3181,7 @@ var GitService = class {
|
|
|
3124
3181
|
}
|
|
3125
3182
|
const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
|
|
3126
3183
|
const mainWorktreeRegistered = updatedWorktrees.some(
|
|
3127
|
-
(w) =>
|
|
3184
|
+
(w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath)
|
|
3128
3185
|
);
|
|
3129
3186
|
if (!mainWorktreeRegistered) {
|
|
3130
3187
|
if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
|
|
@@ -3207,7 +3264,7 @@ var GitService = class {
|
|
|
3207
3264
|
const existence = await Promise.all(
|
|
3208
3265
|
lfsFileList.map(async (f) => {
|
|
3209
3266
|
try {
|
|
3210
|
-
await fs4.access(
|
|
3267
|
+
await fs4.access(path6.join(worktreePath, f));
|
|
3211
3268
|
return f;
|
|
3212
3269
|
} catch {
|
|
3213
3270
|
return null;
|
|
@@ -3235,7 +3292,7 @@ var GitService = class {
|
|
|
3235
3292
|
let allDownloaded = true;
|
|
3236
3293
|
const notDownloaded = [];
|
|
3237
3294
|
for (const file of samplesToCheck) {
|
|
3238
|
-
const filePath =
|
|
3295
|
+
const filePath = path6.join(worktreePath, file);
|
|
3239
3296
|
try {
|
|
3240
3297
|
const handle = await fs4.open(filePath, "r");
|
|
3241
3298
|
try {
|
|
@@ -3324,12 +3381,12 @@ var GitService = class {
|
|
|
3324
3381
|
}
|
|
3325
3382
|
async addWorktree(branchName, worktreePath) {
|
|
3326
3383
|
const bareGit = this.getCachedGit(this.bareRepoPath, this.isLfsSkipEnabled());
|
|
3327
|
-
const absoluteWorktreePath =
|
|
3328
|
-
await fs4.mkdir(
|
|
3384
|
+
const absoluteWorktreePath = path6.resolve(worktreePath);
|
|
3385
|
+
await fs4.mkdir(path6.dirname(absoluteWorktreePath), { recursive: true });
|
|
3329
3386
|
try {
|
|
3330
3387
|
await fs4.access(absoluteWorktreePath);
|
|
3331
3388
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
3332
|
-
const isValidWorktree = worktrees.some((w) =>
|
|
3389
|
+
const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
3333
3390
|
if (isValidWorktree) {
|
|
3334
3391
|
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
3335
3392
|
return;
|
|
@@ -3374,7 +3431,7 @@ var GitService = class {
|
|
|
3374
3431
|
}
|
|
3375
3432
|
if (errorMessage.includes("already registered worktree")) {
|
|
3376
3433
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
3377
|
-
const existingWorktree = worktrees.find((w) =>
|
|
3434
|
+
const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
3378
3435
|
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
3379
3436
|
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
|
|
3380
3437
|
return;
|
|
@@ -3420,7 +3477,7 @@ var GitService = class {
|
|
|
3420
3477
|
try {
|
|
3421
3478
|
await fs4.access(absoluteWorktreePath);
|
|
3422
3479
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
3423
|
-
const isValidWorktree = worktrees.some((w) =>
|
|
3480
|
+
const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
3424
3481
|
if (isValidWorktree) {
|
|
3425
3482
|
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
3426
3483
|
return;
|
|
@@ -3450,7 +3507,7 @@ var GitService = class {
|
|
|
3450
3507
|
const fallbackErrorMessage = getErrorMessage(fallbackError);
|
|
3451
3508
|
if (fallbackErrorMessage.includes("already registered worktree")) {
|
|
3452
3509
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
3453
|
-
const existingWorktree = worktrees.find((w) =>
|
|
3510
|
+
const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
3454
3511
|
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
3455
3512
|
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
|
|
3456
3513
|
return;
|
|
@@ -3673,6 +3730,23 @@ var GitService = class {
|
|
|
3673
3730
|
return false;
|
|
3674
3731
|
}
|
|
3675
3732
|
}
|
|
3733
|
+
async getChangedPathsInRange(worktreePath, fromRef, toRef) {
|
|
3734
|
+
const worktreeGit = this.getCachedGit(worktreePath);
|
|
3735
|
+
try {
|
|
3736
|
+
const out = await worktreeGit.raw([
|
|
3737
|
+
"-c",
|
|
3738
|
+
"core.quotePath=false",
|
|
3739
|
+
"diff",
|
|
3740
|
+
"--name-only",
|
|
3741
|
+
"--no-renames",
|
|
3742
|
+
`${fromRef}..${toRef}`
|
|
3743
|
+
]);
|
|
3744
|
+
return out.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0);
|
|
3745
|
+
} catch (error) {
|
|
3746
|
+
this.logger.warn(`Failed to compute diff ${fromRef}..${toRef} in ${worktreePath}: ${getErrorMessage(error)}`);
|
|
3747
|
+
return null;
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3676
3750
|
async compareTreeContent(worktreePath, branch) {
|
|
3677
3751
|
const worktreeGit = this.getCachedGit(worktreePath);
|
|
3678
3752
|
try {
|
|
@@ -3757,7 +3831,7 @@ var GitService = class {
|
|
|
3757
3831
|
// src/services/path-resolution.service.ts
|
|
3758
3832
|
import { createHash } from "crypto";
|
|
3759
3833
|
import * as fs5 from "fs";
|
|
3760
|
-
import * as
|
|
3834
|
+
import * as path7 from "path";
|
|
3761
3835
|
var BRANCH_STEM_MAX = 80;
|
|
3762
3836
|
var BRANCH_HASH_LEN = 8;
|
|
3763
3837
|
var PathResolutionService = class {
|
|
@@ -3767,22 +3841,22 @@ var PathResolutionService = class {
|
|
|
3767
3841
|
return `${stem}-${hash}`;
|
|
3768
3842
|
}
|
|
3769
3843
|
getBranchWorktreePath(worktreeDir, branchName) {
|
|
3770
|
-
return
|
|
3844
|
+
return path7.join(worktreeDir, this.sanitizeBranchName(branchName));
|
|
3771
3845
|
}
|
|
3772
3846
|
resolveRealPath(inputPath) {
|
|
3773
|
-
const absolute =
|
|
3847
|
+
const absolute = path7.resolve(inputPath);
|
|
3774
3848
|
const missing = [];
|
|
3775
3849
|
let current = absolute;
|
|
3776
3850
|
while (!fs5.existsSync(current)) {
|
|
3777
|
-
const parent =
|
|
3851
|
+
const parent = path7.dirname(current);
|
|
3778
3852
|
if (parent === current) {
|
|
3779
3853
|
return absolute;
|
|
3780
3854
|
}
|
|
3781
|
-
missing.unshift(
|
|
3855
|
+
missing.unshift(path7.basename(current));
|
|
3782
3856
|
current = parent;
|
|
3783
3857
|
}
|
|
3784
3858
|
try {
|
|
3785
|
-
return
|
|
3859
|
+
return path7.join(fs5.realpathSync(current), ...missing);
|
|
3786
3860
|
} catch {
|
|
3787
3861
|
return absolute;
|
|
3788
3862
|
}
|
|
@@ -3792,7 +3866,7 @@ var PathResolutionService = class {
|
|
|
3792
3866
|
const a = fold(resolved);
|
|
3793
3867
|
const b = fold(resolvedBase);
|
|
3794
3868
|
if (a === b) return true;
|
|
3795
|
-
return a.length > b.length && a.charAt(b.length) ===
|
|
3869
|
+
return a.length > b.length && a.charAt(b.length) === path7.sep && a.startsWith(b);
|
|
3796
3870
|
}
|
|
3797
3871
|
normalizeWorktreePath(worktreePath, worktreeBaseDir) {
|
|
3798
3872
|
const resolved = this.resolveRealPath(worktreePath);
|
|
@@ -3800,7 +3874,7 @@ var PathResolutionService = class {
|
|
|
3800
3874
|
if (!this.isResolvedPathInsideBase(resolved, resolvedBase)) {
|
|
3801
3875
|
throw new Error(`Worktree path '${worktreePath}' is outside base directory '${worktreeBaseDir}'`);
|
|
3802
3876
|
}
|
|
3803
|
-
return
|
|
3877
|
+
return path7.relative(resolvedBase, resolved);
|
|
3804
3878
|
}
|
|
3805
3879
|
isPathInsideBaseDir(targetPath, baseDir) {
|
|
3806
3880
|
const resolved = this.resolveRealPath(targetPath);
|
|
@@ -3906,7 +3980,7 @@ var WorktreeSyncService = class {
|
|
|
3906
3980
|
};
|
|
3907
3981
|
}
|
|
3908
3982
|
const barePath = this.gitService.getBareRepoPath();
|
|
3909
|
-
const lockTarget =
|
|
3983
|
+
const lockTarget = path8.join(barePath, "HEAD");
|
|
3910
3984
|
try {
|
|
3911
3985
|
await fs6.access(lockTarget);
|
|
3912
3986
|
} catch {
|
|
@@ -4104,12 +4178,12 @@ var WorktreeSyncService = class {
|
|
|
4104
4178
|
}
|
|
4105
4179
|
const reservedPaths = /* @__PURE__ */ new Map();
|
|
4106
4180
|
for (const w of worktrees) {
|
|
4107
|
-
reservedPaths.set(
|
|
4181
|
+
reservedPaths.set(path8.resolve(w.path), w.branch);
|
|
4108
4182
|
}
|
|
4109
4183
|
const plan = [];
|
|
4110
4184
|
for (const branchName of newBranches) {
|
|
4111
4185
|
const worktreePath = this.pathResolution.getBranchWorktreePath(this.config.worktreeDir, branchName);
|
|
4112
|
-
const resolved =
|
|
4186
|
+
const resolved = path8.resolve(worktreePath);
|
|
4113
4187
|
const conflict = reservedPaths.get(resolved);
|
|
4114
4188
|
if (conflict && conflict !== branchName) {
|
|
4115
4189
|
this.logger.error(
|
|
@@ -4314,12 +4388,12 @@ var WorktreeSyncService = class {
|
|
|
4314
4388
|
}
|
|
4315
4389
|
async updateExistingWorktrees(worktrees, remoteBranches) {
|
|
4316
4390
|
this.logger.info("Step 4: Checking for worktrees that need updates...");
|
|
4317
|
-
const divergedDir =
|
|
4391
|
+
const divergedDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
4318
4392
|
try {
|
|
4319
4393
|
const diverged = await fs6.readdir(divergedDir);
|
|
4320
4394
|
if (diverged.length > 0) {
|
|
4321
4395
|
this.logger.info(
|
|
4322
|
-
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${
|
|
4396
|
+
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path8.relative(process.cwd(), divergedDir)}`
|
|
4323
4397
|
);
|
|
4324
4398
|
}
|
|
4325
4399
|
} catch {
|
|
@@ -4349,7 +4423,23 @@ var WorktreeSyncService = class {
|
|
|
4349
4423
|
return { action: "diverged", worktree };
|
|
4350
4424
|
}
|
|
4351
4425
|
const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
|
|
4352
|
-
|
|
4426
|
+
if (!isBehind) return null;
|
|
4427
|
+
const sparseCfg = this.config.sparseCheckout;
|
|
4428
|
+
if (sparseCfg && sparseCfg.skipUpdateWhenOutsideSparse !== false) {
|
|
4429
|
+
const sparseService = this.gitService.getSparseCheckoutService();
|
|
4430
|
+
if (sparseService.resolveMode(sparseCfg) === "cone") {
|
|
4431
|
+
const diff = await this.gitService.getChangedPathsInRange(
|
|
4432
|
+
worktree.path,
|
|
4433
|
+
"HEAD",
|
|
4434
|
+
`origin/${worktree.branch}`
|
|
4435
|
+
);
|
|
4436
|
+
if (diff !== null && !sparseService.pathsTouchSparse(diff, sparseCfg)) {
|
|
4437
|
+
this.logger.info(`\u23ED\uFE0F Skipping '${worktree.branch}' - upstream changes outside sparse paths`);
|
|
4438
|
+
return null;
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
return { action: "update", worktree };
|
|
4353
4443
|
})
|
|
4354
4444
|
)
|
|
4355
4445
|
);
|
|
@@ -4427,13 +4517,13 @@ var WorktreeSyncService = class {
|
|
|
4427
4517
|
}
|
|
4428
4518
|
async cleanupOrphanedDirectories(worktrees) {
|
|
4429
4519
|
try {
|
|
4430
|
-
const worktreeRelativePaths = worktrees.map((w) =>
|
|
4520
|
+
const worktreeRelativePaths = worktrees.map((w) => path8.relative(this.config.worktreeDir, w.path));
|
|
4431
4521
|
const allDirs = await fs6.readdir(this.config.worktreeDir);
|
|
4432
4522
|
const regularDirs = allDirs.filter((dir) => !dir.startsWith("."));
|
|
4433
4523
|
const orphanedDirs = [];
|
|
4434
4524
|
for (const dir of regularDirs) {
|
|
4435
4525
|
const isPartOfWorktree = worktreeRelativePaths.some((worktreePath) => {
|
|
4436
|
-
return worktreePath === dir || worktreePath.startsWith(dir +
|
|
4526
|
+
return worktreePath === dir || worktreePath.startsWith(dir + path8.sep);
|
|
4437
4527
|
});
|
|
4438
4528
|
if (!isPartOfWorktree) {
|
|
4439
4529
|
orphanedDirs.push(dir);
|
|
@@ -4442,7 +4532,7 @@ var WorktreeSyncService = class {
|
|
|
4442
4532
|
if (orphanedDirs.length > 0) {
|
|
4443
4533
|
this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
|
|
4444
4534
|
for (const dir of orphanedDirs) {
|
|
4445
|
-
const dirPath =
|
|
4535
|
+
const dirPath = path8.join(this.config.worktreeDir, dir);
|
|
4446
4536
|
try {
|
|
4447
4537
|
const stat3 = await fs6.stat(dirPath);
|
|
4448
4538
|
if (stat3.isDirectory()) {
|
|
@@ -4476,7 +4566,7 @@ var WorktreeSyncService = class {
|
|
|
4476
4566
|
} else {
|
|
4477
4567
|
this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
|
|
4478
4568
|
const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
|
|
4479
|
-
const relativePath =
|
|
4569
|
+
const relativePath = path8.relative(process.cwd(), divergedPath);
|
|
4480
4570
|
this.logger.info(` Moved to: ${relativePath}`);
|
|
4481
4571
|
this.logger.info(` Your local changes are preserved. To review:`);
|
|
4482
4572
|
this.logger.info(` cd ${relativePath}`);
|
|
@@ -4500,12 +4590,12 @@ var WorktreeSyncService = class {
|
|
|
4500
4590
|
}
|
|
4501
4591
|
}
|
|
4502
4592
|
async divergeWorktree(worktreePath, branchName) {
|
|
4503
|
-
const divergedBaseDir =
|
|
4593
|
+
const divergedBaseDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
4504
4594
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
4505
4595
|
const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
4506
4596
|
const safeBranchName = this.pathResolution.sanitizeBranchName(branchName);
|
|
4507
4597
|
const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
|
|
4508
|
-
const divergedPath =
|
|
4598
|
+
const divergedPath = path8.join(divergedBaseDir, divergedName);
|
|
4509
4599
|
await fs6.mkdir(divergedBaseDir, { recursive: true });
|
|
4510
4600
|
try {
|
|
4511
4601
|
await fs6.rename(worktreePath, divergedPath);
|
|
@@ -4532,7 +4622,7 @@ var WorktreeSyncService = class {
|
|
|
4532
4622
|
Original worktree location: ${worktreePath}`
|
|
4533
4623
|
};
|
|
4534
4624
|
await fs6.writeFile(
|
|
4535
|
-
|
|
4625
|
+
path8.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
|
|
4536
4626
|
JSON.stringify(metadata, null, 2)
|
|
4537
4627
|
);
|
|
4538
4628
|
return divergedPath;
|
|
@@ -4541,7 +4631,7 @@ var WorktreeSyncService = class {
|
|
|
4541
4631
|
|
|
4542
4632
|
// src/services/file-copy.service.ts
|
|
4543
4633
|
import * as fs7 from "fs/promises";
|
|
4544
|
-
import * as
|
|
4634
|
+
import * as path9 from "path";
|
|
4545
4635
|
import { glob } from "glob";
|
|
4546
4636
|
var DEFAULT_IGNORE_PATTERNS = [
|
|
4547
4637
|
"**/node_modules/**",
|
|
@@ -4568,8 +4658,8 @@ var FileCopyService = class {
|
|
|
4568
4658
|
}
|
|
4569
4659
|
const filesToCopy = await this.expandPatterns(sourceDir, patterns);
|
|
4570
4660
|
for (const relativePath of filesToCopy) {
|
|
4571
|
-
const sourcePath =
|
|
4572
|
-
const destPath =
|
|
4661
|
+
const sourcePath = path9.join(sourceDir, relativePath);
|
|
4662
|
+
const destPath = path9.join(destDir, relativePath);
|
|
4573
4663
|
try {
|
|
4574
4664
|
const copied = await this.copyFile(sourcePath, destPath);
|
|
4575
4665
|
if (copied) {
|
|
@@ -4610,7 +4700,7 @@ var FileCopyService = class {
|
|
|
4610
4700
|
return false;
|
|
4611
4701
|
} catch {
|
|
4612
4702
|
}
|
|
4613
|
-
const destDir =
|
|
4703
|
+
const destDir = path9.dirname(destPath);
|
|
4614
4704
|
await fs7.mkdir(destDir, { recursive: true });
|
|
4615
4705
|
await fs7.copyFile(sourcePath, destPath);
|
|
4616
4706
|
return true;
|
|
@@ -4828,6 +4918,8 @@ var AppEventEmitter = class {
|
|
|
4828
4918
|
|
|
4829
4919
|
// src/services/InteractiveUIService.tsx
|
|
4830
4920
|
import * as fs8 from "fs/promises";
|
|
4921
|
+
var WAIT_SYNC_FAST_TIMEOUT_MS = 2e3;
|
|
4922
|
+
var WAIT_SYNC_DEFAULT_TIMEOUT_MS = 3e4;
|
|
4831
4923
|
var InteractiveUIService = class {
|
|
4832
4924
|
app = null;
|
|
4833
4925
|
syncServices;
|
|
@@ -4935,6 +5027,9 @@ var InteractiveUIService = class {
|
|
|
4935
5027
|
}
|
|
4936
5028
|
this.cronJobs = [];
|
|
4937
5029
|
}
|
|
5030
|
+
registerCronJob(job) {
|
|
5031
|
+
this.cronJobs.push(job);
|
|
5032
|
+
}
|
|
4938
5033
|
renderUI() {
|
|
4939
5034
|
if (this.app) {
|
|
4940
5035
|
this.app.unmount();
|
|
@@ -4958,8 +5053,8 @@ var InteractiveUIService = class {
|
|
|
4958
5053
|
getWorktreeStatusForRepo: (index) => this.getWorktreeStatusForRepo(index),
|
|
4959
5054
|
getDivergedDirectoriesForRepo: (index) => this.getDivergedDirectoriesForRepo(index),
|
|
4960
5055
|
deleteDivergedDirectory: (repoIndex, name) => this.deleteDivergedDirectory(repoIndex, name),
|
|
4961
|
-
openEditorInWorktree: (
|
|
4962
|
-
openTerminalInWorktree: (repoIndex,
|
|
5056
|
+
openEditorInWorktree: (path14) => this.openEditorInWorktree(path14),
|
|
5057
|
+
openTerminalInWorktree: (repoIndex, path14, branchName) => this.openTerminalInWorktree(repoIndex, path14, branchName),
|
|
4963
5058
|
copyBranchFiles: (repoIndex, baseBranch, targetBranch) => this.copyBranchFiles(repoIndex, baseBranch, targetBranch),
|
|
4964
5059
|
createWorktreeForBranch: (repoIndex, branchName) => this.createWorktreeForBranch(repoIndex, branchName),
|
|
4965
5060
|
executeOnBranchCreatedHooks: (repoIndex, context) => this.executeOnBranchCreatedHooks(repoIndex, context)
|
|
@@ -5049,18 +5144,17 @@ var InteractiveUIService = class {
|
|
|
5049
5144
|
await this.destroy();
|
|
5050
5145
|
process.exit(0);
|
|
5051
5146
|
}
|
|
5052
|
-
async waitForInProgressSyncs() {
|
|
5147
|
+
async waitForInProgressSyncs(timeoutMs = WAIT_SYNC_DEFAULT_TIMEOUT_MS) {
|
|
5053
5148
|
const inProgressServices = this.syncServices.filter((s) => s.isSyncInProgress());
|
|
5054
5149
|
if (inProgressServices.length === 0) {
|
|
5055
5150
|
return;
|
|
5056
5151
|
}
|
|
5057
5152
|
this.addLog(`Waiting for ${inProgressServices.length} in-progress sync(s) to finish...`, "info");
|
|
5058
5153
|
const syncChecks = inProgressServices.map(async (service) => {
|
|
5059
|
-
const timeout = 3e4;
|
|
5060
5154
|
const checkInterval = 500;
|
|
5061
5155
|
const startTime = Date.now();
|
|
5062
5156
|
while (service.isSyncInProgress()) {
|
|
5063
|
-
if (Date.now() - startTime >
|
|
5157
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
5064
5158
|
throw new Error("Timeout waiting for sync operations to complete");
|
|
5065
5159
|
}
|
|
5066
5160
|
await new Promise((resolve9) => setTimeout(resolve9, checkInterval));
|
|
@@ -5070,7 +5164,7 @@ var InteractiveUIService = class {
|
|
|
5070
5164
|
await Promise.all(syncChecks);
|
|
5071
5165
|
} catch {
|
|
5072
5166
|
this.addLog(
|
|
5073
|
-
|
|
5167
|
+
`Warning: Timeout waiting for sync operations to complete after ${formatDuration2(timeoutMs)}. Proceeding with potential data loss risk.`,
|
|
5074
5168
|
"warn"
|
|
5075
5169
|
);
|
|
5076
5170
|
}
|
|
@@ -5196,7 +5290,7 @@ var InteractiveUIService = class {
|
|
|
5196
5290
|
}
|
|
5197
5291
|
const service = this.syncServices[repoIndex];
|
|
5198
5292
|
const worktreeDir = service.config.worktreeDir;
|
|
5199
|
-
const divergedDir =
|
|
5293
|
+
const divergedDir = path10.join(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
5200
5294
|
let dirEntries;
|
|
5201
5295
|
try {
|
|
5202
5296
|
dirEntries = await fs8.readdir(divergedDir, { withFileTypes: true, encoding: "utf-8" });
|
|
@@ -5206,8 +5300,8 @@ var InteractiveUIService = class {
|
|
|
5206
5300
|
const subdirs = dirEntries.filter((e) => e.isDirectory());
|
|
5207
5301
|
const results = await Promise.allSettled(
|
|
5208
5302
|
subdirs.map(async (entry) => {
|
|
5209
|
-
const fullPath =
|
|
5210
|
-
const infoFilePath =
|
|
5303
|
+
const fullPath = path10.join(divergedDir, entry.name);
|
|
5304
|
+
const infoFilePath = path10.join(fullPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE);
|
|
5211
5305
|
let originalBranch = entry.name;
|
|
5212
5306
|
let divergedAt = "";
|
|
5213
5307
|
try {
|
|
@@ -5242,11 +5336,11 @@ var InteractiveUIService = class {
|
|
|
5242
5336
|
}
|
|
5243
5337
|
const service = this.syncServices[repoIndex];
|
|
5244
5338
|
const worktreeDir = service.config.worktreeDir;
|
|
5245
|
-
const divergedBase =
|
|
5339
|
+
const divergedBase = path10.resolve(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
5246
5340
|
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
|
|
5247
5341
|
throw new Error(`Invalid diverged directory name: "${name}"`);
|
|
5248
5342
|
}
|
|
5249
|
-
const targetPath =
|
|
5343
|
+
const targetPath = path10.join(divergedBase, name);
|
|
5250
5344
|
if (!this.pathResolution.isPathInsideBaseDir(targetPath, divergedBase)) {
|
|
5251
5345
|
throw new Error(`Path traversal rejected: "${name}" resolves outside the diverged directory`);
|
|
5252
5346
|
}
|
|
@@ -5485,11 +5579,11 @@ var InteractiveUIService = class {
|
|
|
5485
5579
|
this.addLog(`Failed to copy files to new branch: ${error}`, "error");
|
|
5486
5580
|
}
|
|
5487
5581
|
}
|
|
5488
|
-
async destroy() {
|
|
5582
|
+
async destroy(fast = false) {
|
|
5489
5583
|
this.isDestroyed = true;
|
|
5490
5584
|
this.cancelCronJobs();
|
|
5491
5585
|
try {
|
|
5492
|
-
await this.waitForInProgressSyncs();
|
|
5586
|
+
await this.waitForInProgressSyncs(fast ? WAIT_SYNC_FAST_TIMEOUT_MS : WAIT_SYNC_DEFAULT_TIMEOUT_MS);
|
|
5493
5587
|
} catch {
|
|
5494
5588
|
}
|
|
5495
5589
|
this.hookExecutionService.cleanup();
|
|
@@ -5635,7 +5729,7 @@ function reconstructCliCommand(config) {
|
|
|
5635
5729
|
|
|
5636
5730
|
// src/utils/config-generator.ts
|
|
5637
5731
|
import * as fs9 from "fs/promises";
|
|
5638
|
-
import * as
|
|
5732
|
+
import * as path11 from "path";
|
|
5639
5733
|
function serializeToESM(obj, indent = 0) {
|
|
5640
5734
|
const spaces = " ".repeat(indent);
|
|
5641
5735
|
const innerSpaces = " ".repeat(indent + 2);
|
|
@@ -5665,9 +5759,9 @@ ${spaces}}`;
|
|
|
5665
5759
|
return String(obj);
|
|
5666
5760
|
}
|
|
5667
5761
|
async function generateConfigFile(config, configPath) {
|
|
5668
|
-
const configDir =
|
|
5762
|
+
const configDir = path11.dirname(configPath);
|
|
5669
5763
|
await fs9.mkdir(configDir, { recursive: true });
|
|
5670
|
-
const worktreeDirRelative =
|
|
5764
|
+
const worktreeDirRelative = path11.relative(configDir, config.worktreeDir);
|
|
5671
5765
|
const useRelativeWorktree = !worktreeDirRelative.startsWith("../../../");
|
|
5672
5766
|
const repoName = extractRepoNameFromUrl(config.repoUrl);
|
|
5673
5767
|
const repository = {
|
|
@@ -5676,7 +5770,7 @@ async function generateConfigFile(config, configPath) {
|
|
|
5676
5770
|
worktreeDir: useRelativeWorktree ? `./${worktreeDirRelative}` : config.worktreeDir
|
|
5677
5771
|
};
|
|
5678
5772
|
if (config.bareRepoDir) {
|
|
5679
|
-
const bareRepoDirRelative =
|
|
5773
|
+
const bareRepoDirRelative = path11.relative(configDir, config.bareRepoDir);
|
|
5680
5774
|
const useRelativeBare = !bareRepoDirRelative.startsWith("../../../");
|
|
5681
5775
|
repository.bareRepoDir = useRelativeBare ? `./${bareRepoDirRelative}` : config.bareRepoDir;
|
|
5682
5776
|
}
|
|
@@ -5697,11 +5791,11 @@ export default ${serializeToESM(configObject)};
|
|
|
5697
5791
|
await fs9.writeFile(configPath, configContent, "utf-8");
|
|
5698
5792
|
}
|
|
5699
5793
|
function getDefaultConfigPath() {
|
|
5700
|
-
return
|
|
5794
|
+
return path11.join(process.cwd(), "sync-worktrees.config.js");
|
|
5701
5795
|
}
|
|
5702
5796
|
async function findConfigInCwd(cwd = process.cwd()) {
|
|
5703
5797
|
for (const name of CONFIG_FILE_NAMES) {
|
|
5704
|
-
const full =
|
|
5798
|
+
const full = path11.join(cwd, name);
|
|
5705
5799
|
try {
|
|
5706
5800
|
await fs9.access(full);
|
|
5707
5801
|
return full;
|
|
@@ -5712,7 +5806,7 @@ async function findConfigInCwd(cwd = process.cwd()) {
|
|
|
5712
5806
|
}
|
|
5713
5807
|
|
|
5714
5808
|
// src/utils/interactive.ts
|
|
5715
|
-
import * as
|
|
5809
|
+
import * as path12 from "path";
|
|
5716
5810
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
5717
5811
|
async function promptForConfig(partialConfig) {
|
|
5718
5812
|
console.log("\u{1F527} Welcome to sync-worktrees interactive setup!\n");
|
|
@@ -5752,8 +5846,8 @@ async function promptForConfig(partialConfig) {
|
|
|
5752
5846
|
if (!worktreeDir.trim() && defaultWorktreeDir) {
|
|
5753
5847
|
worktreeDir = defaultWorktreeDir;
|
|
5754
5848
|
}
|
|
5755
|
-
if (!
|
|
5756
|
-
worktreeDir =
|
|
5849
|
+
if (!path12.isAbsolute(worktreeDir)) {
|
|
5850
|
+
worktreeDir = path12.resolve(worktreeDir);
|
|
5757
5851
|
}
|
|
5758
5852
|
}
|
|
5759
5853
|
let bareRepoDir = partialConfig.bareRepoDir;
|
|
@@ -5772,8 +5866,8 @@ async function promptForConfig(partialConfig) {
|
|
|
5772
5866
|
return true;
|
|
5773
5867
|
}
|
|
5774
5868
|
});
|
|
5775
|
-
if (!
|
|
5776
|
-
bareRepoDir =
|
|
5869
|
+
if (!path12.isAbsolute(bareRepoDir)) {
|
|
5870
|
+
bareRepoDir = path12.resolve(bareRepoDir);
|
|
5777
5871
|
}
|
|
5778
5872
|
}
|
|
5779
5873
|
let runOnce = partialConfig.runOnce;
|
|
@@ -5845,8 +5939,8 @@ async function promptForConfig(partialConfig) {
|
|
|
5845
5939
|
return true;
|
|
5846
5940
|
}
|
|
5847
5941
|
});
|
|
5848
|
-
if (!
|
|
5849
|
-
configPath =
|
|
5942
|
+
if (!path12.isAbsolute(configPath)) {
|
|
5943
|
+
configPath = path12.resolve(configPath);
|
|
5850
5944
|
}
|
|
5851
5945
|
try {
|
|
5852
5946
|
await generateConfigFile(finalConfig, configPath);
|
|
@@ -5864,26 +5958,55 @@ async function promptForConfig(partialConfig) {
|
|
|
5864
5958
|
return { config: finalConfig, savedConfigPath };
|
|
5865
5959
|
}
|
|
5866
5960
|
|
|
5867
|
-
// src/
|
|
5868
|
-
var
|
|
5869
|
-
function setupSignalHandlers() {
|
|
5870
|
-
|
|
5871
|
-
const
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5961
|
+
// src/utils/signal-handlers.ts
|
|
5962
|
+
var DEFAULT_FORCE_EXIT_MS = 3e3;
|
|
5963
|
+
function setupSignalHandlers(options = {}) {
|
|
5964
|
+
const forceExitMs = options.forceExitMs ?? DEFAULT_FORCE_EXIT_MS;
|
|
5965
|
+
const log = options.log ?? ((msg) => console.log(msg));
|
|
5966
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
5967
|
+
const target = options.process ?? process;
|
|
5968
|
+
const cleanupFns = [];
|
|
5969
|
+
let signalCount = 0;
|
|
5970
|
+
const handler = (signal) => {
|
|
5971
|
+
signalCount += 1;
|
|
5972
|
+
if (signalCount >= 2) {
|
|
5973
|
+
log(`
|
|
5974
|
+
Received second ${signal}, forcing exit.`);
|
|
5975
|
+
exit(130);
|
|
5976
|
+
return;
|
|
5977
|
+
}
|
|
5978
|
+
log(`
|
|
5979
|
+
Received ${signal}, shutting down (Ctrl+C again to force exit)...`);
|
|
5980
|
+
const watchdog = setTimeout(() => {
|
|
5981
|
+
log(`
|
|
5982
|
+
Shutdown took longer than ${forceExitMs}ms, forcing exit.`);
|
|
5983
|
+
exit(130);
|
|
5984
|
+
}, forceExitMs);
|
|
5985
|
+
if (typeof watchdog.unref === "function") {
|
|
5986
|
+
watchdog.unref();
|
|
5987
|
+
}
|
|
5988
|
+
void Promise.allSettled(cleanupFns.map((fn) => Promise.resolve().then(() => fn(true)))).then(() => {
|
|
5989
|
+
clearTimeout(watchdog);
|
|
5990
|
+
exit(0);
|
|
5991
|
+
});
|
|
5992
|
+
};
|
|
5993
|
+
const sigintListener = () => handler("SIGINT");
|
|
5994
|
+
const sigtermListener = () => handler("SIGTERM");
|
|
5995
|
+
target.on("SIGINT", sigintListener);
|
|
5996
|
+
target.on("SIGTERM", sigtermListener);
|
|
5997
|
+
return {
|
|
5998
|
+
register: (fn) => {
|
|
5999
|
+
cleanupFns.push(fn);
|
|
6000
|
+
},
|
|
6001
|
+
dispose: () => {
|
|
6002
|
+
target.removeListener("SIGINT", sigintListener);
|
|
6003
|
+
target.removeListener("SIGTERM", sigtermListener);
|
|
5881
6004
|
}
|
|
5882
|
-
process.exit(0);
|
|
5883
6005
|
};
|
|
5884
|
-
process.on("SIGINT", () => void handler("SIGINT"));
|
|
5885
|
-
process.on("SIGTERM", () => void handler("SIGTERM"));
|
|
5886
6006
|
}
|
|
6007
|
+
|
|
6008
|
+
// src/index.ts
|
|
6009
|
+
var signalHandle = setupSignalHandlers();
|
|
5887
6010
|
async function runSingleRepository(config) {
|
|
5888
6011
|
const logger = Logger.createDefault(void 0, config.debug);
|
|
5889
6012
|
logger.info("\n\u{1F4CB} CLI Command (for future reference):");
|
|
@@ -5900,11 +6023,11 @@ async function runSingleRepository(config) {
|
|
|
5900
6023
|
await syncService.sync();
|
|
5901
6024
|
} else {
|
|
5902
6025
|
const uiService = new InteractiveUIService([syncService], void 0, config.cronSchedule);
|
|
5903
|
-
|
|
6026
|
+
signalHandle.register((fast) => uiService.destroy(fast));
|
|
5904
6027
|
await syncService.sync();
|
|
5905
6028
|
uiService.updateLastSyncTime();
|
|
5906
6029
|
void uiService.calculateAndUpdateDiskSpace();
|
|
5907
|
-
cron3.schedule(config.cronSchedule, async () => {
|
|
6030
|
+
const job = cron3.schedule(config.cronSchedule, async () => {
|
|
5908
6031
|
try {
|
|
5909
6032
|
uiService.setStatus("syncing");
|
|
5910
6033
|
await syncService.sync();
|
|
@@ -5915,6 +6038,7 @@ async function runSingleRepository(config) {
|
|
|
5915
6038
|
uiService.setStatus("idle");
|
|
5916
6039
|
}
|
|
5917
6040
|
});
|
|
6041
|
+
uiService.registerCronJob(job);
|
|
5918
6042
|
}
|
|
5919
6043
|
} catch (error) {
|
|
5920
6044
|
logger.error("\u274C Fatal Error during initialization:", error);
|
|
@@ -5981,7 +6105,7 @@ async function runMultipleRepositories(repositories, runOnce, configPath, maxPar
|
|
|
5981
6105
|
const displaySchedule = uniqueSchedules.length === 1 ? uniqueSchedules[0] : void 0;
|
|
5982
6106
|
const allServices = Array.from(services.values());
|
|
5983
6107
|
const uiService = new InteractiveUIService(allServices, configPath, displaySchedule, maxParallel, reloadOptions);
|
|
5984
|
-
|
|
6108
|
+
signalHandle.register((fast) => uiService.destroy(fast));
|
|
5985
6109
|
void uiService.calculateAndUpdateDiskSpace();
|
|
5986
6110
|
uiService.setupCronJobs();
|
|
5987
6111
|
uiService.addLog(`\u{1F4CB} ${repositories.length} repositories configured`);
|
|
@@ -6076,13 +6200,12 @@ async function runInteractive(partial, options) {
|
|
|
6076
6200
|
await runSingleRepository(config);
|
|
6077
6201
|
}
|
|
6078
6202
|
async function main() {
|
|
6079
|
-
setupSignalHandlers();
|
|
6080
6203
|
const options = parseArguments();
|
|
6081
6204
|
if (!options.config && !options.repoUrl && !options.worktreeDir) {
|
|
6082
6205
|
const discovered = await findConfigInCwd();
|
|
6083
6206
|
if (discovered) {
|
|
6084
6207
|
options.config = discovered;
|
|
6085
|
-
console.log(`\u{1F4C4} Using config: ${
|
|
6208
|
+
console.log(`\u{1F4C4} Using config: ${path13.relative(process.cwd(), discovered)}`);
|
|
6086
6209
|
}
|
|
6087
6210
|
}
|
|
6088
6211
|
if (options.config) {
|