@karmaniverous/jeeves-watcher 0.18.10 → 0.18.12

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.
@@ -226,7 +226,7 @@ async function startFromConfig(configPath, descriptor) {
226
226
  const config = await loadConfig(resolvedPath);
227
227
  // Dynamic import breaks the circular dependency between this module
228
228
  // and app/index.ts (which defines JeevesWatcher and re-exports startFromConfig).
229
- const { JeevesWatcher } = await import('./index-B_g6RfR8.js');
229
+ const { JeevesWatcher } = await import('./index-C2iJGo8u.js');
230
230
  const app = new JeevesWatcher(config, resolvedPath, descriptor);
231
231
  installShutdownHandlers(() => app.stop());
232
232
  await app.start();
@@ -621,6 +621,39 @@ async function retry(fn, options) {
621
621
  */
622
622
  /** Promisified execFile for git commands. */
623
623
  const execFileAsync = promisify(execFile);
624
+ /** Standard timeout (ms) for most git operations. */
625
+ const GIT_TIMEOUT_STANDARD = 30_000;
626
+ /** Extended timeout (ms) for cherry-pick operations. */
627
+ const GIT_TIMEOUT_CHERRY_PICK = 120_000;
628
+ /** Timeout (ms) for push operations. */
629
+ const GIT_TIMEOUT_PUSH = 60_000;
630
+ /**
631
+ * Extract string fields from a child-process error.
632
+ * Node's execFile wraps failures in an Error with `stderr`, `stdout`, and
633
+ * `code` properties that aren't part of the Error type.
634
+ */
635
+ function getExecErrorFields(error) {
636
+ if (!(error instanceof Error)) {
637
+ return { message: String(error), stderr: '', stdout: '' };
638
+ }
639
+ const rec = error;
640
+ const stderr = typeof rec['stderr'] === 'string' ? rec['stderr'] : '';
641
+ const stdout = typeof rec['stdout'] === 'string' ? rec['stdout'] : '';
642
+ return { message: error.message || '', stderr, stdout };
643
+ }
644
+ /**
645
+ * Build an authenticated push URL by injecting a token into an HTTPS remote.
646
+ * Non-HTTPS URLs are returned as-is.
647
+ *
648
+ * @param remoteUrl - The remote repository URL.
649
+ * @param accessToken - Optional access token to inject.
650
+ * @returns The URL with the token injected if applicable.
651
+ */
652
+ function buildAuthenticatedPushUrl(remoteUrl, accessToken) {
653
+ if (!accessToken)
654
+ return remoteUrl;
655
+ return remoteUrl.replace(/^https:\/\//, `https://${encodeURIComponent(accessToken)}@`);
656
+ }
624
657
  /**
625
658
  * Stage files via stdin to avoid ENAMETOOLONG on Windows.
626
659
  *
@@ -632,22 +665,34 @@ const execFileAsync = promisify(execFile);
632
665
  *
633
666
  * @param files - Absolute paths of files to stage.
634
667
  * @param cwd - Repository root (working directory for git).
668
+ * @param timeoutMs - Kill the child process after this many milliseconds. Default: 30000.
635
669
  */
636
- function gitAddViaStdin(files, cwd) {
670
+ function gitAddViaStdin(files, cwd, timeoutMs = 30_000) {
637
671
  return new Promise((resolve, reject) => {
672
+ let timedOut = false;
638
673
  const child = execFile('git', ['add', '--pathspec-from-file=-', '--pathspec-file-nul'], { cwd }, (error) => {
674
+ clearTimeout(timer);
675
+ if (timedOut)
676
+ return;
639
677
  if (error)
640
678
  reject(error);
641
679
  else
642
680
  resolve();
643
681
  });
682
+ const timer = setTimeout(() => {
683
+ timedOut = true;
684
+ child.kill();
685
+ reject(new Error(`git add timed out after ${String(timeoutMs)}ms`));
686
+ }, timeoutMs);
644
687
  if (!child.stdin) {
688
+ clearTimeout(timer);
645
689
  reject(new Error('Failed to open stdin for git add'));
646
690
  return;
647
691
  }
648
692
  // Suppress EPIPE — expected if git exits before we finish writing
649
693
  child.stdin.on('error', (err) => {
650
694
  if (err.code !== 'EPIPE') {
695
+ clearTimeout(timer);
651
696
  reject(err);
652
697
  }
653
698
  });
@@ -1581,4 +1626,4 @@ const watcherDescriptor = {
1581
1626
  const program = createServiceCli(watcherDescriptor);
1582
1627
  program.parse();
1583
1628
 
1584
- export { CONFIG_WATCH_VALID_SCOPES as C, GitignoreFilter as G, VALID_REINDEX_SCOPES as V, executeReindex as a, gitAddViaStdin as b, normalizePathCase as c, normalizeSlashes as d, execFileAsync as e, findRootForPath as f, getWatchRootBases as g, startFromConfig as h, isPathWatched as i, loadConfig as l, mergeInferenceRules as m, normalizeError as n, retry as r, sleep as s };
1629
+ export { CONFIG_WATCH_VALID_SCOPES as C, GIT_TIMEOUT_CHERRY_PICK as G, VALID_REINDEX_SCOPES as V, GIT_TIMEOUT_PUSH as a, GIT_TIMEOUT_STANDARD as b, GitignoreFilter as c, buildAuthenticatedPushUrl as d, execFileAsync as e, executeReindex as f, findRootForPath as g, getExecErrorFields as h, getWatchRootBases as i, gitAddViaStdin as j, isPathWatched as k, loadConfig as l, mergeInferenceRules as m, normalizeError as n, normalizePathCase as o, normalizeSlashes as p, startFromConfig as q, retry as r, sleep as s };
package/dist/index.d.ts CHANGED
@@ -1297,6 +1297,7 @@ declare class VcsManager {
1297
1297
  private commitInFlight;
1298
1298
  private consecutiveCommitFailures;
1299
1299
  private started;
1300
+ private paused;
1300
1301
  private isBaseline;
1301
1302
  private _lastPushTime;
1302
1303
  constructor(rootPath: string, config: VcsConfig, logger: pino.Logger, commitMessageGenerator?: CommitMessageGenerator, remoteUrl?: string, accessToken?: string);
@@ -1306,10 +1307,13 @@ declare class VcsManager {
1306
1307
  * Begin accepting file changes. Sets the manager to started state and
1307
1308
  * starts the squash manager if configured.
1308
1309
  *
1310
+ * Performs startup orphan detection/recovery before starting the squash
1311
+ * manager, ensuring the repo is on the configured branch.
1312
+ *
1309
1313
  * Baseline mode (commit prefix `"baseline:"`) remains active until
1310
1314
  * {@link endBaseline} is called by the coordinator after the initial scan.
1311
1315
  */
1312
- start(): void;
1316
+ start(): Promise<void>;
1313
1317
  /**
1314
1318
  * Signal that the initial filesystem scan is complete.
1315
1319
  * Clears the baseline flag so subsequent commits use normal "watcher: batch" messages.
@@ -1335,6 +1339,18 @@ declare class VcsManager {
1335
1339
  * @param filePath - Absolute path of the deleted file.
1336
1340
  */
1337
1341
  handleUnlink(filePath: string): void;
1342
+ /**
1343
+ * Pause the commit pipeline. Drains pending commits via flush(), then
1344
+ * sets a flag that makes commitBatch queue instead of execute.
1345
+ * Used by SquashManager to coordinate squash operations.
1346
+ */
1347
+ pause(): Promise<void>;
1348
+ /**
1349
+ * Resume the commit pipeline after a pause.
1350
+ * Re-enables normal commit operations and starts a throttle timer
1351
+ * if there are pending files that accumulated during the pause.
1352
+ */
1353
+ resume(): void;
1338
1354
  /**
1339
1355
  * Flush all pending files immediately.
1340
1356
  */
@@ -1360,9 +1376,14 @@ declare class VcsManager {
1360
1376
  * Check for and remove stale index.lock files before retrying.
1361
1377
  */
1362
1378
  private removeStaleLock;
1379
+ /**
1380
+ * Check whether a git error represents a "nothing to commit" state.
1381
+ * Git outputs this message to stdout (not stderr) on exit code 1.
1382
+ */
1383
+ private isNothingToCommitError;
1363
1384
  /**
1364
1385
  * Commit a batch of files to the git repo with index.lock retry,
1365
- * stale lock detection, circuit breaker, and re-queue cap.
1386
+ * stale lock detection, circuit breaker, pause support, and re-queue cap.
1366
1387
  */
1367
1388
  private commitBatch;
1368
1389
  }
@@ -1383,8 +1404,9 @@ declare class VcsCoordinator {
1383
1404
  constructor(config: JeevesWatcherConfig, logger: pino.Logger);
1384
1405
  /**
1385
1406
  * Start all VcsManager instances.
1407
+ * Runs startup orphan detection/recovery for each root.
1386
1408
  */
1387
- start(): void;
1409
+ start(): Promise<void>;
1388
1410
  /**
1389
1411
  * Route a file event to the correct root's VcsManager.
1390
1412
  *
@@ -1939,15 +1961,31 @@ interface CommitInfo {
1939
1961
  * then cherry-picks retained commits on top. Force-pushes to remote
1940
1962
  * if configured (D13).
1941
1963
  */
1964
+ /** Options for SquashManager construction beyond the required params. */
1965
+ interface SquashManagerOptions {
1966
+ /** Git branch name for squash operations. Default: "master". */
1967
+ branch?: string;
1968
+ /** Called before squash to drain and pause the commit pipeline. */
1969
+ pauseCommits?: () => Promise<void>;
1970
+ /** Called after squash to resume the commit pipeline. */
1971
+ resumeCommits?: () => void;
1972
+ /** Remote URL for force-push after squash. */
1973
+ remoteUrl?: string;
1974
+ /** Access token for HTTPS push authentication. */
1975
+ accessToken?: string;
1976
+ }
1942
1977
  declare class SquashManager {
1943
1978
  private readonly rootPath;
1944
1979
  private readonly retention;
1980
+ private readonly branch;
1981
+ private readonly pauseCommits;
1982
+ private readonly resumeCommits;
1945
1983
  private readonly remoteUrl;
1946
1984
  private readonly accessToken;
1947
1985
  private readonly logger;
1948
1986
  private intervalTimer;
1949
1987
  private running;
1950
- constructor(rootPath: string, retention: VcsRetentionConfig, logger: pino.Logger, remoteUrl?: string, accessToken?: string);
1988
+ constructor(rootPath: string, retention: VcsRetentionConfig, logger: pino.Logger, options?: SquashManagerOptions);
1951
1989
  /**
1952
1990
  * Start the cron-based squash scheduler.
1953
1991
  * Checks every 60 seconds if the cron expression matches.
@@ -1959,6 +1997,9 @@ declare class SquashManager {
1959
1997
  stop(): void;
1960
1998
  /**
1961
1999
  * Run the squash operation. Exposed publicly for testing.
2000
+ *
2001
+ * Coordinates with VcsManager by calling pause/resume callbacks
2002
+ * to drain and suspend the commit pipeline during the squash.
1962
2003
  */
1963
2004
  runSquash(): Promise<SquashResult>;
1964
2005
  /**
@@ -1978,6 +2019,7 @@ declare class SquashManager {
1978
2019
  calculateRetentionBoundary(commits: CommitInfo[]): number;
1979
2020
  /**
1980
2021
  * Perform the squash: create orphan with baseline + cherry-pick retained commits.
2022
+ * Uses the configured branch name instead of dynamic detection (Bug 1).
1981
2023
  */
1982
2024
  private performSquash;
1983
2025
  /**