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 CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import * as path8 from "path";
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 React4 from "react";
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 React3, { useState as useState2, useEffect as useEffect2, useCallback } from "react";
326
- import { Box as Box3, useInput as useInput2 } from "ink";
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" }, "?"), /* @__PURE__ */ React2.createElement(Text2, null, " or "), /* @__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" }, "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" }, "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" }, "q"), /* @__PURE__ */ React2.createElement(Text2, null, " or "), /* @__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"))));
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/App.tsx
378
- var App = ({ repositoryCount, cronSchedule, onManualSync, onReload, onQuit }) => {
379
- const [showHelp, setShowHelp] = useState2(false);
380
- const [status, setStatus] = useState2("idle");
381
- const [lastSyncTime, setLastSyncTime] = useState2(null);
382
- const [diskSpaceUsed, setDiskSpaceUsed] = useState2(null);
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 (key.escape || input2 === "q") {
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 = useCallback(() => {
1045
+ const updateLastSyncTime = useCallback3(() => {
417
1046
  setLastSyncTime(/* @__PURE__ */ new Date());
418
1047
  setStatus("idle");
419
1048
  }, []);
420
- useEffect2(() => {
421
- globalThis.__inkAppMethods = {
422
- updateLastSyncTime,
423
- setStatus,
424
- setDiskSpace: setDiskSpaceUsed
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
- delete globalThis.__inkAppMethods;
1067
+ unsubscribers.forEach((unsub) => unsub());
428
1068
  };
429
- }, [updateLastSyncTime, setStatus]);
430
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(
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
- ), showHelp && /* @__PURE__ */ React3.createElement(HelpModal_default, { onClose: () => setShowHelp(false) }));
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
- console.log(this.prefix() + this.formatMessage(message, args));
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
- console.log(this.prefix() + this.formatMessage(message, args));
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
- console.warn(this.prefix() + this.formatMessage(message, args));
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
- console.error(this.prefix() + message, error);
1445
+ formattedMessage += ` ${error.message}`;
749
1446
  } else if (error) {
750
- console.error(this.prefix() + message, error);
1447
+ formattedMessage += ` ${String(error)}`;
1448
+ }
1449
+ if (this.outputFn) {
1450
+ this.outputFn(formattedMessage, "error");
751
1451
  } else {
752
- console.error(this.prefix() + message);
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
- console.log("\n" + content + "\n");
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(path9, reasons) {
1028
- super(`Worktree at '${path9}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
1029
- this.path = path9;
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
- this.logger.info("Fetching remote branches...");
1373
- await bareGit.fetch(["--all"]);
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
- originalConsoleLog;
2573
- originalConsoleWarn;
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
- redirectConsole() {
2591
- console.log = (...args) => {
2592
- const message = args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ");
2593
- this.originalConsoleLog(message);
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
- restoreConsole() {
2605
- console.log = this.originalConsoleLog;
2606
- console.warn = this.originalConsoleWarn;
2607
- console.error = this.originalConsoleError;
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__ */ React4.createElement(
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("Manual sync failed:", 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
- const methods = globalThis.__inkAppMethods;
2750
- if (methods && methods.updateLastSyncTime) {
2751
- methods.updateLastSyncTime();
2752
- }
3633
+ appEvents.emit("updateLastSyncTime");
2753
3634
  }
2754
3635
  setStatus(status) {
2755
- const methods = globalThis.__inkAppMethods;
2756
- if (methods && methods.setStatus) {
2757
- methods.setStatus(status);
2758
- }
3636
+ appEvents.emit("setStatus", status);
2759
3637
  }
2760
3638
  setDiskSpace(diskSpace) {
2761
- const methods = globalThis.__inkAppMethods;
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
- delete globalThis.__inkAppMethods;
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 path7 from "path";
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 fs6 from "fs/promises";
2901
- import * as path6 from "path";
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 = path6.dirname(configPath);
2932
- await fs6.mkdir(configDir, { recursive: true });
2933
- const worktreeDirRelative = path6.relative(configDir, config.worktreeDir);
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 = path6.relative(configDir, config.bareRepoDir);
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 fs6.writeFile(configPath, configContent, "utf-8");
3963
+ await fs7.writeFile(configPath, configContent, "utf-8");
2961
3964
  }
2962
3965
  function getDefaultConfigPath() {
2963
- return path6.join(process.cwd(), "sync-worktrees.config.js");
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 (!path7.isAbsolute(worktreeDir)) {
3006
- worktreeDir = path7.resolve(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 (!path7.isAbsolute(bareRepoDir)) {
3026
- bareRepoDir = path7.resolve(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 (!path7.isAbsolute(configPath)) {
3098
- configPath = path7.resolve(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 ${path7.relative(process.cwd(), configPath)}`);
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
- const initResults = await Promise.allSettled(
3160
- repositories.map(
3161
- (repoConfig) => limit(async () => {
3162
- const repoLogger = Logger.createDefault(repoConfig.name, repoConfig.debug);
3163
- repoLogger.info(`
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
- repoLogger.info(` URL: ${repoConfig.repoUrl}`);
3166
- repoLogger.info(` Worktrees: ${repoConfig.worktreeDir}`);
3167
- if (repoConfig.bareRepoDir) {
3168
- repoLogger.info(` Bare repo: ${repoConfig.bareRepoDir}`);
3169
- }
3170
- if (!repoConfig.logger) {
3171
- repoConfig.logger = repoLogger;
3172
- }
3173
- const syncService = new WorktreeSyncService(repoConfig);
3174
- await syncService.initialize();
3175
- return { name: repoConfig.name, service: syncService };
3176
- })
3177
- )
3178
- );
3179
- const servicesToSync = [];
3180
- for (const result of initResults) {
3181
- if (result.status === "fulfilled") {
3182
- services.set(result.value.name, result.value.service);
3183
- servicesToSync.push(result.value);
3184
- } else {
3185
- globalLogger.error(`\u274C Failed to initialize repository:`, result.reason);
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
- const syncResults = await Promise.allSettled(
3189
- servicesToSync.map(
3190
- ({ name, service }) => limit(async () => {
3191
- try {
3192
- await service.sync();
3193
- } catch (error) {
3194
- globalLogger.error(`\u274C Error syncing repository '${name}':`, error);
3195
- throw error;
3196
- }
3197
- })
3198
- )
3199
- );
3200
- const successCount = syncResults.filter((r) => r.status === "fulfilled").length;
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
- if (!runOnce) {
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
- globalLogger.info(`Running scheduled sync for: ${repo.name}`);
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
- globalLogger.error(`Error syncing '${repo.name}':`, error);
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
- globalLogger.info(`All ${repositories.length} repositories scheduled`);
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
- globalLogger.info(`${schedule3}: ${repoCount} repository(ies)`);
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 = path8.dirname(path8.resolve(configPath));
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 = path8.dirname(path8.resolve(options.config));
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(`