sync-worktrees 2.1.0 → 2.2.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 +1161 -148
- package/dist/index.js.map +4 -4
- package/package.json +2 -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 path10 from "path";
|
|
5
5
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
6
6
|
import * as cron2 from "node-cron";
|
|
7
7
|
import pLimit2 from "p-limit";
|
|
@@ -163,6 +163,9 @@ var ConfigLoaderService = class {
|
|
|
163
163
|
if (repoObj.runOnce !== void 0 && typeof repoObj.runOnce !== "boolean") {
|
|
164
164
|
throw new Error(`Repository '${repoObj.name}' has invalid 'runOnce' property`);
|
|
165
165
|
}
|
|
166
|
+
if (repoObj.filesToCopyOnBranchCreate !== void 0) {
|
|
167
|
+
this.validateFilesToCopyConfig(repoObj.filesToCopyOnBranchCreate, `Repository '${repoObj.name}'`);
|
|
168
|
+
}
|
|
166
169
|
});
|
|
167
170
|
if (configObj.defaults) {
|
|
168
171
|
if (typeof configObj.defaults !== "object") {
|
|
@@ -178,6 +181,9 @@ var ConfigLoaderService = class {
|
|
|
178
181
|
if (defaults.retry !== void 0 && typeof defaults.retry !== "object") {
|
|
179
182
|
throw new Error("Invalid 'retry' in defaults");
|
|
180
183
|
}
|
|
184
|
+
if (defaults.filesToCopyOnBranchCreate !== void 0) {
|
|
185
|
+
this.validateFilesToCopyConfig(defaults.filesToCopyOnBranchCreate, "defaults");
|
|
186
|
+
}
|
|
181
187
|
}
|
|
182
188
|
if (configObj.retry !== void 0) {
|
|
183
189
|
if (typeof configObj.retry !== "object") {
|
|
@@ -258,6 +264,19 @@ var ConfigLoaderService = class {
|
|
|
258
264
|
);
|
|
259
265
|
}
|
|
260
266
|
}
|
|
267
|
+
validateFilesToCopyConfig(filesToCopy, context) {
|
|
268
|
+
if (!Array.isArray(filesToCopy)) {
|
|
269
|
+
throw new Error(`'filesToCopyOnBranchCreate' in ${context} must be an array`);
|
|
270
|
+
}
|
|
271
|
+
for (let i = 0; i < filesToCopy.length; i++) {
|
|
272
|
+
const pattern = filesToCopy[i];
|
|
273
|
+
if (typeof pattern !== "string" || pattern.trim() === "") {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`'filesToCopyOnBranchCreate' in ${context} must contain only non-empty strings (invalid at index ${i})`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
261
280
|
resolveRepositoryConfig(repo, defaults, configDir, globalRetry) {
|
|
262
281
|
const resolved = {
|
|
263
282
|
name: repo.name,
|
|
@@ -291,6 +310,9 @@ var ConfigLoaderService = class {
|
|
|
291
310
|
if (repo.updateExistingWorktrees !== void 0 || defaults?.updateExistingWorktrees !== void 0) {
|
|
292
311
|
resolved.updateExistingWorktrees = repo.updateExistingWorktrees ?? defaults?.updateExistingWorktrees ?? true;
|
|
293
312
|
}
|
|
313
|
+
if (repo.filesToCopyOnBranchCreate || defaults?.filesToCopyOnBranchCreate) {
|
|
314
|
+
resolved.filesToCopyOnBranchCreate = repo.filesToCopyOnBranchCreate ?? defaults?.filesToCopyOnBranchCreate;
|
|
315
|
+
}
|
|
294
316
|
return resolved;
|
|
295
317
|
}
|
|
296
318
|
resolvePath(inputPath, baseDir) {
|
|
@@ -317,13 +339,15 @@ var ConfigLoaderService = class {
|
|
|
317
339
|
};
|
|
318
340
|
|
|
319
341
|
// src/services/InteractiveUIService.tsx
|
|
320
|
-
import
|
|
342
|
+
import React7 from "react";
|
|
343
|
+
import * as path7 from "path";
|
|
321
344
|
import { render } from "ink";
|
|
322
345
|
import * as cron from "node-cron";
|
|
346
|
+
import { spawn } from "child_process";
|
|
323
347
|
|
|
324
348
|
// src/components/App.tsx
|
|
325
|
-
import
|
|
326
|
-
import { Box as
|
|
349
|
+
import React6, { useState as useState5, useEffect as useEffect5, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
350
|
+
import { Box as Box6, useInput as useInput5, useStdout } from "ink";
|
|
327
351
|
|
|
328
352
|
// src/components/StatusBar.tsx
|
|
329
353
|
import React, { useState, useEffect } from "react";
|
|
@@ -359,7 +383,7 @@ var StatusBar = ({ status, repositoryCount, lastSyncTime, cronSchedule, diskSpac
|
|
|
359
383
|
const getStatusIcon = () => {
|
|
360
384
|
return status === "syncing" ? "\u27F3" : "\u2713";
|
|
361
385
|
};
|
|
362
|
-
return /* @__PURE__ */ React.createElement(Box, { borderStyle: "single", paddingX: 1 }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: "100%" }, /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, { bold: true }, getStatusIcon(), " Status:", " ", /* @__PURE__ */ React.createElement(Text, { color: getStatusColor() }, status === "syncing" ? "Syncing..." : "Running")), /* @__PURE__ */ React.createElement(Text, null, "Repositories: ", /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, repositoryCount))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Last Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(lastSyncTime))), cronSchedule && /* @__PURE__ */ React.createElement(Text, null, "Next Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(nextSyncTime)))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Disk Space: ", /* @__PURE__ */ React.createElement(Text, { color: "magenta" }, diskSpaceUsed || "Calculating...")))));
|
|
386
|
+
return /* @__PURE__ */ React.createElement(Box, { borderStyle: "single", paddingX: 1 }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: "100%" }, /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, { bold: true }, getStatusIcon(), " Status:", " ", /* @__PURE__ */ React.createElement(Text, { color: getStatusColor() }, status === "syncing" ? "Syncing..." : "Running")), /* @__PURE__ */ React.createElement(Text, null, "Repositories: ", /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, repositoryCount))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Last Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(lastSyncTime))), cronSchedule && /* @__PURE__ */ React.createElement(Text, null, "Next Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(nextSyncTime)))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Disk Space: ", /* @__PURE__ */ React.createElement(Text, { color: "magenta" }, diskSpaceUsed || "Calculating...")), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "s"), "ync", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "c"), "reate", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "o"), "pen", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "r"), "eload", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "?"), "help", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "q"), "uit"))));
|
|
363
387
|
};
|
|
364
388
|
var StatusBar_default = StatusBar;
|
|
365
389
|
|
|
@@ -370,27 +394,632 @@ var HelpModal = ({ onClose }) => {
|
|
|
370
394
|
useInput(() => {
|
|
371
395
|
onClose();
|
|
372
396
|
});
|
|
373
|
-
return /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", alignItems: "center", flexDirection: "column", marginTop: 2, marginBottom: 2 }, /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u{1F333} sync-worktrees - Keyboard Shortcuts")), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "
|
|
397
|
+
return /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", alignItems: "center", flexDirection: "column", marginTop: 2, marginBottom: 2 }, /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u{1F333} sync-worktrees - Keyboard Shortcuts")), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green", dimColor: true }, "Navigation"), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "j"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u2193")), /* @__PURE__ */ React2.createElement(Text2, null, "Scroll down one line")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "k"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u2191")), /* @__PURE__ */ React2.createElement(Text2, null, "Scroll up one line")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "gg")), /* @__PURE__ */ React2.createElement(Text2, null, "Jump to top")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "G")), /* @__PURE__ */ React2.createElement(Text2, null, "Jump to bottom (re-enables auto-scroll)")), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green", dimColor: true }, "Actions")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "s")), /* @__PURE__ */ React2.createElement(Text2, null, "Manually trigger sync for all repositories")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "c")), /* @__PURE__ */ React2.createElement(Text2, null, "Create a new branch")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "o")), /* @__PURE__ */ React2.createElement(Text2, null, "Open editor in worktree")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "r")), /* @__PURE__ */ React2.createElement(Text2, null, "Reload configuration and re-sync all repos")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "?"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "h")), /* @__PURE__ */ React2.createElement(Text2, null, "Toggle this help screen")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "q"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "Esc")), /* @__PURE__ */ React2.createElement(Text2, null, "Gracefully quit"))), /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Press any key to close"))));
|
|
374
398
|
};
|
|
375
399
|
var HelpModal_default = HelpModal;
|
|
376
400
|
|
|
377
|
-
// src/components/
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
401
|
+
// src/components/BranchCreationWizard.tsx
|
|
402
|
+
import React3, { useState as useState2, useEffect as useEffect2, useCallback } from "react";
|
|
403
|
+
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
404
|
+
var isValidGitBranchName = (name) => {
|
|
405
|
+
if (!name.trim()) {
|
|
406
|
+
return { valid: false, error: "Branch name cannot be empty" };
|
|
407
|
+
}
|
|
408
|
+
if (name.startsWith("-")) {
|
|
409
|
+
return { valid: false, error: "Branch name cannot start with '-'" };
|
|
410
|
+
}
|
|
411
|
+
if (name.endsWith(".lock")) {
|
|
412
|
+
return { valid: false, error: "Branch name cannot end with '.lock'" };
|
|
413
|
+
}
|
|
414
|
+
if (name.includes("..")) {
|
|
415
|
+
return { valid: false, error: "Branch name cannot contain '..'" };
|
|
416
|
+
}
|
|
417
|
+
if (name.includes("@{")) {
|
|
418
|
+
return { valid: false, error: "Branch name cannot contain '@{'" };
|
|
419
|
+
}
|
|
420
|
+
if (name.startsWith(".") || name.endsWith(".")) {
|
|
421
|
+
return { valid: false, error: "Branch name cannot start or end with '.'" };
|
|
422
|
+
}
|
|
423
|
+
if (name.includes("//")) {
|
|
424
|
+
return { valid: false, error: "Branch name cannot contain consecutive slashes" };
|
|
425
|
+
}
|
|
426
|
+
if (/[\x00-\x1f\x7f~^:?*\[\\]/.test(name)) {
|
|
427
|
+
return { valid: false, error: "Branch name contains invalid characters" };
|
|
428
|
+
}
|
|
429
|
+
return { valid: true };
|
|
430
|
+
};
|
|
431
|
+
var BranchCreationWizard = ({
|
|
432
|
+
repositories,
|
|
433
|
+
getBranchesForRepo,
|
|
434
|
+
getDefaultBranchForRepo,
|
|
435
|
+
createAndPushBranch,
|
|
436
|
+
onClose,
|
|
437
|
+
onComplete
|
|
438
|
+
}) => {
|
|
439
|
+
const [step, setStep] = useState2(repositories.length > 1 ? "SELECT_PROJECT" : "SELECT_BRANCH");
|
|
440
|
+
const [selectedProjectIndex, setSelectedProjectIndex] = useState2(repositories.length === 1 ? 0 : 0);
|
|
441
|
+
const [branches, setBranches] = useState2([]);
|
|
442
|
+
const [defaultBranch, setDefaultBranch] = useState2("");
|
|
443
|
+
const [selectedBranchIndex, setSelectedBranchIndex] = useState2(0);
|
|
444
|
+
const [branchName, setBranchName] = useState2("");
|
|
445
|
+
const [existingSuffix, setExistingSuffix] = useState2(null);
|
|
446
|
+
const [validationError, setValidationError] = useState2(null);
|
|
447
|
+
const [result, setResult] = useState2(null);
|
|
448
|
+
const [loading, setLoading] = useState2(false);
|
|
449
|
+
const loadBranches = useCallback(
|
|
450
|
+
async (repoIndex) => {
|
|
451
|
+
setLoading(true);
|
|
452
|
+
try {
|
|
453
|
+
const branchList = await getBranchesForRepo(repoIndex);
|
|
454
|
+
const defaultBr = getDefaultBranchForRepo(repoIndex);
|
|
455
|
+
setBranches(branchList);
|
|
456
|
+
setDefaultBranch(defaultBr);
|
|
457
|
+
const defaultIndex = branchList.indexOf(defaultBr);
|
|
458
|
+
setSelectedBranchIndex(defaultIndex >= 0 ? defaultIndex : 0);
|
|
459
|
+
} catch {
|
|
460
|
+
setBranches([]);
|
|
461
|
+
}
|
|
462
|
+
setLoading(false);
|
|
463
|
+
},
|
|
464
|
+
[getBranchesForRepo, getDefaultBranchForRepo]
|
|
465
|
+
);
|
|
466
|
+
const checkBranchExists = useCallback(
|
|
467
|
+
(name) => {
|
|
468
|
+
if (!name.trim()) {
|
|
469
|
+
setExistingSuffix(null);
|
|
470
|
+
setValidationError(null);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const validation = isValidGitBranchName(name);
|
|
474
|
+
if (!validation.valid) {
|
|
475
|
+
setValidationError(validation.error ?? null);
|
|
476
|
+
setExistingSuffix(null);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
setValidationError(null);
|
|
480
|
+
let suffix = 0;
|
|
481
|
+
let testName = name;
|
|
482
|
+
while (branches.includes(testName)) {
|
|
483
|
+
suffix++;
|
|
484
|
+
testName = `${name}-${suffix}`;
|
|
485
|
+
}
|
|
486
|
+
setExistingSuffix(suffix > 0 ? suffix : null);
|
|
487
|
+
},
|
|
488
|
+
[branches]
|
|
489
|
+
);
|
|
490
|
+
useEffect2(() => {
|
|
491
|
+
if (step === "SELECT_BRANCH" && branches.length === 0 && !loading) {
|
|
492
|
+
loadBranches(selectedProjectIndex);
|
|
493
|
+
}
|
|
494
|
+
}, [step, selectedProjectIndex, branches.length, loading, loadBranches]);
|
|
495
|
+
useEffect2(() => {
|
|
496
|
+
if (step === "ENTER_NAME") {
|
|
497
|
+
checkBranchExists(branchName);
|
|
498
|
+
}
|
|
499
|
+
}, [branchName, step, checkBranchExists]);
|
|
500
|
+
const handleCreateBranch = async () => {
|
|
501
|
+
const trimmedName = branchName.trim();
|
|
502
|
+
if (!trimmedName) return;
|
|
503
|
+
const validation = isValidGitBranchName(trimmedName);
|
|
504
|
+
if (!validation.valid) {
|
|
505
|
+
setValidationError(validation.error ?? null);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
setStep("CREATING");
|
|
509
|
+
const baseBranch = branches[selectedBranchIndex];
|
|
510
|
+
const createResult = await createAndPushBranch(selectedProjectIndex, baseBranch, trimmedName);
|
|
511
|
+
setResult(createResult);
|
|
512
|
+
setStep("RESULT");
|
|
513
|
+
};
|
|
383
514
|
useInput2((input2, key) => {
|
|
515
|
+
if (step === "CREATING") return;
|
|
516
|
+
if (key.escape) {
|
|
517
|
+
if (step === "SELECT_PROJECT") {
|
|
518
|
+
onClose();
|
|
519
|
+
} else if (step === "SELECT_BRANCH") {
|
|
520
|
+
if (repositories.length > 1) {
|
|
521
|
+
setBranches([]);
|
|
522
|
+
setStep("SELECT_PROJECT");
|
|
523
|
+
} else {
|
|
524
|
+
onClose();
|
|
525
|
+
}
|
|
526
|
+
} else if (step === "ENTER_NAME") {
|
|
527
|
+
setBranchName("");
|
|
528
|
+
setExistingSuffix(null);
|
|
529
|
+
setStep("SELECT_BRANCH");
|
|
530
|
+
} else if (step === "RESULT") {
|
|
531
|
+
const context = result?.success ? {
|
|
532
|
+
repoIndex: selectedProjectIndex,
|
|
533
|
+
baseBranch: branches[selectedBranchIndex],
|
|
534
|
+
newBranch: result.finalName
|
|
535
|
+
} : void 0;
|
|
536
|
+
onComplete(result?.success ?? false, context);
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (step === "SELECT_PROJECT") {
|
|
541
|
+
if (key.upArrow) {
|
|
542
|
+
setSelectedProjectIndex((prev) => Math.max(0, prev - 1));
|
|
543
|
+
} else if (key.downArrow) {
|
|
544
|
+
setSelectedProjectIndex((prev) => Math.min(repositories.length - 1, prev + 1));
|
|
545
|
+
} else if (key.return) {
|
|
546
|
+
loadBranches(selectedProjectIndex);
|
|
547
|
+
setStep("SELECT_BRANCH");
|
|
548
|
+
}
|
|
549
|
+
} else if (step === "SELECT_BRANCH") {
|
|
550
|
+
if (key.upArrow) {
|
|
551
|
+
setSelectedBranchIndex((prev) => Math.max(0, prev - 1));
|
|
552
|
+
} else if (key.downArrow) {
|
|
553
|
+
setSelectedBranchIndex((prev) => Math.min(branches.length - 1, prev + 1));
|
|
554
|
+
} else if (key.return && branches.length > 0) {
|
|
555
|
+
setStep("ENTER_NAME");
|
|
556
|
+
}
|
|
557
|
+
} else if (step === "ENTER_NAME") {
|
|
558
|
+
if (key.return && branchName.trim()) {
|
|
559
|
+
void handleCreateBranch();
|
|
560
|
+
} else if (key.backspace || key.delete) {
|
|
561
|
+
setBranchName((prev) => prev.slice(0, -1));
|
|
562
|
+
} else if (input2 && !key.ctrl && !key.meta) {
|
|
563
|
+
const validChar = /^[a-zA-Z0-9/_-]$/.test(input2);
|
|
564
|
+
if (validChar) {
|
|
565
|
+
setBranchName((prev) => prev + input2);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
} else if (step === "RESULT") {
|
|
569
|
+
const context = result?.success ? {
|
|
570
|
+
repoIndex: selectedProjectIndex,
|
|
571
|
+
baseBranch: branches[selectedBranchIndex],
|
|
572
|
+
newBranch: result.finalName
|
|
573
|
+
} : void 0;
|
|
574
|
+
onComplete(result?.success ?? false, context);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
const getStepNumber = () => {
|
|
578
|
+
if (repositories.length === 1) {
|
|
579
|
+
if (step === "SELECT_BRANCH") return 1;
|
|
580
|
+
if (step === "ENTER_NAME") return 2;
|
|
581
|
+
return 2;
|
|
582
|
+
}
|
|
583
|
+
if (step === "SELECT_PROJECT") return 1;
|
|
584
|
+
if (step === "SELECT_BRANCH") return 2;
|
|
585
|
+
if (step === "ENTER_NAME") return 3;
|
|
586
|
+
return 3;
|
|
587
|
+
};
|
|
588
|
+
const getTotalSteps = () => repositories.length === 1 ? 2 : 3;
|
|
589
|
+
const renderProjectSelection = () => /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Select repository:"), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, repositories.map((repo, idx) => /* @__PURE__ */ React3.createElement(Box3, { key: repo.index }, /* @__PURE__ */ React3.createElement(Text3, { color: idx === selectedProjectIndex ? "cyan" : void 0 }, idx === selectedProjectIndex ? "> " : " ", repo.name)))));
|
|
590
|
+
const renderBranchSelection = () => {
|
|
591
|
+
if (loading) {
|
|
592
|
+
return /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Loading branches...");
|
|
593
|
+
}
|
|
594
|
+
if (branches.length === 0) {
|
|
595
|
+
return /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, "No branches found");
|
|
596
|
+
}
|
|
597
|
+
const visibleCount = 8;
|
|
598
|
+
const halfVisible = Math.floor(visibleCount / 2);
|
|
599
|
+
let startIdx = Math.max(0, selectedBranchIndex - halfVisible);
|
|
600
|
+
const endIdx = Math.min(branches.length, startIdx + visibleCount);
|
|
601
|
+
if (endIdx - startIdx < visibleCount) {
|
|
602
|
+
startIdx = Math.max(0, endIdx - visibleCount);
|
|
603
|
+
}
|
|
604
|
+
const visibleBranches = branches.slice(startIdx, endIdx);
|
|
605
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Select base branch:"), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, startIdx > 0 && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ..."), visibleBranches.map((branch, idx) => {
|
|
606
|
+
const actualIdx = startIdx + idx;
|
|
607
|
+
const isSelected = actualIdx === selectedBranchIndex;
|
|
608
|
+
const isDefault = branch === defaultBranch;
|
|
609
|
+
return /* @__PURE__ */ React3.createElement(Box3, { key: branch }, /* @__PURE__ */ React3.createElement(Text3, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", branch, isDefault && /* @__PURE__ */ React3.createElement(Text3, { color: "green" }, " (default)")));
|
|
610
|
+
}), endIdx < branches.length && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ...")));
|
|
611
|
+
};
|
|
612
|
+
const renderNameInput = () => {
|
|
613
|
+
const baseBranch = branches[selectedBranchIndex] || "";
|
|
614
|
+
const finalName = existingSuffix !== null ? `${branchName}-${existingSuffix}` : branchName;
|
|
615
|
+
const endsWithSlash = branchName.endsWith("/");
|
|
616
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Base branch: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, baseBranch)), /* @__PURE__ */ React3.createElement(Text3, null, "Enter new branch name:"), /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, "> "), /* @__PURE__ */ React3.createElement(Text3, null, branchName), /* @__PURE__ */ React3.createElement(Text3, { color: "gray" }, "|")), validationError && /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, validationError), !validationError && endsWithSlash && /* @__PURE__ */ React3.createElement(Text3, { color: "yellow", dimColor: true }, "Hint: consecutive slashes (//) are not allowed"), !validationError && !endsWithSlash && existingSuffix !== null && branchName && /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Name exists, will create: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, finalName)));
|
|
617
|
+
};
|
|
618
|
+
const renderCreating = () => /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Creating branch..."), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Please wait while the branch is created and pushed to remote."));
|
|
619
|
+
const renderResult = () => {
|
|
620
|
+
if (!result) return null;
|
|
621
|
+
if (result.success) {
|
|
622
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "green" }, "Branch created successfully!"), /* @__PURE__ */ React3.createElement(Text3, null, "Created: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, result.finalName)), /* @__PURE__ */ React3.createElement(Text3, null, "From: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, branches[selectedBranchIndex])), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Syncing now to create the worktree..."));
|
|
623
|
+
}
|
|
624
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, "Failed to create branch"), /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, result.error));
|
|
625
|
+
};
|
|
626
|
+
const renderContent = () => {
|
|
627
|
+
switch (step) {
|
|
628
|
+
case "SELECT_PROJECT":
|
|
629
|
+
return renderProjectSelection();
|
|
630
|
+
case "SELECT_BRANCH":
|
|
631
|
+
return renderBranchSelection();
|
|
632
|
+
case "ENTER_NAME":
|
|
633
|
+
return renderNameInput();
|
|
634
|
+
case "CREATING":
|
|
635
|
+
return renderCreating();
|
|
636
|
+
case "RESULT":
|
|
637
|
+
return renderResult();
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
const renderFooter = () => {
|
|
641
|
+
if (step === "CREATING") return null;
|
|
642
|
+
if (step === "RESULT") {
|
|
643
|
+
return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Press any key to continue");
|
|
644
|
+
}
|
|
645
|
+
if (step === "ENTER_NAME") {
|
|
646
|
+
return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Enter to create \u2022 ESC to go back");
|
|
647
|
+
}
|
|
648
|
+
return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u2191/\u2193 to navigate \u2022 Enter to select \u2022 ESC to cancel");
|
|
649
|
+
};
|
|
650
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Box3, { borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "\u{1F33F} Create New Branch", " ", step !== "CREATING" && step !== "RESULT" && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "(Step ", getStepNumber(), "/", getTotalSteps(), ")"))), repositories.length > 1 && step !== "SELECT_PROJECT" && step !== "CREATING" && step !== "RESULT" && /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Repository: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, repositories[selectedProjectIndex].name))), renderContent(), /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, renderFooter())));
|
|
651
|
+
};
|
|
652
|
+
var BranchCreationWizard_default = BranchCreationWizard;
|
|
653
|
+
|
|
654
|
+
// src/components/OpenEditorWizard.tsx
|
|
655
|
+
import React4, { useState as useState3, useEffect as useEffect3, useMemo, useCallback as useCallback2, useRef } from "react";
|
|
656
|
+
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
657
|
+
var OpenEditorWizard = ({
|
|
658
|
+
repositories,
|
|
659
|
+
getWorktreesForRepo,
|
|
660
|
+
openEditorInWorktree,
|
|
661
|
+
onClose
|
|
662
|
+
}) => {
|
|
663
|
+
const [step, setStep] = useState3(repositories.length > 1 ? "SELECT_PROJECT" : "SELECT_WORKTREE");
|
|
664
|
+
const [selectedProjectIndex, setSelectedProjectIndex] = useState3(0);
|
|
665
|
+
const [projectFilter, setProjectFilter] = useState3("");
|
|
666
|
+
const selectedRepoIndexRef = useRef(repositories.length === 1 ? 0 : -1);
|
|
667
|
+
const [worktrees, setWorktrees] = useState3([]);
|
|
668
|
+
const [selectedWorktreeIndex, setSelectedWorktreeIndex] = useState3(0);
|
|
669
|
+
const [worktreeFilter, setWorktreeFilter] = useState3("");
|
|
670
|
+
const [loading, setLoading] = useState3(false);
|
|
671
|
+
const [error, setError] = useState3(null);
|
|
672
|
+
const filteredProjects = useMemo(() => {
|
|
673
|
+
if (!projectFilter) return repositories;
|
|
674
|
+
const lowerFilter = projectFilter.toLowerCase();
|
|
675
|
+
return repositories.filter((repo) => repo.name.toLowerCase().includes(lowerFilter));
|
|
676
|
+
}, [repositories, projectFilter]);
|
|
677
|
+
const filteredWorktrees = useMemo(() => {
|
|
678
|
+
if (!worktreeFilter) return worktrees;
|
|
679
|
+
const lowerFilter = worktreeFilter.toLowerCase();
|
|
680
|
+
return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerFilter));
|
|
681
|
+
}, [worktrees, worktreeFilter]);
|
|
682
|
+
const loadWorktrees = useCallback2(
|
|
683
|
+
async (repoIndex) => {
|
|
684
|
+
setLoading(true);
|
|
685
|
+
try {
|
|
686
|
+
const wts = await getWorktreesForRepo(repoIndex);
|
|
687
|
+
setWorktrees(wts);
|
|
688
|
+
setSelectedWorktreeIndex(0);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
setError(`Failed to load worktrees: ${err}`);
|
|
691
|
+
setStep("ERROR");
|
|
692
|
+
}
|
|
693
|
+
setLoading(false);
|
|
694
|
+
},
|
|
695
|
+
[getWorktreesForRepo]
|
|
696
|
+
);
|
|
697
|
+
useEffect3(() => {
|
|
698
|
+
if (step === "SELECT_WORKTREE" && worktrees.length === 0 && !loading && selectedRepoIndexRef.current >= 0) {
|
|
699
|
+
loadWorktrees(selectedRepoIndexRef.current);
|
|
700
|
+
}
|
|
701
|
+
}, [step, worktrees.length, loading, loadWorktrees]);
|
|
702
|
+
const handleOpenEditor = () => {
|
|
703
|
+
const worktree = filteredWorktrees[selectedWorktreeIndex];
|
|
704
|
+
if (!worktree) return;
|
|
705
|
+
setStep("OPENING");
|
|
706
|
+
const result = openEditorInWorktree(worktree.path);
|
|
707
|
+
if (result.success) {
|
|
708
|
+
onClose();
|
|
709
|
+
} else {
|
|
710
|
+
setError(result.error || "Failed to open editor");
|
|
711
|
+
setStep("ERROR");
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
useInput3((input2, key) => {
|
|
715
|
+
if (step === "OPENING") return;
|
|
716
|
+
if (key.escape) {
|
|
717
|
+
if (step === "SELECT_PROJECT") {
|
|
718
|
+
onClose();
|
|
719
|
+
} else if (step === "SELECT_WORKTREE") {
|
|
720
|
+
if (repositories.length > 1) {
|
|
721
|
+
setWorktrees([]);
|
|
722
|
+
setWorktreeFilter("");
|
|
723
|
+
selectedRepoIndexRef.current = -1;
|
|
724
|
+
setStep("SELECT_PROJECT");
|
|
725
|
+
} else {
|
|
726
|
+
onClose();
|
|
727
|
+
}
|
|
728
|
+
} else if (step === "ERROR") {
|
|
729
|
+
onClose();
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (step === "SELECT_PROJECT") {
|
|
734
|
+
if (key.upArrow) {
|
|
735
|
+
setSelectedProjectIndex((prev) => Math.max(0, prev - 1));
|
|
736
|
+
} else if (key.downArrow) {
|
|
737
|
+
setSelectedProjectIndex((prev) => Math.min(filteredProjects.length - 1, prev + 1));
|
|
738
|
+
} else if (key.return && filteredProjects.length > 0) {
|
|
739
|
+
const selectedRepo = filteredProjects[selectedProjectIndex];
|
|
740
|
+
if (selectedRepo) {
|
|
741
|
+
selectedRepoIndexRef.current = selectedRepo.index;
|
|
742
|
+
setStep("SELECT_WORKTREE");
|
|
743
|
+
loadWorktrees(selectedRepo.index);
|
|
744
|
+
}
|
|
745
|
+
} else if (key.backspace || key.delete) {
|
|
746
|
+
setProjectFilter((prev) => prev.slice(0, -1));
|
|
747
|
+
setSelectedProjectIndex(0);
|
|
748
|
+
} else if (input2 && !key.ctrl && !key.meta) {
|
|
749
|
+
setProjectFilter((prev) => prev + input2);
|
|
750
|
+
setSelectedProjectIndex(0);
|
|
751
|
+
}
|
|
752
|
+
} else if (step === "SELECT_WORKTREE") {
|
|
753
|
+
if (key.upArrow) {
|
|
754
|
+
setSelectedWorktreeIndex((prev) => Math.max(0, prev - 1));
|
|
755
|
+
} else if (key.downArrow) {
|
|
756
|
+
setSelectedWorktreeIndex((prev) => Math.min(filteredWorktrees.length - 1, prev + 1));
|
|
757
|
+
} else if (key.return && filteredWorktrees.length > 0) {
|
|
758
|
+
handleOpenEditor();
|
|
759
|
+
} else if (key.backspace || key.delete) {
|
|
760
|
+
setWorktreeFilter((prev) => prev.slice(0, -1));
|
|
761
|
+
setSelectedWorktreeIndex(0);
|
|
762
|
+
} else if (input2 && !key.ctrl && !key.meta) {
|
|
763
|
+
setWorktreeFilter((prev) => prev + input2);
|
|
764
|
+
setSelectedWorktreeIndex(0);
|
|
765
|
+
}
|
|
766
|
+
} else if (step === "ERROR") {
|
|
767
|
+
onClose();
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
const getStepNumber = () => {
|
|
771
|
+
if (repositories.length === 1) {
|
|
772
|
+
return 1;
|
|
773
|
+
}
|
|
774
|
+
return step === "SELECT_PROJECT" ? 1 : 2;
|
|
775
|
+
};
|
|
776
|
+
const getTotalSteps = () => repositories.length === 1 ? 1 : 2;
|
|
777
|
+
const renderProjectSelection = () => {
|
|
778
|
+
const visibleCount = 8;
|
|
779
|
+
const halfVisible = Math.floor(visibleCount / 2);
|
|
780
|
+
let startIdx = Math.max(0, selectedProjectIndex - halfVisible);
|
|
781
|
+
const endIdx = Math.min(filteredProjects.length, startIdx + visibleCount);
|
|
782
|
+
if (endIdx - startIdx < visibleCount) {
|
|
783
|
+
startIdx = Math.max(0, endIdx - visibleCount);
|
|
784
|
+
}
|
|
785
|
+
const visibleProjects = filteredProjects.slice(startIdx, endIdx);
|
|
786
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Select repository:"), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, null, "Filter: "), /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, projectFilter || "_"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ", "(", filteredProjects.length, "/", repositories.length, " matches)")), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, filteredProjects.length === 0 ? /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "No matches") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, startIdx > 0 && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."), visibleProjects.map((repo, idx) => {
|
|
787
|
+
const actualIdx = startIdx + idx;
|
|
788
|
+
const isSelected = actualIdx === selectedProjectIndex;
|
|
789
|
+
return /* @__PURE__ */ React4.createElement(Box4, { key: repo.index }, /* @__PURE__ */ React4.createElement(Text4, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", repo.name));
|
|
790
|
+
}), endIdx < filteredProjects.length && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."))));
|
|
791
|
+
};
|
|
792
|
+
const renderWorktreeSelection = () => {
|
|
793
|
+
if (loading) {
|
|
794
|
+
return /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "Loading worktrees...");
|
|
795
|
+
}
|
|
796
|
+
if (worktrees.length === 0) {
|
|
797
|
+
return /* @__PURE__ */ React4.createElement(Text4, { color: "red" }, "No worktrees found");
|
|
798
|
+
}
|
|
799
|
+
const visibleCount = 8;
|
|
800
|
+
const halfVisible = Math.floor(visibleCount / 2);
|
|
801
|
+
let startIdx = Math.max(0, selectedWorktreeIndex - halfVisible);
|
|
802
|
+
const endIdx = Math.min(filteredWorktrees.length, startIdx + visibleCount);
|
|
803
|
+
if (endIdx - startIdx < visibleCount) {
|
|
804
|
+
startIdx = Math.max(0, endIdx - visibleCount);
|
|
805
|
+
}
|
|
806
|
+
const visibleWorktrees = filteredWorktrees.slice(startIdx, endIdx);
|
|
807
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Select worktree:"), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, null, "Filter: "), /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, worktreeFilter || "_"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ", "(", filteredWorktrees.length, "/", worktrees.length, " matches)")), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, filteredWorktrees.length === 0 ? /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "No matches") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, startIdx > 0 && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."), visibleWorktrees.map((wt, idx) => {
|
|
808
|
+
const actualIdx = startIdx + idx;
|
|
809
|
+
const isSelected = actualIdx === selectedWorktreeIndex;
|
|
810
|
+
return /* @__PURE__ */ React4.createElement(Box4, { key: wt.path }, /* @__PURE__ */ React4.createElement(Text4, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", wt.branch));
|
|
811
|
+
}), endIdx < filteredWorktrees.length && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."))));
|
|
812
|
+
};
|
|
813
|
+
const renderOpening = () => /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "Opening editor..."));
|
|
814
|
+
const renderError = () => /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "red" }, "Error: ", error), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Press any key to close"));
|
|
815
|
+
const renderContent = () => {
|
|
816
|
+
switch (step) {
|
|
817
|
+
case "SELECT_PROJECT":
|
|
818
|
+
return renderProjectSelection();
|
|
819
|
+
case "SELECT_WORKTREE":
|
|
820
|
+
return renderWorktreeSelection();
|
|
821
|
+
case "OPENING":
|
|
822
|
+
return renderOpening();
|
|
823
|
+
case "ERROR":
|
|
824
|
+
return renderError();
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
const renderFooter = () => {
|
|
828
|
+
if (step === "OPENING") return null;
|
|
829
|
+
if (step === "ERROR") return null;
|
|
830
|
+
return /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "\u2191/\u2193 navigate \u2022 Type to filter \u2022 Enter to select \u2022 ESC to cancel");
|
|
831
|
+
};
|
|
832
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: "blue", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "blue" }, "\u{1F4C2} Open in Editor", " ", step !== "OPENING" && step !== "ERROR" && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "(Step ", getStepNumber(), "/", getTotalSteps(), ")"))), repositories.length > 1 && step === "SELECT_WORKTREE" && !loading && selectedRepoIndexRef.current >= 0 && /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Repository: ", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, repositories.find((r) => r.index === selectedRepoIndexRef.current)?.name))), renderContent(), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1 }, renderFooter())));
|
|
833
|
+
};
|
|
834
|
+
var OpenEditorWizard_default = OpenEditorWizard;
|
|
835
|
+
|
|
836
|
+
// src/components/LogPanel.tsx
|
|
837
|
+
import React5, { useState as useState4, useEffect as useEffect4, useRef as useRef2 } from "react";
|
|
838
|
+
import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
|
|
839
|
+
var LogPanel = ({ logs, height, isActive }) => {
|
|
840
|
+
const [scrollOffset, setScrollOffset] = useState4(0);
|
|
841
|
+
const [autoScroll, setAutoScroll] = useState4(true);
|
|
842
|
+
const [pendingG, setPendingG] = useState4(false);
|
|
843
|
+
const gTimeoutRef = useRef2(null);
|
|
844
|
+
const borderLines = 2;
|
|
845
|
+
const headerLine = 1;
|
|
846
|
+
const visibleLines = Math.max(1, height - borderLines - headerLine);
|
|
847
|
+
const maxOffset = Math.max(0, logs.length - visibleLines);
|
|
848
|
+
useEffect4(() => {
|
|
849
|
+
if (autoScroll) {
|
|
850
|
+
setScrollOffset(maxOffset);
|
|
851
|
+
}
|
|
852
|
+
}, [logs.length, maxOffset, autoScroll]);
|
|
853
|
+
useEffect4(() => {
|
|
854
|
+
return () => {
|
|
855
|
+
if (gTimeoutRef.current) {
|
|
856
|
+
clearTimeout(gTimeoutRef.current);
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
}, []);
|
|
860
|
+
useInput4(
|
|
861
|
+
(input2, key) => {
|
|
862
|
+
if (!isActive) return;
|
|
863
|
+
if (key.upArrow || input2 === "k") {
|
|
864
|
+
setScrollOffset((prev) => Math.max(0, prev - 1));
|
|
865
|
+
setAutoScroll(false);
|
|
866
|
+
setPendingG(false);
|
|
867
|
+
} else if (key.downArrow || input2 === "j") {
|
|
868
|
+
setScrollOffset((prev) => {
|
|
869
|
+
const newOffset = Math.min(maxOffset, prev + 1);
|
|
870
|
+
if (newOffset >= maxOffset) {
|
|
871
|
+
setAutoScroll(true);
|
|
872
|
+
}
|
|
873
|
+
return newOffset;
|
|
874
|
+
});
|
|
875
|
+
setPendingG(false);
|
|
876
|
+
} else if (key.pageUp) {
|
|
877
|
+
setScrollOffset((prev) => Math.max(0, prev - visibleLines));
|
|
878
|
+
setAutoScroll(false);
|
|
879
|
+
setPendingG(false);
|
|
880
|
+
} else if (key.pageDown) {
|
|
881
|
+
setScrollOffset((prev) => {
|
|
882
|
+
const newOffset = Math.min(maxOffset, prev + visibleLines);
|
|
883
|
+
if (newOffset >= maxOffset) {
|
|
884
|
+
setAutoScroll(true);
|
|
885
|
+
}
|
|
886
|
+
return newOffset;
|
|
887
|
+
});
|
|
888
|
+
setPendingG(false);
|
|
889
|
+
} else if (input2 === "g") {
|
|
890
|
+
if (pendingG) {
|
|
891
|
+
setScrollOffset(0);
|
|
892
|
+
setAutoScroll(false);
|
|
893
|
+
setPendingG(false);
|
|
894
|
+
if (gTimeoutRef.current) {
|
|
895
|
+
clearTimeout(gTimeoutRef.current);
|
|
896
|
+
gTimeoutRef.current = null;
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
setPendingG(true);
|
|
900
|
+
gTimeoutRef.current = setTimeout(() => {
|
|
901
|
+
setPendingG(false);
|
|
902
|
+
}, 500);
|
|
903
|
+
}
|
|
904
|
+
} else if (input2 === "G") {
|
|
905
|
+
setScrollOffset(maxOffset);
|
|
906
|
+
setAutoScroll(true);
|
|
907
|
+
setPendingG(false);
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
{ isActive }
|
|
911
|
+
);
|
|
912
|
+
const getLogColor = (level) => {
|
|
913
|
+
switch (level) {
|
|
914
|
+
case "error":
|
|
915
|
+
return "red";
|
|
916
|
+
case "warn":
|
|
917
|
+
return "yellow";
|
|
918
|
+
default:
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
const visibleLogs = logs.slice(scrollOffset, scrollOffset + visibleLines);
|
|
923
|
+
const hasMoreAbove = scrollOffset > 0;
|
|
924
|
+
const hasMoreBelow = scrollOffset + visibleLines < logs.length;
|
|
925
|
+
const aboveCount = scrollOffset;
|
|
926
|
+
const belowCount = logs.length - scrollOffset - visibleLines;
|
|
927
|
+
const emptyLines = Math.max(0, visibleLines - visibleLogs.length);
|
|
928
|
+
return /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "single", flexDirection: "column", flexGrow: 1, paddingX: 1 }, /* @__PURE__ */ React5.createElement(Box5, { justifyContent: "space-between" }, /* @__PURE__ */ React5.createElement(Text5, { bold: true }, "\u{1F4CB} Logs ", logs.length > 0 && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(", logs.length, " entries)")), isActive && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, hasMoreAbove || hasMoreBelow ? "\u2191/\u2193 scroll" : "", " ", autoScroll ? "(auto)" : "")), hasMoreAbove && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2191 ", aboveCount, " more above"), visibleLogs.map((log) => /* @__PURE__ */ React5.createElement(Text5, { key: log.id, color: getLogColor(log.level), wrap: "truncate" }, log.message)), Array.from({ length: emptyLines }).map((_, i) => /* @__PURE__ */ React5.createElement(Text5, { key: `empty-${i}` }, " ")), hasMoreBelow && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2193 ", belowCount, " more below"));
|
|
929
|
+
};
|
|
930
|
+
var LogPanel_default = LogPanel;
|
|
931
|
+
|
|
932
|
+
// src/utils/app-events.ts
|
|
933
|
+
var AppEventEmitter = class {
|
|
934
|
+
listeners = /* @__PURE__ */ new Map();
|
|
935
|
+
on(event, callback) {
|
|
936
|
+
if (!this.listeners.has(event)) {
|
|
937
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
938
|
+
}
|
|
939
|
+
this.listeners.get(event).add(callback);
|
|
940
|
+
return () => {
|
|
941
|
+
this.listeners.get(event)?.delete(callback);
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
emit(event, ...args) {
|
|
945
|
+
const callbacks = this.listeners.get(event);
|
|
946
|
+
if (callbacks) {
|
|
947
|
+
for (const callback of callbacks) {
|
|
948
|
+
try {
|
|
949
|
+
callback(args[0]);
|
|
950
|
+
} catch {
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
removeAllListeners() {
|
|
956
|
+
this.listeners.clear();
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
var appEvents = new AppEventEmitter();
|
|
960
|
+
|
|
961
|
+
// src/components/App.tsx
|
|
962
|
+
var MAX_LOG_ENTRIES = 5e3;
|
|
963
|
+
var App = ({
|
|
964
|
+
repositoryCount,
|
|
965
|
+
cronSchedule,
|
|
966
|
+
onManualSync,
|
|
967
|
+
onReload,
|
|
968
|
+
onQuit,
|
|
969
|
+
getRepositoryList,
|
|
970
|
+
getBranchesForRepo,
|
|
971
|
+
getDefaultBranchForRepo,
|
|
972
|
+
createAndPushBranch,
|
|
973
|
+
getWorktreesForRepo,
|
|
974
|
+
openEditorInWorktree,
|
|
975
|
+
copyBranchFiles,
|
|
976
|
+
createWorktreeForBranch
|
|
977
|
+
}) => {
|
|
978
|
+
const [showHelp, setShowHelp] = useState5(false);
|
|
979
|
+
const [showBranchWizard, setShowBranchWizard] = useState5(false);
|
|
980
|
+
const [showOpenEditorWizard, setShowOpenEditorWizard] = useState5(false);
|
|
981
|
+
const [status, setStatus] = useState5("idle");
|
|
982
|
+
const [lastSyncTime, setLastSyncTime] = useState5(null);
|
|
983
|
+
const [diskSpaceUsed, setDiskSpaceUsed] = useState5(null);
|
|
984
|
+
const [logs, setLogs] = useState5([]);
|
|
985
|
+
const { stdout } = useStdout();
|
|
986
|
+
const addLog = useCallback3((message, level = "info") => {
|
|
987
|
+
setLogs((prev) => {
|
|
988
|
+
const newLogs = [
|
|
989
|
+
...prev,
|
|
990
|
+
{
|
|
991
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
992
|
+
message,
|
|
993
|
+
level,
|
|
994
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
995
|
+
}
|
|
996
|
+
];
|
|
997
|
+
if (newLogs.length > MAX_LOG_ENTRIES) {
|
|
998
|
+
return newLogs.slice(-MAX_LOG_ENTRIES);
|
|
999
|
+
}
|
|
1000
|
+
return newLogs;
|
|
1001
|
+
});
|
|
1002
|
+
}, []);
|
|
1003
|
+
const addLogRef = useRef3(addLog);
|
|
1004
|
+
addLogRef.current = addLog;
|
|
1005
|
+
useInput5((input2) => {
|
|
384
1006
|
if (showHelp) {
|
|
385
1007
|
if (input2 === "?" || input2 === "h") {
|
|
386
1008
|
setShowHelp(false);
|
|
387
1009
|
}
|
|
388
1010
|
return;
|
|
389
1011
|
}
|
|
390
|
-
if (
|
|
1012
|
+
if (showBranchWizard || showOpenEditorWizard) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (input2 === "q") {
|
|
391
1016
|
void onQuit();
|
|
392
1017
|
} else if (input2 === "?" || input2 === "h") {
|
|
393
1018
|
setShowHelp(true);
|
|
1019
|
+
} else if (input2 === "c" && status === "idle") {
|
|
1020
|
+
setShowBranchWizard(true);
|
|
1021
|
+
} else if (input2 === "o" && status === "idle") {
|
|
1022
|
+
setShowOpenEditorWizard(true);
|
|
394
1023
|
} else if (input2 === "s" && status !== "syncing") {
|
|
395
1024
|
setStatus("syncing");
|
|
396
1025
|
void (async () => {
|
|
@@ -413,21 +1042,71 @@ var App = ({ repositoryCount, cronSchedule, onManualSync, onReload, onQuit }) =>
|
|
|
413
1042
|
})();
|
|
414
1043
|
}
|
|
415
1044
|
});
|
|
416
|
-
const updateLastSyncTime =
|
|
1045
|
+
const updateLastSyncTime = useCallback3(() => {
|
|
417
1046
|
setLastSyncTime(/* @__PURE__ */ new Date());
|
|
418
1047
|
setStatus("idle");
|
|
419
1048
|
}, []);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
updateLastSyncTime,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
1049
|
+
useEffect5(() => {
|
|
1050
|
+
const unsubscribers = [
|
|
1051
|
+
appEvents.on("updateLastSyncTime", () => {
|
|
1052
|
+
setLastSyncTime(/* @__PURE__ */ new Date());
|
|
1053
|
+
setStatus("idle");
|
|
1054
|
+
}),
|
|
1055
|
+
appEvents.on("setStatus", (newStatus) => {
|
|
1056
|
+
setStatus(newStatus);
|
|
1057
|
+
}),
|
|
1058
|
+
appEvents.on("setDiskSpace", (diskSpace) => {
|
|
1059
|
+
setDiskSpaceUsed(diskSpace);
|
|
1060
|
+
}),
|
|
1061
|
+
appEvents.on("addLog", ({ message, level }) => {
|
|
1062
|
+
addLogRef.current(message, level);
|
|
1063
|
+
})
|
|
1064
|
+
];
|
|
1065
|
+
appEvents.emit("uiReady");
|
|
426
1066
|
return () => {
|
|
427
|
-
|
|
1067
|
+
unsubscribers.forEach((unsub) => unsub());
|
|
428
1068
|
};
|
|
429
|
-
}, [
|
|
430
|
-
|
|
1069
|
+
}, []);
|
|
1070
|
+
const statusBarHeight = 5;
|
|
1071
|
+
const terminalRows = stdout.rows ?? 24;
|
|
1072
|
+
const logPanelHeight = Math.max(5, terminalRows - statusBarHeight);
|
|
1073
|
+
const showModal = showHelp || showBranchWizard || showOpenEditorWizard;
|
|
1074
|
+
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", minHeight: terminalRows }, !showModal && /* @__PURE__ */ React6.createElement(LogPanel_default, { logs, height: logPanelHeight, isActive: !showModal }), showHelp && /* @__PURE__ */ React6.createElement(HelpModal_default, { onClose: () => setShowHelp(false) }), showBranchWizard && /* @__PURE__ */ React6.createElement(
|
|
1075
|
+
BranchCreationWizard_default,
|
|
1076
|
+
{
|
|
1077
|
+
repositories: getRepositoryList(),
|
|
1078
|
+
getBranchesForRepo,
|
|
1079
|
+
getDefaultBranchForRepo,
|
|
1080
|
+
createAndPushBranch,
|
|
1081
|
+
onClose: () => setShowBranchWizard(false),
|
|
1082
|
+
onComplete: (success, context) => {
|
|
1083
|
+
setShowBranchWizard(false);
|
|
1084
|
+
if (success && context) {
|
|
1085
|
+
setStatus("syncing");
|
|
1086
|
+
void (async () => {
|
|
1087
|
+
try {
|
|
1088
|
+
await createWorktreeForBranch(context.repoIndex, context.newBranch);
|
|
1089
|
+
if (copyBranchFiles) {
|
|
1090
|
+
await copyBranchFiles(context.repoIndex, context.baseBranch, context.newBranch);
|
|
1091
|
+
}
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
console.error("Failed to create worktree:", error);
|
|
1094
|
+
} finally {
|
|
1095
|
+
setStatus("idle");
|
|
1096
|
+
}
|
|
1097
|
+
})();
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
), showOpenEditorWizard && /* @__PURE__ */ React6.createElement(
|
|
1102
|
+
OpenEditorWizard_default,
|
|
1103
|
+
{
|
|
1104
|
+
repositories: getRepositoryList(),
|
|
1105
|
+
getWorktreesForRepo,
|
|
1106
|
+
openEditorInWorktree,
|
|
1107
|
+
onClose: () => setShowOpenEditorWizard(false)
|
|
1108
|
+
}
|
|
1109
|
+
), /* @__PURE__ */ React6.createElement(
|
|
431
1110
|
StatusBar_default,
|
|
432
1111
|
{
|
|
433
1112
|
status,
|
|
@@ -436,7 +1115,7 @@ var App = ({ repositoryCount, cronSchedule, onManualSync, onReload, onQuit }) =>
|
|
|
436
1115
|
cronSchedule,
|
|
437
1116
|
diskSpaceUsed: diskSpaceUsed ?? void 0
|
|
438
1117
|
}
|
|
439
|
-
)
|
|
1118
|
+
));
|
|
440
1119
|
};
|
|
441
1120
|
var App_default = App;
|
|
442
1121
|
|
|
@@ -726,34 +1405,66 @@ function getDefaultBareRepoDir(repoUrl, baseDir = ".bare") {
|
|
|
726
1405
|
var Logger = class _Logger {
|
|
727
1406
|
repoName;
|
|
728
1407
|
debugEnabled;
|
|
1408
|
+
outputFn;
|
|
729
1409
|
constructor(options = {}) {
|
|
730
1410
|
this.repoName = options.repoName;
|
|
731
1411
|
this.debugEnabled = options.debug ?? false;
|
|
1412
|
+
this.outputFn = options.outputFn;
|
|
732
1413
|
}
|
|
733
1414
|
prefix() {
|
|
734
1415
|
return this.repoName ? `[${this.repoName}] ` : "";
|
|
735
1416
|
}
|
|
736
1417
|
debug(message, ...args) {
|
|
737
1418
|
if (!this.debugEnabled) return;
|
|
738
|
-
|
|
1419
|
+
const formattedMessage = this.prefix() + this.formatMessage(message, args);
|
|
1420
|
+
if (this.outputFn) {
|
|
1421
|
+
this.outputFn(formattedMessage, "debug");
|
|
1422
|
+
} else {
|
|
1423
|
+
console.log(formattedMessage);
|
|
1424
|
+
}
|
|
739
1425
|
}
|
|
740
1426
|
info(message, ...args) {
|
|
741
|
-
|
|
1427
|
+
const formattedMessage = this.prefix() + this.formatMessage(message, args);
|
|
1428
|
+
if (this.outputFn) {
|
|
1429
|
+
this.outputFn(formattedMessage, "info");
|
|
1430
|
+
} else {
|
|
1431
|
+
console.log(formattedMessage);
|
|
1432
|
+
}
|
|
742
1433
|
}
|
|
743
1434
|
warn(message, ...args) {
|
|
744
|
-
|
|
1435
|
+
const formattedMessage = this.prefix() + this.formatMessage(message, args);
|
|
1436
|
+
if (this.outputFn) {
|
|
1437
|
+
this.outputFn(formattedMessage, "warn");
|
|
1438
|
+
} else {
|
|
1439
|
+
console.warn(formattedMessage);
|
|
1440
|
+
}
|
|
745
1441
|
}
|
|
746
1442
|
error(message, error) {
|
|
1443
|
+
let formattedMessage = this.prefix() + message;
|
|
747
1444
|
if (error instanceof Error) {
|
|
748
|
-
|
|
1445
|
+
formattedMessage += ` ${error.message}`;
|
|
749
1446
|
} else if (error) {
|
|
750
|
-
|
|
1447
|
+
formattedMessage += ` ${String(error)}`;
|
|
1448
|
+
}
|
|
1449
|
+
if (this.outputFn) {
|
|
1450
|
+
this.outputFn(formattedMessage, "error");
|
|
751
1451
|
} else {
|
|
752
|
-
|
|
1452
|
+
if (error instanceof Error) {
|
|
1453
|
+
console.error(this.prefix() + message, error);
|
|
1454
|
+
} else if (error) {
|
|
1455
|
+
console.error(this.prefix() + message, error);
|
|
1456
|
+
} else {
|
|
1457
|
+
console.error(this.prefix() + message);
|
|
1458
|
+
}
|
|
753
1459
|
}
|
|
754
1460
|
}
|
|
755
1461
|
table(content) {
|
|
756
|
-
|
|
1462
|
+
const formattedMessage = "\n" + content + "\n";
|
|
1463
|
+
if (this.outputFn) {
|
|
1464
|
+
this.outputFn(formattedMessage, "info");
|
|
1465
|
+
} else {
|
|
1466
|
+
console.log(formattedMessage);
|
|
1467
|
+
}
|
|
757
1468
|
}
|
|
758
1469
|
formatMessage(message, args) {
|
|
759
1470
|
if (args.length === 0) {
|
|
@@ -1024,9 +1735,9 @@ var WorktreeError = class extends SyncWorktreesError {
|
|
|
1024
1735
|
}
|
|
1025
1736
|
};
|
|
1026
1737
|
var WorktreeNotCleanError = class extends WorktreeError {
|
|
1027
|
-
constructor(
|
|
1028
|
-
super(`Worktree at '${
|
|
1029
|
-
this.path =
|
|
1738
|
+
constructor(path11, reasons) {
|
|
1739
|
+
super(`Worktree at '${path11}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
|
|
1740
|
+
this.path = path11;
|
|
1030
1741
|
this.reasons = reasons;
|
|
1031
1742
|
}
|
|
1032
1743
|
};
|
|
@@ -1347,12 +2058,16 @@ var GitService = class {
|
|
|
1347
2058
|
metadataService;
|
|
1348
2059
|
statusService;
|
|
1349
2060
|
logger;
|
|
2061
|
+
updateLogger(logger) {
|
|
2062
|
+
this.logger = logger;
|
|
2063
|
+
}
|
|
1350
2064
|
async initialize() {
|
|
1351
2065
|
const { repoUrl } = this.config;
|
|
2066
|
+
let needsClone = false;
|
|
1352
2067
|
try {
|
|
1353
2068
|
await fs4.access(path4.join(this.bareRepoPath, "HEAD"));
|
|
1354
|
-
this.logger.info(`Bare repository at "${this.bareRepoPath}" already exists. Using it.`);
|
|
1355
2069
|
} catch {
|
|
2070
|
+
needsClone = true;
|
|
1356
2071
|
this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
|
|
1357
2072
|
await fs4.mkdir(path4.dirname(this.bareRepoPath), { recursive: true });
|
|
1358
2073
|
const cloneGit = this.isLfsSkipEnabled() ? simpleGit3().env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3();
|
|
@@ -1369,11 +2084,12 @@ var GitService = class {
|
|
|
1369
2084
|
} catch {
|
|
1370
2085
|
await bareGit.addConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*");
|
|
1371
2086
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
2087
|
+
if (needsClone) {
|
|
2088
|
+
this.logger.info("Fetching remote branches...");
|
|
2089
|
+
await bareGit.fetch(["--all"]);
|
|
2090
|
+
}
|
|
1374
2091
|
this.defaultBranch = await this.detectDefaultBranch(bareGit);
|
|
1375
2092
|
this.mainWorktreePath = path4.join(this.config.worktreeDir, this.defaultBranch);
|
|
1376
|
-
this.logger.info(`Detected default branch: ${this.defaultBranch}`);
|
|
1377
2093
|
let needsMainWorktree = true;
|
|
1378
2094
|
try {
|
|
1379
2095
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
@@ -1443,6 +2159,9 @@ var GitService = class {
|
|
|
1443
2159
|
}
|
|
1444
2160
|
return this.git;
|
|
1445
2161
|
}
|
|
2162
|
+
isInitialized() {
|
|
2163
|
+
return this.git !== null;
|
|
2164
|
+
}
|
|
1446
2165
|
getDefaultBranch() {
|
|
1447
2166
|
return this.defaultBranch;
|
|
1448
2167
|
}
|
|
@@ -1935,6 +2654,18 @@ var GitService = class {
|
|
|
1935
2654
|
return false;
|
|
1936
2655
|
}
|
|
1937
2656
|
}
|
|
2657
|
+
async isLocalAheadOfRemote(worktreePath, branch) {
|
|
2658
|
+
const worktreeGit = simpleGit3(worktreePath);
|
|
2659
|
+
try {
|
|
2660
|
+
const mergeBase = await worktreeGit.raw(["merge-base", "HEAD", `origin/${branch}`]);
|
|
2661
|
+
const mergeBaseSha = mergeBase.trim();
|
|
2662
|
+
const remoteSha = await worktreeGit.revparse([`origin/${branch}`]);
|
|
2663
|
+
const remoteShaTrimmed = remoteSha.trim();
|
|
2664
|
+
return mergeBaseSha === remoteShaTrimmed;
|
|
2665
|
+
} catch {
|
|
2666
|
+
return false;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
1938
2669
|
async compareTreeContent(worktreePath, branch) {
|
|
1939
2670
|
const worktreeGit = simpleGit3(worktreePath);
|
|
1940
2671
|
try {
|
|
@@ -1972,6 +2703,29 @@ var GitService = class {
|
|
|
1972
2703
|
const commit = await git.revparse([ref]);
|
|
1973
2704
|
return commit.trim();
|
|
1974
2705
|
}
|
|
2706
|
+
async branchExists(branchName) {
|
|
2707
|
+
const bareGit = simpleGit3(this.bareRepoPath);
|
|
2708
|
+
const localBranches = await bareGit.branch();
|
|
2709
|
+
const local = localBranches.all.includes(branchName);
|
|
2710
|
+
const remoteBranches = await bareGit.branch(["-r"]);
|
|
2711
|
+
const remote = remoteBranches.all.includes(`origin/${branchName}`);
|
|
2712
|
+
return { local, remote };
|
|
2713
|
+
}
|
|
2714
|
+
async getLocalBranches() {
|
|
2715
|
+
const bareGit = simpleGit3(this.bareRepoPath);
|
|
2716
|
+
const branches = await bareGit.branch();
|
|
2717
|
+
return branches.all;
|
|
2718
|
+
}
|
|
2719
|
+
async createBranch(branchName, baseBranch) {
|
|
2720
|
+
const bareGit = simpleGit3(this.bareRepoPath);
|
|
2721
|
+
await bareGit.raw(["branch", branchName, `origin/${baseBranch}`]);
|
|
2722
|
+
this.logger.info(`Created branch '${branchName}' from '${baseBranch}'`);
|
|
2723
|
+
}
|
|
2724
|
+
async pushBranch(branchName) {
|
|
2725
|
+
const bareGit = simpleGit3(this.bareRepoPath);
|
|
2726
|
+
await bareGit.push(["origin", `${branchName}:${branchName}`, "-u"]);
|
|
2727
|
+
this.logger.info(`Pushed branch '${branchName}' to remote`);
|
|
2728
|
+
}
|
|
1975
2729
|
async getWorktreeMetadata(worktreePath) {
|
|
1976
2730
|
return this.metadataService.loadMetadataFromPath(this.bareRepoPath, worktreePath);
|
|
1977
2731
|
}
|
|
@@ -2026,9 +2780,19 @@ var WorktreeSyncService = class {
|
|
|
2026
2780
|
async initialize() {
|
|
2027
2781
|
await this.gitService.initialize();
|
|
2028
2782
|
}
|
|
2783
|
+
isInitialized() {
|
|
2784
|
+
return this.gitService.isInitialized();
|
|
2785
|
+
}
|
|
2029
2786
|
isSyncInProgress() {
|
|
2030
2787
|
return this.syncInProgress;
|
|
2031
2788
|
}
|
|
2789
|
+
getGitService() {
|
|
2790
|
+
return this.gitService;
|
|
2791
|
+
}
|
|
2792
|
+
updateLogger(logger) {
|
|
2793
|
+
this.logger = logger;
|
|
2794
|
+
this.gitService.updateLogger(logger);
|
|
2795
|
+
}
|
|
2032
2796
|
async sync() {
|
|
2033
2797
|
if (this.syncInProgress) {
|
|
2034
2798
|
this.logger.warn("\u26A0\uFE0F Sync already in progress, skipping...");
|
|
@@ -2353,6 +3117,11 @@ var WorktreeSyncService = class {
|
|
|
2353
3117
|
if (!isClean) return null;
|
|
2354
3118
|
const canFastForward = await this.gitService.canFastForward(worktree.path, worktree.branch);
|
|
2355
3119
|
if (!canFastForward) {
|
|
3120
|
+
const isAhead = await this.gitService.isLocalAheadOfRemote(worktree.path, worktree.branch);
|
|
3121
|
+
if (isAhead) {
|
|
3122
|
+
this.logger.info(`\u23ED\uFE0F Skipping '${worktree.branch}' - has unpushed commits`);
|
|
3123
|
+
return null;
|
|
3124
|
+
}
|
|
2356
3125
|
await this.handleDivergedBranch(worktree);
|
|
2357
3126
|
return null;
|
|
2358
3127
|
}
|
|
@@ -2521,6 +3290,84 @@ var WorktreeSyncService = class {
|
|
|
2521
3290
|
}
|
|
2522
3291
|
};
|
|
2523
3292
|
|
|
3293
|
+
// src/services/file-copy.service.ts
|
|
3294
|
+
import * as fs6 from "fs/promises";
|
|
3295
|
+
import * as path6 from "path";
|
|
3296
|
+
import { glob } from "glob";
|
|
3297
|
+
var DEFAULT_IGNORE_PATTERNS = [
|
|
3298
|
+
"**/node_modules/**",
|
|
3299
|
+
"**/.git/**",
|
|
3300
|
+
"**/dist/**",
|
|
3301
|
+
"**/build/**",
|
|
3302
|
+
"**/.next/**",
|
|
3303
|
+
"**/coverage/**"
|
|
3304
|
+
];
|
|
3305
|
+
var FileCopyService = class {
|
|
3306
|
+
/**
|
|
3307
|
+
* Copy files matching patterns from source to destination directory.
|
|
3308
|
+
* Skips files that already exist at destination.
|
|
3309
|
+
* Preserves directory structure relative to source.
|
|
3310
|
+
*/
|
|
3311
|
+
async copyFiles(sourceDir, destDir, patterns) {
|
|
3312
|
+
const result = {
|
|
3313
|
+
copied: [],
|
|
3314
|
+
skipped: [],
|
|
3315
|
+
errors: []
|
|
3316
|
+
};
|
|
3317
|
+
if (!patterns || patterns.length === 0) {
|
|
3318
|
+
return result;
|
|
3319
|
+
}
|
|
3320
|
+
const filesToCopy = await this.expandPatterns(sourceDir, patterns);
|
|
3321
|
+
for (const relativePath of filesToCopy) {
|
|
3322
|
+
const sourcePath = path6.join(sourceDir, relativePath);
|
|
3323
|
+
const destPath = path6.join(destDir, relativePath);
|
|
3324
|
+
try {
|
|
3325
|
+
const copied = await this.copyFile(sourcePath, destPath);
|
|
3326
|
+
if (copied) {
|
|
3327
|
+
result.copied.push(relativePath);
|
|
3328
|
+
} else {
|
|
3329
|
+
result.skipped.push(relativePath);
|
|
3330
|
+
}
|
|
3331
|
+
} catch (error) {
|
|
3332
|
+
result.errors.push({
|
|
3333
|
+
file: relativePath,
|
|
3334
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
return result;
|
|
3339
|
+
}
|
|
3340
|
+
async expandPatterns(sourceDir, patterns) {
|
|
3341
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
3342
|
+
for (const pattern of patterns) {
|
|
3343
|
+
try {
|
|
3344
|
+
const matches = await glob(pattern, {
|
|
3345
|
+
cwd: sourceDir,
|
|
3346
|
+
nodir: true,
|
|
3347
|
+
dot: true,
|
|
3348
|
+
ignore: DEFAULT_IGNORE_PATTERNS
|
|
3349
|
+
});
|
|
3350
|
+
for (const match of matches) {
|
|
3351
|
+
allFiles.add(match);
|
|
3352
|
+
}
|
|
3353
|
+
} catch {
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
return Array.from(allFiles);
|
|
3357
|
+
}
|
|
3358
|
+
async copyFile(sourcePath, destPath) {
|
|
3359
|
+
try {
|
|
3360
|
+
await fs6.access(destPath);
|
|
3361
|
+
return false;
|
|
3362
|
+
} catch {
|
|
3363
|
+
}
|
|
3364
|
+
const destDir = path6.dirname(destPath);
|
|
3365
|
+
await fs6.mkdir(destDir, { recursive: true });
|
|
3366
|
+
await fs6.copyFile(sourcePath, destPath);
|
|
3367
|
+
return true;
|
|
3368
|
+
}
|
|
3369
|
+
};
|
|
3370
|
+
|
|
2524
3371
|
// src/utils/disk-space.ts
|
|
2525
3372
|
import fastFolderSize from "fast-folder-size";
|
|
2526
3373
|
async function calculateDirectorySize(dirPath) {
|
|
@@ -2569,9 +3416,8 @@ var InteractiveUIService = class {
|
|
|
2569
3416
|
cronSchedule;
|
|
2570
3417
|
cronJobs = [];
|
|
2571
3418
|
repositoryCount;
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
originalConsoleError;
|
|
3419
|
+
logBuffer = [];
|
|
3420
|
+
uiReady = false;
|
|
2575
3421
|
constructor(syncServices, configPath, cronSchedule) {
|
|
2576
3422
|
if (syncServices.length === 0) {
|
|
2577
3423
|
throw new Error("InteractiveUIService requires at least one WorktreeSyncService");
|
|
@@ -2580,31 +3426,52 @@ var InteractiveUIService = class {
|
|
|
2580
3426
|
this.configPath = configPath;
|
|
2581
3427
|
this.cronSchedule = cronSchedule;
|
|
2582
3428
|
this.repositoryCount = syncServices.length;
|
|
2583
|
-
this.originalConsoleLog = console.log.bind(console);
|
|
2584
|
-
this.originalConsoleWarn = console.warn.bind(console);
|
|
2585
|
-
this.originalConsoleError = console.error.bind(console);
|
|
2586
|
-
this.redirectConsole();
|
|
2587
3429
|
this.setupCronJobs();
|
|
3430
|
+
this.startBufferFlushCheck();
|
|
2588
3431
|
this.renderUI();
|
|
3432
|
+
this.injectLoggersIntoServices();
|
|
3433
|
+
setTimeout(() => {
|
|
3434
|
+
this.addLog("\u{1F680} sync-worktrees UI initialized", "info");
|
|
3435
|
+
}, 100);
|
|
3436
|
+
}
|
|
3437
|
+
startBufferFlushCheck() {
|
|
3438
|
+
const unsubscribe = appEvents.on("uiReady", () => {
|
|
3439
|
+
this.uiReady = true;
|
|
3440
|
+
this.flushLogBuffer();
|
|
3441
|
+
unsubscribe();
|
|
3442
|
+
});
|
|
2589
3443
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
const
|
|
2593
|
-
this.
|
|
2594
|
-
};
|
|
2595
|
-
console.warn = (...args) => {
|
|
2596
|
-
const message = args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ");
|
|
2597
|
-
this.originalConsoleWarn(message);
|
|
2598
|
-
};
|
|
2599
|
-
console.error = (...args) => {
|
|
2600
|
-
const message = args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ");
|
|
2601
|
-
this.originalConsoleError(message);
|
|
3444
|
+
createOutputFn() {
|
|
3445
|
+
return (message, level) => {
|
|
3446
|
+
const uiLevel = level === "debug" ? "info" : level;
|
|
3447
|
+
this.addLog(message, uiLevel);
|
|
2602
3448
|
};
|
|
2603
3449
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
3450
|
+
injectLoggersIntoServices() {
|
|
3451
|
+
const outputFn = this.createOutputFn();
|
|
3452
|
+
for (const service of this.syncServices) {
|
|
3453
|
+
const config = service.config;
|
|
3454
|
+
service.updateLogger(
|
|
3455
|
+
new Logger({
|
|
3456
|
+
repoName: config.name,
|
|
3457
|
+
debug: config.debug,
|
|
3458
|
+
outputFn
|
|
3459
|
+
})
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
addLog(message, level = "info") {
|
|
3464
|
+
if (this.uiReady) {
|
|
3465
|
+
appEvents.emit("addLog", { message, level });
|
|
3466
|
+
} else {
|
|
3467
|
+
this.logBuffer.push({ message, level });
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
flushLogBuffer() {
|
|
3471
|
+
for (const log of this.logBuffer) {
|
|
3472
|
+
appEvents.emit("addLog", { message: log.message, level: log.level });
|
|
3473
|
+
}
|
|
3474
|
+
this.logBuffer = [];
|
|
2608
3475
|
}
|
|
2609
3476
|
setupCronJobs() {
|
|
2610
3477
|
if (!this.cronSchedule) {
|
|
@@ -2618,6 +3485,9 @@ var InteractiveUIService = class {
|
|
|
2618
3485
|
const task = cron.schedule(schedule3, async () => {
|
|
2619
3486
|
this.setStatus("syncing");
|
|
2620
3487
|
try {
|
|
3488
|
+
if (!service.isInitialized()) {
|
|
3489
|
+
await service.initialize();
|
|
3490
|
+
}
|
|
2621
3491
|
await service.sync();
|
|
2622
3492
|
} catch (error) {
|
|
2623
3493
|
console.error(`Error syncing: ${error.message}`);
|
|
@@ -2641,28 +3511,42 @@ var InteractiveUIService = class {
|
|
|
2641
3511
|
this.app.unmount();
|
|
2642
3512
|
}
|
|
2643
3513
|
this.app = render(
|
|
2644
|
-
/* @__PURE__ */
|
|
3514
|
+
/* @__PURE__ */ React7.createElement(
|
|
2645
3515
|
App_default,
|
|
2646
3516
|
{
|
|
2647
3517
|
repositoryCount: this.repositoryCount,
|
|
2648
3518
|
cronSchedule: this.cronSchedule,
|
|
2649
3519
|
onManualSync: () => this.handleManualSync(),
|
|
2650
3520
|
onReload: () => this.handleReload(),
|
|
2651
|
-
onQuit: () => this.handleQuit()
|
|
3521
|
+
onQuit: () => this.handleQuit(),
|
|
3522
|
+
getRepositoryList: () => this.getRepositoryList(),
|
|
3523
|
+
getBranchesForRepo: (index) => this.getBranchesForRepo(index),
|
|
3524
|
+
getDefaultBranchForRepo: (index) => this.getDefaultBranchForRepo(index),
|
|
3525
|
+
createAndPushBranch: (repoIndex, baseBranch, branchName) => this.createAndPushBranch(repoIndex, baseBranch, branchName),
|
|
3526
|
+
getWorktreesForRepo: (index) => this.getWorktreesForRepo(index),
|
|
3527
|
+
openEditorInWorktree: (path11) => this.openEditorInWorktree(path11),
|
|
3528
|
+
copyBranchFiles: (repoIndex, baseBranch, targetBranch) => this.copyBranchFiles(repoIndex, baseBranch, targetBranch),
|
|
3529
|
+
createWorktreeForBranch: (repoIndex, branchName) => this.createWorktreeForBranch(repoIndex, branchName)
|
|
2652
3530
|
}
|
|
2653
3531
|
)
|
|
2654
3532
|
);
|
|
2655
3533
|
}
|
|
2656
3534
|
async handleManualSync() {
|
|
3535
|
+
await this.triggerInitialSync();
|
|
3536
|
+
}
|
|
3537
|
+
async triggerInitialSync() {
|
|
2657
3538
|
this.setStatus("syncing");
|
|
2658
3539
|
try {
|
|
2659
3540
|
for (const service of this.syncServices) {
|
|
3541
|
+
if (!service.isInitialized()) {
|
|
3542
|
+
await service.initialize();
|
|
3543
|
+
}
|
|
2660
3544
|
await service.sync();
|
|
2661
3545
|
}
|
|
2662
3546
|
this.updateLastSyncTime();
|
|
2663
3547
|
await this.calculateAndUpdateDiskSpace();
|
|
2664
3548
|
} catch (error) {
|
|
2665
|
-
console.error("
|
|
3549
|
+
console.error("Sync failed:", error);
|
|
2666
3550
|
} finally {
|
|
2667
3551
|
this.setStatus("idle");
|
|
2668
3552
|
}
|
|
@@ -2746,22 +3630,13 @@ var InteractiveUIService = class {
|
|
|
2746
3630
|
}
|
|
2747
3631
|
}
|
|
2748
3632
|
updateLastSyncTime() {
|
|
2749
|
-
|
|
2750
|
-
if (methods && methods.updateLastSyncTime) {
|
|
2751
|
-
methods.updateLastSyncTime();
|
|
2752
|
-
}
|
|
3633
|
+
appEvents.emit("updateLastSyncTime");
|
|
2753
3634
|
}
|
|
2754
3635
|
setStatus(status) {
|
|
2755
|
-
|
|
2756
|
-
if (methods && methods.setStatus) {
|
|
2757
|
-
methods.setStatus(status);
|
|
2758
|
-
}
|
|
3636
|
+
appEvents.emit("setStatus", status);
|
|
2759
3637
|
}
|
|
2760
3638
|
setDiskSpace(diskSpace) {
|
|
2761
|
-
|
|
2762
|
-
if (methods && methods.setDiskSpace) {
|
|
2763
|
-
methods.setDiskSpace(diskSpace);
|
|
2764
|
-
}
|
|
3639
|
+
appEvents.emit("setDiskSpace", diskSpace);
|
|
2765
3640
|
}
|
|
2766
3641
|
async calculateAndUpdateDiskSpace() {
|
|
2767
3642
|
try {
|
|
@@ -2776,14 +3651,137 @@ var InteractiveUIService = class {
|
|
|
2776
3651
|
this.setDiskSpace("N/A");
|
|
2777
3652
|
}
|
|
2778
3653
|
}
|
|
3654
|
+
getRepositoryList() {
|
|
3655
|
+
return this.syncServices.map((service, index) => ({
|
|
3656
|
+
index,
|
|
3657
|
+
name: service.config.name || `repo-${index}`,
|
|
3658
|
+
repoUrl: service.config.repoUrl
|
|
3659
|
+
}));
|
|
3660
|
+
}
|
|
3661
|
+
async getBranchesForRepo(repoIndex) {
|
|
3662
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3663
|
+
throw new Error(`Invalid repository index: ${repoIndex}`);
|
|
3664
|
+
}
|
|
3665
|
+
const service = this.syncServices[repoIndex];
|
|
3666
|
+
const gitService = service.getGitService();
|
|
3667
|
+
return gitService.getRemoteBranches();
|
|
3668
|
+
}
|
|
3669
|
+
getDefaultBranchForRepo(repoIndex) {
|
|
3670
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3671
|
+
throw new Error(`Invalid repository index: ${repoIndex}`);
|
|
3672
|
+
}
|
|
3673
|
+
const service = this.syncServices[repoIndex];
|
|
3674
|
+
const gitService = service.getGitService();
|
|
3675
|
+
return gitService.getDefaultBranch();
|
|
3676
|
+
}
|
|
3677
|
+
async createAndPushBranch(repoIndex, baseBranch, branchName) {
|
|
3678
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3679
|
+
return { success: false, finalName: branchName, error: `Invalid repository index: ${repoIndex}` };
|
|
3680
|
+
}
|
|
3681
|
+
const service = this.syncServices[repoIndex];
|
|
3682
|
+
const gitService = service.getGitService();
|
|
3683
|
+
try {
|
|
3684
|
+
let finalName = branchName;
|
|
3685
|
+
let suffix = 0;
|
|
3686
|
+
while (true) {
|
|
3687
|
+
const exists = await gitService.branchExists(finalName);
|
|
3688
|
+
if (!exists.local && !exists.remote) {
|
|
3689
|
+
break;
|
|
3690
|
+
}
|
|
3691
|
+
suffix++;
|
|
3692
|
+
finalName = `${branchName}-${suffix}`;
|
|
3693
|
+
}
|
|
3694
|
+
await gitService.createBranch(finalName, baseBranch);
|
|
3695
|
+
await gitService.pushBranch(finalName);
|
|
3696
|
+
return { success: true, finalName };
|
|
3697
|
+
} catch (error) {
|
|
3698
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3699
|
+
return { success: false, finalName: branchName, error: errorMessage };
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
async getWorktreesForRepo(repoIndex) {
|
|
3703
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3704
|
+
throw new Error(`Invalid repository index: ${repoIndex}`);
|
|
3705
|
+
}
|
|
3706
|
+
const service = this.syncServices[repoIndex];
|
|
3707
|
+
const gitService = service.getGitService();
|
|
3708
|
+
return gitService.getWorktrees();
|
|
3709
|
+
}
|
|
3710
|
+
async createWorktreeForBranch(repoIndex, branchName) {
|
|
3711
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3712
|
+
throw new Error(`Invalid repository index: ${repoIndex}`);
|
|
3713
|
+
}
|
|
3714
|
+
const service = this.syncServices[repoIndex];
|
|
3715
|
+
const gitService = service.getGitService();
|
|
3716
|
+
const worktreeDir = service.config.worktreeDir;
|
|
3717
|
+
const worktreePath = path7.join(worktreeDir, branchName);
|
|
3718
|
+
await gitService.addWorktree(branchName, worktreePath);
|
|
3719
|
+
}
|
|
3720
|
+
openEditorInWorktree(worktreePath) {
|
|
3721
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "code";
|
|
3722
|
+
try {
|
|
3723
|
+
const child = spawn(editor, [worktreePath], {
|
|
3724
|
+
detached: true,
|
|
3725
|
+
stdio: "ignore"
|
|
3726
|
+
});
|
|
3727
|
+
child.on("error", (err) => {
|
|
3728
|
+
this.addLog(`Failed to open editor '${editor}': ${err.message}`, "error");
|
|
3729
|
+
this.addLog("Set EDITOR or VISUAL environment variable to your preferred editor", "warn");
|
|
3730
|
+
});
|
|
3731
|
+
child.unref();
|
|
3732
|
+
return { success: true };
|
|
3733
|
+
} catch (err) {
|
|
3734
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3735
|
+
this.addLog(`Failed to open editor '${editor}': ${errorMessage}`, "error");
|
|
3736
|
+
return { success: false, error: errorMessage };
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
async copyBranchFiles(repoIndex, baseBranch, targetBranch) {
|
|
3740
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
const service = this.syncServices[repoIndex];
|
|
3744
|
+
const config = service.config;
|
|
3745
|
+
if (!config.filesToCopyOnBranchCreate?.length) {
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
3748
|
+
const gitService = service.getGitService();
|
|
3749
|
+
const worktrees = await gitService.getWorktrees();
|
|
3750
|
+
const sourceWorktree = worktrees.find((w) => w.branch === baseBranch);
|
|
3751
|
+
const targetWorktree = worktrees.find((w) => w.branch === targetBranch);
|
|
3752
|
+
if (!sourceWorktree || !targetWorktree) {
|
|
3753
|
+
console.warn(`Could not find worktrees for file copy: source=${baseBranch}, target=${targetBranch}`);
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
const fileCopyService = new FileCopyService();
|
|
3757
|
+
try {
|
|
3758
|
+
const result = await fileCopyService.copyFiles(
|
|
3759
|
+
sourceWorktree.path,
|
|
3760
|
+
targetWorktree.path,
|
|
3761
|
+
config.filesToCopyOnBranchCreate
|
|
3762
|
+
);
|
|
3763
|
+
if (result.copied.length > 0) {
|
|
3764
|
+
console.log(`\u{1F4CB} Copied ${result.copied.length} file(s) to new branch: ${result.copied.join(", ")}`);
|
|
3765
|
+
}
|
|
3766
|
+
if (result.errors.length > 0) {
|
|
3767
|
+
console.warn(`\u26A0\uFE0F Failed to copy ${result.errors.length} file(s):`);
|
|
3768
|
+
for (const err of result.errors) {
|
|
3769
|
+
console.warn(` - ${err.file}: ${err.error}`);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
} catch (error) {
|
|
3773
|
+
console.error(`Failed to copy files to new branch: ${error}`);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
2779
3776
|
destroy() {
|
|
2780
3777
|
this.cancelCronJobs();
|
|
2781
|
-
this.restoreConsole();
|
|
2782
3778
|
if (this.app) {
|
|
2783
3779
|
this.app.unmount();
|
|
2784
3780
|
this.app = null;
|
|
2785
3781
|
}
|
|
2786
|
-
|
|
3782
|
+
appEvents.removeAllListeners();
|
|
3783
|
+
this.uiReady = false;
|
|
3784
|
+
this.logBuffer = [];
|
|
2787
3785
|
}
|
|
2788
3786
|
};
|
|
2789
3787
|
|
|
@@ -2842,6 +3840,10 @@ function parseArguments() {
|
|
|
2842
3840
|
type: "boolean",
|
|
2843
3841
|
description: "Enable debug mode to show detailed reasons why worktrees are not cleaned up.",
|
|
2844
3842
|
default: false
|
|
3843
|
+
}).option("sync-on-start", {
|
|
3844
|
+
type: "boolean",
|
|
3845
|
+
description: "Run sync immediately when starting the interactive UI (config mode only).",
|
|
3846
|
+
default: false
|
|
2845
3847
|
}).help().alias("help", "h").parseSync();
|
|
2846
3848
|
return {
|
|
2847
3849
|
config: argv.config,
|
|
@@ -2855,7 +3857,8 @@ function parseArguments() {
|
|
|
2855
3857
|
branchMaxAge: argv.branchMaxAge,
|
|
2856
3858
|
skipLfs: argv.skipLfs,
|
|
2857
3859
|
noUpdateExisting: argv["no-update-existing"],
|
|
2858
|
-
debug: argv.debug
|
|
3860
|
+
debug: argv.debug,
|
|
3861
|
+
syncOnStart: argv["sync-on-start"]
|
|
2859
3862
|
};
|
|
2860
3863
|
}
|
|
2861
3864
|
function isInteractiveMode(config) {
|
|
@@ -2893,12 +3896,12 @@ function reconstructCliCommand(config) {
|
|
|
2893
3896
|
}
|
|
2894
3897
|
|
|
2895
3898
|
// src/utils/interactive.ts
|
|
2896
|
-
import * as
|
|
3899
|
+
import * as path9 from "path";
|
|
2897
3900
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
2898
3901
|
|
|
2899
3902
|
// src/utils/config-generator.ts
|
|
2900
|
-
import * as
|
|
2901
|
-
import * as
|
|
3903
|
+
import * as fs7 from "fs/promises";
|
|
3904
|
+
import * as path8 from "path";
|
|
2902
3905
|
function serializeToESM(obj, indent = 0) {
|
|
2903
3906
|
const spaces = " ".repeat(indent);
|
|
2904
3907
|
const innerSpaces = " ".repeat(indent + 2);
|
|
@@ -2928,9 +3931,9 @@ ${spaces}}`;
|
|
|
2928
3931
|
return String(obj);
|
|
2929
3932
|
}
|
|
2930
3933
|
async function generateConfigFile(config, configPath) {
|
|
2931
|
-
const configDir =
|
|
2932
|
-
await
|
|
2933
|
-
const worktreeDirRelative =
|
|
3934
|
+
const configDir = path8.dirname(configPath);
|
|
3935
|
+
await fs7.mkdir(configDir, { recursive: true });
|
|
3936
|
+
const worktreeDirRelative = path8.relative(configDir, config.worktreeDir);
|
|
2934
3937
|
const useRelativeWorktree = !worktreeDirRelative.startsWith("../../../");
|
|
2935
3938
|
const repoName = extractRepoNameFromUrl(config.repoUrl);
|
|
2936
3939
|
const repository = {
|
|
@@ -2939,7 +3942,7 @@ async function generateConfigFile(config, configPath) {
|
|
|
2939
3942
|
worktreeDir: useRelativeWorktree ? `./${worktreeDirRelative}` : config.worktreeDir
|
|
2940
3943
|
};
|
|
2941
3944
|
if (config.bareRepoDir) {
|
|
2942
|
-
const bareRepoDirRelative =
|
|
3945
|
+
const bareRepoDirRelative = path8.relative(configDir, config.bareRepoDir);
|
|
2943
3946
|
const useRelativeBare = !bareRepoDirRelative.startsWith("../../../");
|
|
2944
3947
|
repository.bareRepoDir = useRelativeBare ? `./${bareRepoDirRelative}` : config.bareRepoDir;
|
|
2945
3948
|
}
|
|
@@ -2957,10 +3960,10 @@ async function generateConfigFile(config, configPath) {
|
|
|
2957
3960
|
|
|
2958
3961
|
export default ${serializeToESM(configObject)};
|
|
2959
3962
|
`;
|
|
2960
|
-
await
|
|
3963
|
+
await fs7.writeFile(configPath, configContent, "utf-8");
|
|
2961
3964
|
}
|
|
2962
3965
|
function getDefaultConfigPath() {
|
|
2963
|
-
return
|
|
3966
|
+
return path8.join(process.cwd(), "sync-worktrees.config.js");
|
|
2964
3967
|
}
|
|
2965
3968
|
|
|
2966
3969
|
// src/utils/interactive.ts
|
|
@@ -3002,8 +4005,8 @@ async function promptForConfig(partialConfig) {
|
|
|
3002
4005
|
if (!worktreeDir.trim() && defaultWorktreeDir) {
|
|
3003
4006
|
worktreeDir = defaultWorktreeDir;
|
|
3004
4007
|
}
|
|
3005
|
-
if (!
|
|
3006
|
-
worktreeDir =
|
|
4008
|
+
if (!path9.isAbsolute(worktreeDir)) {
|
|
4009
|
+
worktreeDir = path9.resolve(worktreeDir);
|
|
3007
4010
|
}
|
|
3008
4011
|
}
|
|
3009
4012
|
let bareRepoDir = partialConfig.bareRepoDir;
|
|
@@ -3022,8 +4025,8 @@ async function promptForConfig(partialConfig) {
|
|
|
3022
4025
|
return true;
|
|
3023
4026
|
}
|
|
3024
4027
|
});
|
|
3025
|
-
if (!
|
|
3026
|
-
bareRepoDir =
|
|
4028
|
+
if (!path9.isAbsolute(bareRepoDir)) {
|
|
4029
|
+
bareRepoDir = path9.resolve(bareRepoDir);
|
|
3027
4030
|
}
|
|
3028
4031
|
}
|
|
3029
4032
|
let runOnce = partialConfig.runOnce;
|
|
@@ -3094,8 +4097,8 @@ async function promptForConfig(partialConfig) {
|
|
|
3094
4097
|
return true;
|
|
3095
4098
|
}
|
|
3096
4099
|
});
|
|
3097
|
-
if (!
|
|
3098
|
-
configPath =
|
|
4100
|
+
if (!path9.isAbsolute(configPath)) {
|
|
4101
|
+
configPath = path9.resolve(configPath);
|
|
3099
4102
|
}
|
|
3100
4103
|
try {
|
|
3101
4104
|
await generateConfigFile(finalConfig, configPath);
|
|
@@ -3103,7 +4106,7 @@ async function promptForConfig(partialConfig) {
|
|
|
3103
4106
|
\u2705 Configuration saved to: ${configPath}`);
|
|
3104
4107
|
console.log(`
|
|
3105
4108
|
\u{1F4A1} You can now use this config file with:`);
|
|
3106
|
-
console.log(` sync-worktrees --config ${
|
|
4109
|
+
console.log(` sync-worktrees --config ${path9.relative(process.cwd(), configPath)}`);
|
|
3107
4110
|
console.log("");
|
|
3108
4111
|
} catch (error) {
|
|
3109
4112
|
console.error(`
|
|
@@ -3150,62 +4153,66 @@ async function runSingleRepository(config) {
|
|
|
3150
4153
|
process.exit(1);
|
|
3151
4154
|
}
|
|
3152
4155
|
}
|
|
3153
|
-
async function runMultipleRepositories(repositories, runOnce, configPath, maxParallel) {
|
|
4156
|
+
async function runMultipleRepositories(repositories, runOnce, configPath, maxParallel, syncOnStart) {
|
|
3154
4157
|
const services = /* @__PURE__ */ new Map();
|
|
3155
4158
|
const globalLogger = Logger.createDefault();
|
|
3156
|
-
globalLogger.info(`
|
|
3157
|
-
\u{1F504} Syncing ${repositories.length} repositories...`);
|
|
3158
4159
|
const limit = pLimit2(maxParallel ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES);
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
4160
|
+
if (runOnce) {
|
|
4161
|
+
globalLogger.info(`
|
|
4162
|
+
\u{1F504} Syncing ${repositories.length} repositories...`);
|
|
4163
|
+
const initResults = await Promise.allSettled(
|
|
4164
|
+
repositories.map(
|
|
4165
|
+
(repoConfig) => limit(async () => {
|
|
4166
|
+
const repoLogger = Logger.createDefault(repoConfig.name, repoConfig.debug);
|
|
4167
|
+
repoLogger.info(`
|
|
3164
4168
|
\u{1F4E6} Repository: ${repoConfig.name}`);
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
4169
|
+
repoLogger.info(` URL: ${repoConfig.repoUrl}`);
|
|
4170
|
+
repoLogger.info(` Worktrees: ${repoConfig.worktreeDir}`);
|
|
4171
|
+
if (repoConfig.bareRepoDir) {
|
|
4172
|
+
repoLogger.info(` Bare repo: ${repoConfig.bareRepoDir}`);
|
|
4173
|
+
}
|
|
4174
|
+
if (!repoConfig.logger) {
|
|
4175
|
+
repoConfig.logger = repoLogger;
|
|
4176
|
+
}
|
|
4177
|
+
const syncService = new WorktreeSyncService(repoConfig);
|
|
4178
|
+
await syncService.initialize();
|
|
4179
|
+
return { name: repoConfig.name, service: syncService };
|
|
4180
|
+
})
|
|
4181
|
+
)
|
|
4182
|
+
);
|
|
4183
|
+
const servicesToSync = [];
|
|
4184
|
+
for (const result of initResults) {
|
|
4185
|
+
if (result.status === "fulfilled") {
|
|
4186
|
+
services.set(result.value.name, result.value.service);
|
|
4187
|
+
servicesToSync.push(result.value);
|
|
4188
|
+
} else {
|
|
4189
|
+
globalLogger.error(`\u274C Failed to initialize repository:`, result.reason);
|
|
4190
|
+
}
|
|
3186
4191
|
}
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
}
|
|
3197
|
-
|
|
3198
|
-
)
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
globalLogger.info(`
|
|
4192
|
+
const syncResults = await Promise.allSettled(
|
|
4193
|
+
servicesToSync.map(
|
|
4194
|
+
({ name, service }) => limit(async () => {
|
|
4195
|
+
try {
|
|
4196
|
+
await service.sync();
|
|
4197
|
+
} catch (error) {
|
|
4198
|
+
globalLogger.error(`\u274C Error syncing repository '${name}':`, error);
|
|
4199
|
+
throw error;
|
|
4200
|
+
}
|
|
4201
|
+
})
|
|
4202
|
+
)
|
|
4203
|
+
);
|
|
4204
|
+
const successCount = syncResults.filter((r) => r.status === "fulfilled").length;
|
|
4205
|
+
globalLogger.info(`
|
|
3202
4206
|
\u2705 Successfully synced ${successCount}/${servicesToSync.length} repositories`);
|
|
3203
|
-
|
|
4207
|
+
} else {
|
|
4208
|
+
for (const repoConfig of repositories) {
|
|
4209
|
+
const syncService = new WorktreeSyncService(repoConfig);
|
|
4210
|
+
services.set(repoConfig.name, syncService);
|
|
4211
|
+
}
|
|
3204
4212
|
const uniqueSchedules = [...new Set(repositories.map((r) => r.cronSchedule))];
|
|
3205
4213
|
const displaySchedule = uniqueSchedules.length === 1 ? uniqueSchedules[0] : void 0;
|
|
3206
4214
|
const allServices = Array.from(services.values());
|
|
3207
4215
|
const uiService = new InteractiveUIService(allServices, configPath, displaySchedule);
|
|
3208
|
-
uiService.updateLastSyncTime();
|
|
3209
4216
|
void uiService.calculateAndUpdateDiskSpace();
|
|
3210
4217
|
const cronJobs = /* @__PURE__ */ new Map();
|
|
3211
4218
|
for (const repoConfig of repositories) {
|
|
@@ -3221,11 +4228,14 @@ async function runMultipleRepositories(repositories, runOnce, configPath, maxPar
|
|
|
3221
4228
|
(repo) => limit(async () => {
|
|
3222
4229
|
const service = services.get(repo.name);
|
|
3223
4230
|
if (!service) return;
|
|
3224
|
-
|
|
4231
|
+
uiService.addLog(`Running scheduled sync for: ${repo.name}`);
|
|
3225
4232
|
try {
|
|
4233
|
+
if (!service.isInitialized()) {
|
|
4234
|
+
await service.initialize();
|
|
4235
|
+
}
|
|
3226
4236
|
await service.sync();
|
|
3227
4237
|
} catch (error) {
|
|
3228
|
-
|
|
4238
|
+
uiService.addLog(`Error syncing '${repo.name}': ${error}`, "error");
|
|
3229
4239
|
}
|
|
3230
4240
|
})
|
|
3231
4241
|
)
|
|
@@ -3235,10 +4245,13 @@ async function runMultipleRepositories(repositories, runOnce, configPath, maxPar
|
|
|
3235
4245
|
});
|
|
3236
4246
|
}
|
|
3237
4247
|
}
|
|
3238
|
-
|
|
4248
|
+
uiService.addLog(`\u{1F4CB} ${repositories.length} repositories configured`);
|
|
3239
4249
|
for (const [schedule3] of cronJobs) {
|
|
3240
4250
|
const repoCount = repositories.filter((r) => r.cronSchedule === schedule3).length;
|
|
3241
|
-
|
|
4251
|
+
uiService.addLog(`\u23F0 ${schedule3}: ${repoCount} repository(ies)`);
|
|
4252
|
+
}
|
|
4253
|
+
if (syncOnStart) {
|
|
4254
|
+
await uiService.triggerInitialSync();
|
|
3242
4255
|
}
|
|
3243
4256
|
}
|
|
3244
4257
|
}
|
|
@@ -3246,7 +4259,7 @@ async function listRepositories(configPath, filter) {
|
|
|
3246
4259
|
const configLoader = new ConfigLoaderService();
|
|
3247
4260
|
try {
|
|
3248
4261
|
const configFile = await configLoader.loadConfigFile(configPath);
|
|
3249
|
-
const configDir =
|
|
4262
|
+
const configDir = path10.dirname(path10.resolve(configPath));
|
|
3250
4263
|
let repositories = configFile.repositories.map(
|
|
3251
4264
|
(repo) => configLoader.resolveRepositoryConfig(repo, configFile.defaults, configDir, configFile.retry)
|
|
3252
4265
|
);
|
|
@@ -3287,7 +4300,7 @@ async function main() {
|
|
|
3287
4300
|
}
|
|
3288
4301
|
try {
|
|
3289
4302
|
const configFile = await configLoader.loadConfigFile(options.config);
|
|
3290
|
-
const configDir =
|
|
4303
|
+
const configDir = path10.dirname(path10.resolve(options.config));
|
|
3291
4304
|
let repositories = configFile.repositories.map(
|
|
3292
4305
|
(repo) => configLoader.resolveRepositoryConfig(repo, configFile.defaults, configDir, configFile.retry)
|
|
3293
4306
|
);
|
|
@@ -3312,7 +4325,7 @@ async function main() {
|
|
|
3312
4325
|
}));
|
|
3313
4326
|
}
|
|
3314
4327
|
const maxParallel = configFile.parallelism?.maxRepositories ?? configFile.defaults?.parallelism?.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES;
|
|
3315
|
-
await runMultipleRepositories(repositories, globalRunOnce, options.config, maxParallel);
|
|
4328
|
+
await runMultipleRepositories(repositories, globalRunOnce, options.config, maxParallel, options.syncOnStart);
|
|
3316
4329
|
} catch (error) {
|
|
3317
4330
|
if (error instanceof Error && error.message.includes("Config file not found")) {
|
|
3318
4331
|
console.error(`
|