@jonit-dev/night-watch-cli 1.7.56 → 1.7.58

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/cli.js CHANGED
@@ -3,15 +3,6 @@ import 'reflect-metadata';
3
3
 
4
4
  // dist/cli.js
5
5
  import "reflect-metadata";
6
- import "reflect-metadata";
7
- import "reflect-metadata";
8
- import "reflect-metadata";
9
- import "reflect-metadata";
10
- import "reflect-metadata";
11
- import "reflect-metadata";
12
- import "reflect-metadata";
13
- import "reflect-metadata";
14
- import "reflect-metadata";
15
6
  import * as fs from "fs";
16
7
  import * as path from "path";
17
8
  import { fileURLToPath } from "url";
@@ -139,14 +130,15 @@ import * as fs27 from "fs";
139
130
  import * as path27 from "path";
140
131
  import * as fs28 from "fs";
141
132
  import * as path28 from "path";
142
- import { spawn as spawn5 } from "child_process";
133
+ import { execSync as execSync5, spawn as spawn5 } from "child_process";
143
134
  import { Router } from "express";
144
135
  import { Router as Router2 } from "express";
145
136
  import { Router as Router3 } from "express";
146
137
  import { Router as Router4 } from "express";
138
+ import { CronExpressionParser } from "cron-parser";
147
139
  import * as fs29 from "fs";
148
140
  import * as path29 from "path";
149
- import { execSync as execSync5 } from "child_process";
141
+ import { execSync as execSync6 } from "child_process";
150
142
  import { Router as Router5 } from "express";
151
143
  import * as path30 from "path";
152
144
  import { Router as Router6 } from "express";
@@ -154,7 +146,7 @@ import { Router as Router7 } from "express";
154
146
  import * as path31 from "path";
155
147
  import { Router as Router8 } from "express";
156
148
  import { Router as Router9 } from "express";
157
- import { CronExpressionParser } from "cron-parser";
149
+ import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
158
150
  import { spawnSync } from "child_process";
159
151
  import * as fs32 from "fs";
160
152
  import * as path33 from "path";
@@ -162,7 +154,7 @@ import * as fs33 from "fs";
162
154
  import * as path34 from "path";
163
155
  import chalk3 from "chalk";
164
156
  import chalk4 from "chalk";
165
- import { execSync as execSync6 } from "child_process";
157
+ import { execSync as execSync7 } from "child_process";
166
158
  import * as fs34 from "fs";
167
159
  import * as readline3 from "readline";
168
160
  import * as fs35 from "fs";
@@ -263,8 +255,8 @@ var init_constants = __esm({
263
255
  DEFAULT_PRD_DIR = "docs/prds";
264
256
  DEFAULT_MAX_RUNTIME = 7200;
265
257
  DEFAULT_REVIEWER_MAX_RUNTIME = 3600;
266
- DEFAULT_CRON_SCHEDULE = "0 0-21 * * *";
267
- DEFAULT_REVIEWER_SCHEDULE = "0 0,3,6,9,12,15,18,21 * * *";
258
+ DEFAULT_CRON_SCHEDULE = "5 */3 * * *";
259
+ DEFAULT_REVIEWER_SCHEDULE = "25 */6 * * *";
268
260
  DEFAULT_CRON_SCHEDULE_OFFSET = 0;
269
261
  DEFAULT_MAX_RETRIES = 3;
270
262
  DEFAULT_REVIEWER_MAX_RETRIES = 2;
@@ -277,7 +269,7 @@ var init_constants = __esm({
277
269
  DEFAULT_EXECUTOR_ENABLED = true;
278
270
  DEFAULT_REVIEWER_ENABLED = true;
279
271
  DEFAULT_PROVIDER_ENV = {};
280
- DEFAULT_FALLBACK_ON_RATE_LIMIT = false;
272
+ DEFAULT_FALLBACK_ON_RATE_LIMIT = true;
281
273
  DEFAULT_CLAUDE_MODEL = "sonnet";
282
274
  VALID_CLAUDE_MODELS = ["sonnet", "opus"];
283
275
  CLAUDE_MODEL_IDS = {
@@ -286,7 +278,7 @@ var init_constants = __esm({
286
278
  };
287
279
  DEFAULT_NOTIFICATIONS = { webhooks: [] };
288
280
  DEFAULT_PRD_PRIORITY = [];
289
- DEFAULT_SLICER_SCHEDULE = "0 */6 * * *";
281
+ DEFAULT_SLICER_SCHEDULE = "35 */12 * * *";
290
282
  DEFAULT_SLICER_MAX_RUNTIME = 600;
291
283
  DEFAULT_ROADMAP_SCANNER = {
292
284
  enabled: true,
@@ -307,7 +299,7 @@ var init_constants = __esm({
307
299
  DEFAULT_AUTO_MERGE_METHOD = "squash";
308
300
  VALID_MERGE_METHODS = ["squash", "merge", "rebase"];
309
301
  DEFAULT_QA_ENABLED = true;
310
- DEFAULT_QA_SCHEDULE = "30 1,7,13,19 * * *";
302
+ DEFAULT_QA_SCHEDULE = "45 2,14 * * *";
311
303
  DEFAULT_QA_MAX_RUNTIME = 3600;
312
304
  DEFAULT_QA_ARTIFACTS = "both";
313
305
  DEFAULT_QA_SKIP_LABEL = "skip-qa";
@@ -323,7 +315,7 @@ var init_constants = __esm({
323
315
  };
324
316
  QA_LOG_NAME = "night-watch-qa";
325
317
  DEFAULT_AUDIT_ENABLED = true;
326
- DEFAULT_AUDIT_SCHEDULE = "0 3 * * *";
318
+ DEFAULT_AUDIT_SCHEDULE = "50 3 * * 1";
327
319
  DEFAULT_AUDIT_MAX_RUNTIME = 1800;
328
320
  DEFAULT_AUDIT = {
329
321
  enabled: DEFAULT_AUDIT_ENABLED,
@@ -376,6 +368,7 @@ function getDefaultConfig() {
376
368
  // Cron scheduling
377
369
  cronSchedule: DEFAULT_CRON_SCHEDULE,
378
370
  reviewerSchedule: DEFAULT_REVIEWER_SCHEDULE,
371
+ scheduleBundleId: null,
379
372
  cronScheduleOffset: DEFAULT_CRON_SCHEDULE_OFFSET,
380
373
  maxRetries: DEFAULT_MAX_RETRIES,
381
374
  // Reviewer retry configuration
@@ -443,6 +436,13 @@ function normalizeConfig(rawConfig) {
443
436
  normalized.maxLogSize = readNumber(rawConfig.maxLogSize) ?? readNumber(logging?.maxLogSize);
444
437
  normalized.cronSchedule = readString(rawConfig.cronSchedule) ?? readString(cron?.executorSchedule);
445
438
  normalized.reviewerSchedule = readString(rawConfig.reviewerSchedule) ?? readString(cron?.reviewerSchedule);
439
+ const rawScheduleBundleId = rawConfig.scheduleBundleId;
440
+ if (typeof rawScheduleBundleId === "string") {
441
+ const trimmed = rawScheduleBundleId.trim();
442
+ normalized.scheduleBundleId = trimmed.length > 0 ? trimmed : null;
443
+ } else if (rawScheduleBundleId === null) {
444
+ normalized.scheduleBundleId = null;
445
+ }
446
446
  normalized.cronScheduleOffset = readNumber(rawConfig.cronScheduleOffset);
447
447
  normalized.maxRetries = readNumber(rawConfig.maxRetries);
448
448
  normalized.reviewerMaxRetries = readNumber(rawConfig.reviewerMaxRetries);
@@ -4086,12 +4086,50 @@ var init_prd_states = __esm({
4086
4086
  init_repositories();
4087
4087
  }
4088
4088
  });
4089
+ function isNightWatchEntry(line) {
4090
+ if (line.includes(CRONTAB_MARKER_PREFIX)) {
4091
+ return true;
4092
+ }
4093
+ if (line.includes("night-watch")) {
4094
+ return true;
4095
+ }
4096
+ return LEGACY_SCRIPT_NAMES.some((scriptName) => line.includes(scriptName));
4097
+ }
4098
+ function normalizePathValue(value) {
4099
+ const trimmed = value.trim();
4100
+ let unquoted = trimmed;
4101
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"')) {
4102
+ unquoted = trimmed.slice(1, -1);
4103
+ }
4104
+ return unquoted.replace(/\\ /g, " ").replace(/\/+$/, "");
4105
+ }
4106
+ function extractCdPath(line) {
4107
+ const match = line.match(/\bcd\s+((?:'[^']*'|"[^"]*"|[^;&])+?)\s*(?:&&|;)/);
4108
+ if (!match) {
4109
+ return null;
4110
+ }
4111
+ return normalizePathValue(match[1]);
4112
+ }
4089
4113
  function isEntryForProject(line, projectDir) {
4090
- if (!line.includes(CRONTAB_MARKER_PREFIX)) {
4114
+ if (!isNightWatchEntry(line)) {
4091
4115
  return false;
4092
4116
  }
4093
4117
  const normalized = projectDir.replace(/\/+$/, "");
4094
- const candidates = [`cd ${normalized}`, `cd '${normalized}'`, `cd "${normalized}"`];
4118
+ const extractedPath = extractCdPath(line);
4119
+ if (extractedPath !== null) {
4120
+ return extractedPath === normalized;
4121
+ }
4122
+ const escaped = normalized.replace(/ /g, "\\ ");
4123
+ const candidates = [
4124
+ `cd ${normalized}`,
4125
+ `cd ${normalized}/`,
4126
+ `cd '${normalized}'`,
4127
+ `cd '${normalized}/'`,
4128
+ `cd "${normalized}"`,
4129
+ `cd "${normalized}/"`,
4130
+ `cd ${escaped}`,
4131
+ `cd ${escaped}/`
4132
+ ];
4095
4133
  return candidates.some((candidate) => line.includes(candidate));
4096
4134
  }
4097
4135
  function readCrontab() {
@@ -4176,10 +4214,19 @@ function removeEntriesForProject(projectDir, marker) {
4176
4214
  return removedCount;
4177
4215
  }
4178
4216
  var CRONTAB_MARKER_PREFIX;
4217
+ var LEGACY_SCRIPT_NAMES;
4179
4218
  var init_crontab = __esm({
4180
4219
  "../core/dist/utils/crontab.js"() {
4181
4220
  "use strict";
4182
4221
  CRONTAB_MARKER_PREFIX = "# night-watch-cli:";
4222
+ LEGACY_SCRIPT_NAMES = [
4223
+ "night-watch-cron.sh",
4224
+ "night-watch-pr-reviewer-cron.sh",
4225
+ "night-watch-qa-cron.sh",
4226
+ "night-watch-audit-cron.sh",
4227
+ "night-watch-slice-cron.sh",
4228
+ "night-watch-slicer-cron.sh"
4229
+ ];
4183
4230
  }
4184
4231
  });
4185
4232
  function getProjectName(projectDir) {
@@ -5011,6 +5058,9 @@ var init_claim_manager = __esm({
5011
5058
  init_constants();
5012
5059
  }
5013
5060
  });
5061
+ function isPlainObject(value) {
5062
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5063
+ }
5014
5064
  function saveConfig(projectDir, changes) {
5015
5065
  const configPath = path8.join(projectDir, CONFIG_FILE_NAME);
5016
5066
  try {
@@ -5022,8 +5072,8 @@ function saveConfig(projectDir, changes) {
5022
5072
  const merged = { ...existing };
5023
5073
  for (const [key, value] of Object.entries(changes)) {
5024
5074
  if (value !== void 0) {
5025
- if (key === "notifications" && existing.notifications && typeof existing.notifications === "object") {
5026
- merged.notifications = { ...existing.notifications, ...value };
5075
+ if (PARTIAL_MERGE_KEYS.has(key) && isPlainObject(existing[key]) && isPlainObject(value)) {
5076
+ merged[key] = { ...existing[key], ...value };
5027
5077
  } else {
5028
5078
  merged[key] = value;
5029
5079
  }
@@ -5038,10 +5088,12 @@ function saveConfig(projectDir, changes) {
5038
5088
  };
5039
5089
  }
5040
5090
  }
5091
+ var PARTIAL_MERGE_KEYS;
5041
5092
  var init_config_writer = __esm({
5042
5093
  "../core/dist/utils/config-writer.js"() {
5043
5094
  "use strict";
5044
5095
  init_constants();
5096
+ PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set(["notifications", "qa", "audit", "roadmapScanner"]);
5045
5097
  }
5046
5098
  });
5047
5099
  function getHistoryPath() {
@@ -8854,11 +8906,11 @@ function performInstall(projectDir, config, options) {
8854
8906
  const executorLog = path22.join(logDir, "executor.log");
8855
8907
  const reviewerLog = path22.join(logDir, "reviewer.log");
8856
8908
  if (!options?.force) {
8857
- const existingEntries = Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]));
8858
- if (existingEntries.length > 0) {
8909
+ const existingEntries2 = Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]));
8910
+ if (existingEntries2.length > 0) {
8859
8911
  return {
8860
8912
  success: false,
8861
- entries: existingEntries,
8913
+ entries: existingEntries2,
8862
8914
  error: "Already installed. Uninstall first or use force."
8863
8915
  };
8864
8916
  }
@@ -8905,8 +8957,10 @@ function performInstall(projectDir, config, options) {
8905
8957
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8906
8958
  entries.push(auditEntry);
8907
8959
  }
8960
+ const existingEntries = new Set(Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])));
8908
8961
  const currentCrontab = readCrontab();
8909
- const newCrontab = [...currentCrontab, ...entries];
8962
+ const baseCrontab = options?.force ? currentCrontab.filter((line) => !existingEntries.has(line) && !line.includes(marker)) : currentCrontab;
8963
+ const newCrontab = [...baseCrontab, ...entries];
8910
8964
  writeCrontab(newCrontab);
8911
8965
  return { success: true, entries };
8912
8966
  } catch (err) {
@@ -8918,7 +8972,7 @@ function performInstall(projectDir, config, options) {
8918
8972
  }
8919
8973
  }
8920
8974
  function installCommand(program2) {
8921
- program2.command("install").description("Add crontab entries for automated execution").option("-s, --schedule <cron>", "Cron schedule for PRD executor").option("--reviewer-schedule <cron>", "Cron schedule for reviewer").option("--no-reviewer", "Skip installing reviewer cron").option("--no-slicer", "Skip installing slicer cron").option("--no-qa", "Skip installing QA cron").option("--no-audit", "Skip installing audit cron").action(async (options) => {
8975
+ program2.command("install").description("Add crontab entries for automated execution").option("-s, --schedule <cron>", "Cron schedule for PRD executor").option("--reviewer-schedule <cron>", "Cron schedule for reviewer").option("--no-reviewer", "Skip installing reviewer cron").option("--no-slicer", "Skip installing slicer cron").option("--no-qa", "Skip installing QA cron").option("--no-audit", "Skip installing audit cron").option("-f, --force", "Replace existing cron entries for this project").action(async (options) => {
8922
8976
  try {
8923
8977
  const projectDir = process.cwd();
8924
8978
  const config = loadConfig(projectDir);
@@ -8935,13 +8989,13 @@ function installCommand(program2) {
8935
8989
  const executorLog = path22.join(logDir, "executor.log");
8936
8990
  const reviewerLog = path22.join(logDir, "reviewer.log");
8937
8991
  const existingEntries = Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]));
8938
- if (existingEntries.length > 0) {
8992
+ if (existingEntries.length > 0 && !options.force) {
8939
8993
  warn(`Night Watch is already installed for ${projectName}.`);
8940
8994
  console.log();
8941
8995
  dim("Existing crontab entries:");
8942
8996
  existingEntries.forEach((entry) => dim(` ${entry}`));
8943
8997
  console.log();
8944
- dim("Run 'night-watch uninstall' first to reinstall.");
8998
+ dim("Run 'night-watch install --force' to replace them.");
8945
8999
  return;
8946
9000
  }
8947
9001
  const entries = [];
@@ -8989,8 +9043,10 @@ function installCommand(program2) {
8989
9043
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8990
9044
  entries.push(auditEntry);
8991
9045
  }
9046
+ const existingEntrySet = new Set(existingEntries);
8992
9047
  const currentCrontab = readCrontab();
8993
- const newCrontab = [...currentCrontab, ...entries];
9048
+ const baseCrontab = options.force ? currentCrontab.filter((line) => !existingEntrySet.has(line) && !line.includes(marker)) : currentCrontab;
9049
+ const newCrontab = [...baseCrontab, ...entries];
8994
9050
  writeCrontab(newCrontab);
8995
9051
  success(`Night Watch installed successfully for ${projectName}!`);
8996
9052
  console.log();
@@ -12143,6 +12199,24 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
12143
12199
  });
12144
12200
  }
12145
12201
  }
12202
+ function formatCommandError(error2) {
12203
+ if (!(error2 instanceof Error)) {
12204
+ return String(error2);
12205
+ }
12206
+ const withStreams = error2;
12207
+ const stderr = typeof withStreams.stderr === "string" ? withStreams.stderr : withStreams.stderr?.toString("utf-8") ?? "";
12208
+ const stdout = typeof withStreams.stdout === "string" ? withStreams.stdout : withStreams.stdout?.toString("utf-8") ?? "";
12209
+ const output = stderr.trim() || stdout.trim();
12210
+ return output || error2.message;
12211
+ }
12212
+ function runCliCommand(projectDir, args) {
12213
+ execSync5(`night-watch ${args.join(" ")}`, {
12214
+ cwd: projectDir,
12215
+ encoding: "utf-8",
12216
+ stdio: "pipe",
12217
+ env: process.env
12218
+ });
12219
+ }
12146
12220
  function createActionRouteHandlers(ctx) {
12147
12221
  const router = Router({ mergeParams: true });
12148
12222
  const p = ctx.pathPrefix;
@@ -12165,10 +12239,22 @@ function createActionRouteHandlers(ctx) {
12165
12239
  spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
12166
12240
  });
12167
12241
  router.post(`/${p}install-cron`, (req, res) => {
12168
- spawnAction2(ctx.getProjectDir(req), ["install"], req, res);
12242
+ const projectDir = ctx.getProjectDir(req);
12243
+ try {
12244
+ runCliCommand(projectDir, ["install", "--force"]);
12245
+ res.json({ started: true });
12246
+ } catch (error2) {
12247
+ res.status(500).json({ error: formatCommandError(error2) });
12248
+ }
12169
12249
  });
12170
12250
  router.post(`/${p}uninstall-cron`, (req, res) => {
12171
- spawnAction2(ctx.getProjectDir(req), ["uninstall"], req, res);
12251
+ const projectDir = ctx.getProjectDir(req);
12252
+ try {
12253
+ runCliCommand(projectDir, ["uninstall", "--keep-logs"]);
12254
+ res.json({ started: true });
12255
+ } catch (error2) {
12256
+ res.status(500).json({ error: formatCommandError(error2) });
12257
+ }
12172
12258
  });
12173
12259
  router.post(`/${p}cancel`, async (req, res) => {
12174
12260
  try {
@@ -12558,6 +12644,26 @@ function createProjectBoardRoutes() {
12558
12644
  });
12559
12645
  }
12560
12646
  init_dist();
12647
+ function isValidCronExpression(value) {
12648
+ try {
12649
+ CronExpressionParser.parse(value.trim());
12650
+ return true;
12651
+ } catch {
12652
+ return false;
12653
+ }
12654
+ }
12655
+ function validateCronField(fieldName, value) {
12656
+ if (value === void 0) {
12657
+ return null;
12658
+ }
12659
+ if (typeof value !== "string" || value.trim().length === 0) {
12660
+ return `${fieldName} must be a non-empty string`;
12661
+ }
12662
+ if (!isValidCronExpression(value)) {
12663
+ return `${fieldName} must be a valid cron expression`;
12664
+ }
12665
+ return null;
12666
+ }
12561
12667
  function validateConfigChanges(changes) {
12562
12668
  if (typeof changes !== "object" || changes === null) {
12563
12669
  return "Invalid request body";
@@ -12610,11 +12716,18 @@ function validateConfigChanges(changes) {
12610
12716
  if (changes.prdPriority !== void 0 && (!Array.isArray(changes.prdPriority) || !changes.prdPriority.every((p) => typeof p === "string"))) {
12611
12717
  return "prdPriority must be an array of strings";
12612
12718
  }
12613
- if (changes.cronSchedule !== void 0 && (typeof changes.cronSchedule !== "string" || changes.cronSchedule.trim().length === 0)) {
12614
- return "cronSchedule must be a non-empty string";
12719
+ const rootCronFields = [
12720
+ ["cronSchedule", changes.cronSchedule],
12721
+ ["reviewerSchedule", changes.reviewerSchedule]
12722
+ ];
12723
+ for (const [fieldName, value] of rootCronFields) {
12724
+ const cronError = validateCronField(fieldName, value);
12725
+ if (cronError) {
12726
+ return cronError;
12727
+ }
12615
12728
  }
12616
- if (changes.reviewerSchedule !== void 0 && (typeof changes.reviewerSchedule !== "string" || changes.reviewerSchedule.trim().length === 0)) {
12617
- return "reviewerSchedule must be a non-empty string";
12729
+ if (changes.scheduleBundleId !== void 0 && changes.scheduleBundleId !== null && (typeof changes.scheduleBundleId !== "string" || changes.scheduleBundleId.trim().length === 0)) {
12730
+ return "scheduleBundleId must be a non-empty string or null";
12618
12731
  }
12619
12732
  if (changes.notifications?.webhooks !== void 0) {
12620
12733
  if (!Array.isArray(changes.notifications.webhooks)) {
@@ -12641,6 +12754,19 @@ function validateConfigChanges(changes) {
12641
12754
  if (rs.autoScanInterval !== void 0 && (typeof rs.autoScanInterval !== "number" || rs.autoScanInterval < 30)) {
12642
12755
  return "roadmapScanner.autoScanInterval must be a number >= 30";
12643
12756
  }
12757
+ const slicerScheduleError = validateCronField("roadmapScanner.slicerSchedule", rs.slicerSchedule);
12758
+ if (slicerScheduleError) {
12759
+ return slicerScheduleError;
12760
+ }
12761
+ if (rs.slicerMaxRuntime !== void 0 && (typeof rs.slicerMaxRuntime !== "number" || rs.slicerMaxRuntime < 60)) {
12762
+ return "roadmapScanner.slicerMaxRuntime must be a number >= 60";
12763
+ }
12764
+ if (rs.priorityMode !== void 0 && rs.priorityMode !== "roadmap-first" && rs.priorityMode !== "audit-first") {
12765
+ return "roadmapScanner.priorityMode must be one of: roadmap-first, audit-first";
12766
+ }
12767
+ if (rs.issueColumn !== void 0 && rs.issueColumn !== "Draft" && rs.issueColumn !== "Ready") {
12768
+ return "roadmapScanner.issueColumn must be one of: Draft, Ready";
12769
+ }
12644
12770
  }
12645
12771
  if (changes.providerEnv !== void 0) {
12646
12772
  if (typeof changes.providerEnv !== "object" || changes.providerEnv === null) {
@@ -12700,8 +12826,9 @@ function validateConfigChanges(changes) {
12700
12826
  if (qa.enabled !== void 0 && typeof qa.enabled !== "boolean") {
12701
12827
  return "qa.enabled must be a boolean";
12702
12828
  }
12703
- if (qa.schedule !== void 0 && (typeof qa.schedule !== "string" || qa.schedule.trim().length === 0)) {
12704
- return "qa.schedule must be a non-empty string";
12829
+ const qaScheduleError = validateCronField("qa.schedule", qa.schedule);
12830
+ if (qaScheduleError) {
12831
+ return qaScheduleError;
12705
12832
  }
12706
12833
  if (qa.maxRuntime !== void 0 && (typeof qa.maxRuntime !== "number" || qa.maxRuntime < 60)) {
12707
12834
  return "qa.maxRuntime must be a number >= 60";
@@ -12732,28 +12859,14 @@ function validateConfigChanges(changes) {
12732
12859
  if (audit.enabled !== void 0 && typeof audit.enabled !== "boolean") {
12733
12860
  return "audit.enabled must be a boolean";
12734
12861
  }
12735
- if (audit.schedule !== void 0 && (typeof audit.schedule !== "string" || audit.schedule.trim().length === 0)) {
12736
- return "audit.schedule must be a non-empty string";
12862
+ const auditScheduleError = validateCronField("audit.schedule", audit.schedule);
12863
+ if (auditScheduleError) {
12864
+ return auditScheduleError;
12737
12865
  }
12738
12866
  if (audit.maxRuntime !== void 0 && (typeof audit.maxRuntime !== "number" || audit.maxRuntime < 60)) {
12739
12867
  return "audit.maxRuntime must be a number >= 60";
12740
12868
  }
12741
12869
  }
12742
- if (changes.roadmapScanner !== void 0) {
12743
- const rs = changes.roadmapScanner;
12744
- if (rs.slicerSchedule !== void 0 && (typeof rs.slicerSchedule !== "string" || rs.slicerSchedule.trim().length === 0)) {
12745
- return "roadmapScanner.slicerSchedule must be a non-empty string";
12746
- }
12747
- if (rs.slicerMaxRuntime !== void 0 && (typeof rs.slicerMaxRuntime !== "number" || rs.slicerMaxRuntime < 60)) {
12748
- return "roadmapScanner.slicerMaxRuntime must be a number >= 60";
12749
- }
12750
- if (rs.priorityMode !== void 0 && rs.priorityMode !== "roadmap-first" && rs.priorityMode !== "audit-first") {
12751
- return "roadmapScanner.priorityMode must be one of: roadmap-first, audit-first";
12752
- }
12753
- if (rs.issueColumn !== void 0 && rs.issueColumn !== "Draft" && rs.issueColumn !== "Ready") {
12754
- return "roadmapScanner.issueColumn must be one of: Draft, Ready";
12755
- }
12756
- }
12757
12870
  if (changes.boardProvider !== void 0) {
12758
12871
  if (typeof changes.boardProvider !== "object" || changes.boardProvider === null) {
12759
12872
  return "boardProvider must be an object";
@@ -12838,7 +12951,7 @@ init_dist();
12838
12951
  function runDoctorChecks(projectDir, config) {
12839
12952
  const checks = [];
12840
12953
  try {
12841
- execSync5("git rev-parse --is-inside-work-tree", {
12954
+ execSync6("git rev-parse --is-inside-work-tree", {
12842
12955
  cwd: projectDir,
12843
12956
  stdio: "pipe"
12844
12957
  });
@@ -12847,7 +12960,7 @@ function runDoctorChecks(projectDir, config) {
12847
12960
  checks.push({ name: "git", status: "fail", detail: "Not a git repository" });
12848
12961
  }
12849
12962
  try {
12850
- execSync5(`which ${config.provider}`, { stdio: "pipe" });
12963
+ execSync6(`which ${config.provider}`, { stdio: "pipe" });
12851
12964
  checks.push({
12852
12965
  name: "provider",
12853
12966
  status: "pass",
@@ -13120,9 +13233,20 @@ data: ${JSON.stringify(snapshot)}
13120
13233
  });
13121
13234
  return router;
13122
13235
  }
13236
+ function applyScheduleOffset2(schedule, offset) {
13237
+ if (offset === 0) {
13238
+ return schedule;
13239
+ }
13240
+ const parts = schedule.trim().split(/\s+/);
13241
+ if (parts.length < 5 || !/^\d+$/.test(parts[0])) {
13242
+ return schedule.trim();
13243
+ }
13244
+ parts[0] = String(offset);
13245
+ return parts.join(" ");
13246
+ }
13123
13247
  function computeNextRun(cronExpr) {
13124
13248
  try {
13125
- const interval = CronExpressionParser.parse(cronExpr);
13249
+ const interval = CronExpressionParser2.parse(cronExpr);
13126
13250
  return interval.next().toISOString();
13127
13251
  } catch {
13128
13252
  return null;
@@ -13132,6 +13256,48 @@ function hasScheduledCommand(entries, command) {
13132
13256
  const commandPattern = new RegExp(`\\s${command}\\s+>>`);
13133
13257
  return entries.some((entry) => commandPattern.test(entry));
13134
13258
  }
13259
+ function buildScheduleInfoResponse(config, entries, installed) {
13260
+ const offset = config.cronScheduleOffset ?? 0;
13261
+ const executorSchedule = applyScheduleOffset2(config.cronSchedule, offset);
13262
+ const reviewerSchedule = applyScheduleOffset2(config.reviewerSchedule, offset);
13263
+ const qaSchedule = applyScheduleOffset2(config.qa.schedule, offset);
13264
+ const auditSchedule = applyScheduleOffset2(config.audit.schedule, offset);
13265
+ const plannerSchedule = applyScheduleOffset2(config.roadmapScanner.slicerSchedule, offset);
13266
+ const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
13267
+ const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
13268
+ const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
13269
+ const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
13270
+ const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
13271
+ return {
13272
+ executor: {
13273
+ schedule: executorSchedule,
13274
+ installed: executorInstalled,
13275
+ nextRun: executorInstalled ? computeNextRun(executorSchedule) : null
13276
+ },
13277
+ reviewer: {
13278
+ schedule: reviewerSchedule,
13279
+ installed: reviewerInstalled,
13280
+ nextRun: reviewerInstalled ? computeNextRun(reviewerSchedule) : null
13281
+ },
13282
+ qa: {
13283
+ schedule: qaSchedule,
13284
+ installed: qaInstalled,
13285
+ nextRun: qaInstalled ? computeNextRun(qaSchedule) : null
13286
+ },
13287
+ audit: {
13288
+ schedule: auditSchedule,
13289
+ installed: auditInstalled,
13290
+ nextRun: auditInstalled ? computeNextRun(auditSchedule) : null
13291
+ },
13292
+ planner: {
13293
+ schedule: plannerSchedule,
13294
+ installed: plannerInstalled,
13295
+ nextRun: plannerInstalled ? computeNextRun(plannerSchedule) : null
13296
+ },
13297
+ paused: !installed,
13298
+ entries
13299
+ };
13300
+ }
13135
13301
  function createScheduleInfoRoutes(deps) {
13136
13302
  const { projectDir, getConfig } = deps;
13137
13303
  const router = Router9();
@@ -13139,42 +13305,7 @@ function createScheduleInfoRoutes(deps) {
13139
13305
  try {
13140
13306
  const config = getConfig();
13141
13307
  const snapshot = await fetchStatusSnapshot(projectDir, config);
13142
- const installed = snapshot.crontab.installed;
13143
- const entries = snapshot.crontab.entries;
13144
- const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
13145
- const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
13146
- const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
13147
- const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
13148
- const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
13149
- res.json({
13150
- executor: {
13151
- schedule: config.cronSchedule,
13152
- installed: executorInstalled,
13153
- nextRun: executorInstalled ? computeNextRun(config.cronSchedule) : null
13154
- },
13155
- reviewer: {
13156
- schedule: config.reviewerSchedule,
13157
- installed: reviewerInstalled,
13158
- nextRun: reviewerInstalled ? computeNextRun(config.reviewerSchedule) : null
13159
- },
13160
- qa: {
13161
- schedule: config.qa.schedule,
13162
- installed: qaInstalled,
13163
- nextRun: qaInstalled ? computeNextRun(config.qa.schedule) : null
13164
- },
13165
- audit: {
13166
- schedule: config.audit.schedule,
13167
- installed: auditInstalled,
13168
- nextRun: auditInstalled ? computeNextRun(config.audit.schedule) : null
13169
- },
13170
- planner: {
13171
- schedule: config.roadmapScanner.slicerSchedule,
13172
- installed: plannerInstalled,
13173
- nextRun: plannerInstalled ? computeNextRun(config.roadmapScanner.slicerSchedule) : null
13174
- },
13175
- paused: !installed,
13176
- entries
13177
- });
13308
+ res.json(buildScheduleInfoResponse(config, snapshot.crontab.entries, snapshot.crontab.installed));
13178
13309
  } catch (error2) {
13179
13310
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
13180
13311
  }
@@ -13230,42 +13361,7 @@ data: ${JSON.stringify(snapshot)}
13230
13361
  const config = req.projectConfig;
13231
13362
  const projectDir = req.projectDir;
13232
13363
  const snapshot = await fetchStatusSnapshot(projectDir, config);
13233
- const installed = snapshot.crontab.installed;
13234
- const entries = snapshot.crontab.entries;
13235
- const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
13236
- const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
13237
- const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
13238
- const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
13239
- const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
13240
- res.json({
13241
- executor: {
13242
- schedule: config.cronSchedule,
13243
- installed: executorInstalled,
13244
- nextRun: executorInstalled ? computeNextRun(config.cronSchedule) : null
13245
- },
13246
- reviewer: {
13247
- schedule: config.reviewerSchedule,
13248
- installed: reviewerInstalled,
13249
- nextRun: reviewerInstalled ? computeNextRun(config.reviewerSchedule) : null
13250
- },
13251
- qa: {
13252
- schedule: config.qa.schedule,
13253
- installed: qaInstalled,
13254
- nextRun: qaInstalled ? computeNextRun(config.qa.schedule) : null
13255
- },
13256
- audit: {
13257
- schedule: config.audit.schedule,
13258
- installed: auditInstalled,
13259
- nextRun: auditInstalled ? computeNextRun(config.audit.schedule) : null
13260
- },
13261
- planner: {
13262
- schedule: config.roadmapScanner.slicerSchedule,
13263
- installed: plannerInstalled,
13264
- nextRun: plannerInstalled ? computeNextRun(config.roadmapScanner.slicerSchedule) : null
13265
- },
13266
- paused: !installed,
13267
- entries
13268
- });
13364
+ res.json(buildScheduleInfoResponse(config, snapshot.crontab.entries, snapshot.crontab.installed));
13269
13365
  } catch (error2) {
13270
13366
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
13271
13367
  }
@@ -13620,6 +13716,9 @@ function parseProjectDirs(projects, cwd) {
13620
13716
  const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path33.resolve(cwd, entry));
13621
13717
  return Array.from(new Set(dirs));
13622
13718
  }
13719
+ function shouldInstallGlobal(options) {
13720
+ return options.global !== false;
13721
+ }
13623
13722
  function runCommand2(command, args, cwd) {
13624
13723
  const result = spawnSync(command, args, {
13625
13724
  cwd,
@@ -13649,7 +13748,7 @@ function updateCommand(program2) {
13649
13748
  try {
13650
13749
  const cwd = process.cwd();
13651
13750
  const projectDirs = parseProjectDirs(options.projects, cwd);
13652
- if (!options.noGlobal) {
13751
+ if (shouldInstallGlobal(options)) {
13653
13752
  dim(`Updating global install: npm install -g ${options.globalSpec}`);
13654
13753
  runCommand2("npm", ["install", "-g", options.globalSpec]);
13655
13754
  success("Global CLI update completed.");
@@ -13821,7 +13920,7 @@ function prsCommand(program2) {
13821
13920
  init_dist();
13822
13921
  function getOpenPrBranches(projectDir) {
13823
13922
  try {
13824
- execSync6("git rev-parse --git-dir", {
13923
+ execSync7("git rev-parse --git-dir", {
13825
13924
  cwd: projectDir,
13826
13925
  encoding: "utf-8",
13827
13926
  stdio: ["pipe", "pipe", "pipe"]
@@ -13830,12 +13929,12 @@ function getOpenPrBranches(projectDir) {
13830
13929
  return /* @__PURE__ */ new Set();
13831
13930
  }
13832
13931
  try {
13833
- execSync6("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
13932
+ execSync7("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
13834
13933
  } catch {
13835
13934
  return /* @__PURE__ */ new Set();
13836
13935
  }
13837
13936
  try {
13838
- const output = execSync6("gh pr list --state open --json headRefName", {
13937
+ const output = execSync7("gh pr list --state open --json headRefName", {
13839
13938
  cwd: projectDir,
13840
13939
  encoding: "utf-8",
13841
13940
  stdio: ["pipe", "pipe", "pipe"]
@@ -13,6 +13,7 @@ export interface IInstallOptions {
13
13
  qa?: boolean;
14
14
  noAudit?: boolean;
15
15
  audit?: boolean;
16
+ force?: boolean;
16
17
  }
17
18
  /**
18
19
  * Build PATH export for cron entries using relevant binary directories.