@jonit-dev/night-watch-cli 1.7.66 → 1.7.67
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 +655 -383
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +59 -11
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/queue.d.ts.map +1 -1
- package/dist/commands/queue.js +40 -2
- package/dist/commands/queue.js.map +1 -1
- package/dist/commands/shared/env-builder.d.ts +7 -1
- package/dist/commands/shared/env-builder.d.ts.map +1 -1
- package/dist/commands/shared/env-builder.js +10 -1
- package/dist/commands/shared/env-builder.js.map +1 -1
- package/dist/scripts/night-watch-cron.sh +20 -21
- package/dist/scripts/publish.sh +13 -2
- package/dist/templates/night-watch.config.json +17 -14
- package/dist/web/assets/index-C51Rbsmk.js +381 -0
- package/dist/web/assets/index-DAyP4GOi.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,12 +5,6 @@ import 'reflect-metadata';
|
|
|
5
5
|
import "reflect-metadata";
|
|
6
6
|
import "reflect-metadata";
|
|
7
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
8
|
import * as fs from "fs";
|
|
15
9
|
import * as path from "path";
|
|
16
10
|
import { fileURLToPath } from "url";
|
|
@@ -75,103 +69,105 @@ import * as path13 from "path";
|
|
|
75
69
|
import { spawn } from "child_process";
|
|
76
70
|
import { createHash as createHash3 } from "crypto";
|
|
77
71
|
import { spawn as spawn2 } from "child_process";
|
|
78
|
-
import { execFileSync as execFileSync4 } from "child_process";
|
|
79
72
|
import * as fs15 from "fs";
|
|
80
73
|
import * as path14 from "path";
|
|
81
|
-
import
|
|
74
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
75
|
+
import * as fs16 from "fs";
|
|
82
76
|
import * as path15 from "path";
|
|
77
|
+
import * as os5 from "os";
|
|
78
|
+
import * as path16 from "path";
|
|
83
79
|
import Database7 from "better-sqlite3";
|
|
84
80
|
import "reflect-metadata";
|
|
85
81
|
import { Command as Command3 } from "commander";
|
|
86
|
-
import { existsSync as
|
|
82
|
+
import { existsSync as existsSync29, readFileSync as readFileSync17 } from "fs";
|
|
87
83
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
88
|
-
import { dirname as dirname8, join as
|
|
89
|
-
import
|
|
90
|
-
import
|
|
84
|
+
import { dirname as dirname8, join as join34 } from "path";
|
|
85
|
+
import fs17 from "fs";
|
|
86
|
+
import path17 from "path";
|
|
91
87
|
import { execSync as execSync3 } from "child_process";
|
|
92
88
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
93
|
-
import { dirname as dirname4, join as
|
|
89
|
+
import { dirname as dirname4, join as join16 } from "path";
|
|
94
90
|
import * as readline from "readline";
|
|
95
|
-
import * as
|
|
96
|
-
import * as path17 from "path";
|
|
97
|
-
import { execFileSync as execFileSync5 } from "child_process";
|
|
91
|
+
import * as fs18 from "fs";
|
|
98
92
|
import * as path18 from "path";
|
|
93
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
99
94
|
import * as path19 from "path";
|
|
100
|
-
import * as fs18 from "fs";
|
|
101
95
|
import * as path20 from "path";
|
|
102
|
-
import { execSync as execSync4 } from "child_process";
|
|
103
|
-
import * as path21 from "path";
|
|
104
96
|
import * as fs19 from "fs";
|
|
97
|
+
import * as path21 from "path";
|
|
98
|
+
import { execSync as execSync4 } from "child_process";
|
|
105
99
|
import * as path22 from "path";
|
|
106
100
|
import * as fs20 from "fs";
|
|
107
|
-
import chalk2 from "chalk";
|
|
108
|
-
import { spawn as spawn3 } from "child_process";
|
|
109
101
|
import * as path23 from "path";
|
|
110
102
|
import * as fs21 from "fs";
|
|
111
|
-
import
|
|
103
|
+
import chalk2 from "chalk";
|
|
104
|
+
import { spawn as spawn3 } from "child_process";
|
|
112
105
|
import * as path24 from "path";
|
|
106
|
+
import * as fs22 from "fs";
|
|
107
|
+
import * as fs23 from "fs";
|
|
108
|
+
import * as path25 from "path";
|
|
113
109
|
import * as readline2 from "readline";
|
|
114
110
|
import blessed6 from "blessed";
|
|
115
111
|
import blessed from "blessed";
|
|
116
|
-
import * as
|
|
112
|
+
import * as fs24 from "fs";
|
|
117
113
|
import blessed2 from "blessed";
|
|
118
114
|
import blessed3 from "blessed";
|
|
119
115
|
import cronstrue from "cronstrue";
|
|
120
116
|
import blessed4 from "blessed";
|
|
121
117
|
import { spawn as spawn4 } from "child_process";
|
|
122
118
|
import blessed5 from "blessed";
|
|
123
|
-
import * as
|
|
124
|
-
import * as
|
|
119
|
+
import * as fs25 from "fs";
|
|
120
|
+
import * as path26 from "path";
|
|
121
|
+
import * as fs30 from "fs";
|
|
125
122
|
import * as fs29 from "fs";
|
|
126
|
-
import * as
|
|
127
|
-
import * as path31 from "path";
|
|
123
|
+
import * as path32 from "path";
|
|
128
124
|
import { dirname as dirname7 } from "path";
|
|
129
125
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
130
126
|
import cors from "cors";
|
|
131
127
|
import express from "express";
|
|
132
|
-
import * as fs25 from "fs";
|
|
133
|
-
import * as path26 from "path";
|
|
134
128
|
import * as fs26 from "fs";
|
|
135
129
|
import * as path27 from "path";
|
|
130
|
+
import * as fs27 from "fs";
|
|
131
|
+
import * as path28 from "path";
|
|
136
132
|
import { execSync as execSync5, spawn as spawn5 } from "child_process";
|
|
137
133
|
import { Router } from "express";
|
|
138
134
|
import { Router as Router2 } from "express";
|
|
139
135
|
import { Router as Router3 } from "express";
|
|
140
136
|
import { CronExpressionParser } from "cron-parser";
|
|
141
|
-
import * as
|
|
142
|
-
import * as
|
|
137
|
+
import * as fs28 from "fs";
|
|
138
|
+
import * as path29 from "path";
|
|
143
139
|
import { execSync as execSync6 } from "child_process";
|
|
144
140
|
import { Router as Router4 } from "express";
|
|
145
|
-
import * as
|
|
141
|
+
import * as path30 from "path";
|
|
146
142
|
import { Router as Router5 } from "express";
|
|
147
143
|
import { Router as Router6 } from "express";
|
|
148
|
-
import * as
|
|
144
|
+
import * as path31 from "path";
|
|
149
145
|
import { Router as Router7 } from "express";
|
|
150
146
|
import { Router as Router8 } from "express";
|
|
151
147
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
152
148
|
import { Router as Router9 } from "express";
|
|
153
149
|
import { spawnSync } from "child_process";
|
|
154
|
-
import * as fs30 from "fs";
|
|
155
|
-
import * as path32 from "path";
|
|
156
150
|
import * as fs31 from "fs";
|
|
157
151
|
import * as path33 from "path";
|
|
152
|
+
import * as fs32 from "fs";
|
|
153
|
+
import * as path34 from "path";
|
|
158
154
|
import chalk3 from "chalk";
|
|
159
155
|
import chalk4 from "chalk";
|
|
160
156
|
import { execSync as execSync7 } from "child_process";
|
|
161
|
-
import * as fs32 from "fs";
|
|
162
|
-
import * as readline3 from "readline";
|
|
163
157
|
import * as fs33 from "fs";
|
|
164
|
-
import * as
|
|
165
|
-
import * as
|
|
158
|
+
import * as readline3 from "readline";
|
|
159
|
+
import * as fs34 from "fs";
|
|
166
160
|
import * as path35 from "path";
|
|
161
|
+
import * as os6 from "os";
|
|
162
|
+
import * as path36 from "path";
|
|
167
163
|
import chalk5 from "chalk";
|
|
168
164
|
import { Command } from "commander";
|
|
169
165
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
170
|
-
import * as
|
|
171
|
-
import * as
|
|
166
|
+
import * as fs35 from "fs";
|
|
167
|
+
import * as path37 from "path";
|
|
172
168
|
import * as readline4 from "readline";
|
|
173
169
|
import chalk6 from "chalk";
|
|
174
|
-
import * as
|
|
170
|
+
import * as path38 from "path";
|
|
175
171
|
import { spawn as spawn6 } from "child_process";
|
|
176
172
|
import chalk7 from "chalk";
|
|
177
173
|
import { Command as Command2 } from "commander";
|
|
@@ -260,6 +256,7 @@ var DEFAULT_QUEUE_MAX_CONCURRENCY;
|
|
|
260
256
|
var DEFAULT_QUEUE_MAX_WAIT_TIME;
|
|
261
257
|
var DEFAULT_QUEUE_PRIORITY;
|
|
262
258
|
var DEFAULT_QUEUE;
|
|
259
|
+
var DEFAULT_SCHEDULING_PRIORITY;
|
|
263
260
|
var QUEUE_LOCK_FILE_NAME;
|
|
264
261
|
var init_constants = __esm({
|
|
265
262
|
"../core/dist/constants.js"() {
|
|
@@ -365,7 +362,7 @@ var init_constants = __esm({
|
|
|
365
362
|
PRD_STATES_FILE_NAME = "prd-states.json";
|
|
366
363
|
STATE_DB_FILE_NAME = "state.db";
|
|
367
364
|
MAX_HISTORY_RECORDS_PER_PRD = 10;
|
|
368
|
-
DEFAULT_QUEUE_ENABLED =
|
|
365
|
+
DEFAULT_QUEUE_ENABLED = true;
|
|
369
366
|
DEFAULT_QUEUE_MAX_CONCURRENCY = 1;
|
|
370
367
|
DEFAULT_QUEUE_MAX_WAIT_TIME = 7200;
|
|
371
368
|
DEFAULT_QUEUE_PRIORITY = {
|
|
@@ -381,6 +378,7 @@ var init_constants = __esm({
|
|
|
381
378
|
maxWaitTime: DEFAULT_QUEUE_MAX_WAIT_TIME,
|
|
382
379
|
priority: { ...DEFAULT_QUEUE_PRIORITY }
|
|
383
380
|
};
|
|
381
|
+
DEFAULT_SCHEDULING_PRIORITY = 3;
|
|
384
382
|
QUEUE_LOCK_FILE_NAME = "queue.lock";
|
|
385
383
|
}
|
|
386
384
|
});
|
|
@@ -400,6 +398,7 @@ function getDefaultConfig() {
|
|
|
400
398
|
reviewerSchedule: DEFAULT_REVIEWER_SCHEDULE,
|
|
401
399
|
scheduleBundleId: null,
|
|
402
400
|
cronScheduleOffset: DEFAULT_CRON_SCHEDULE_OFFSET,
|
|
401
|
+
schedulingPriority: DEFAULT_SCHEDULING_PRIORITY,
|
|
403
402
|
maxRetries: DEFAULT_MAX_RETRIES,
|
|
404
403
|
// Reviewer retry configuration
|
|
405
404
|
reviewerMaxRetries: DEFAULT_REVIEWER_MAX_RETRIES,
|
|
@@ -476,6 +475,7 @@ function normalizeConfig(rawConfig) {
|
|
|
476
475
|
normalized.scheduleBundleId = null;
|
|
477
476
|
}
|
|
478
477
|
normalized.cronScheduleOffset = readNumber(rawConfig.cronScheduleOffset);
|
|
478
|
+
normalized.schedulingPriority = readNumber(rawConfig.schedulingPriority);
|
|
479
479
|
normalized.maxRetries = readNumber(rawConfig.maxRetries);
|
|
480
480
|
normalized.reviewerMaxRetries = readNumber(rawConfig.reviewerMaxRetries);
|
|
481
481
|
normalized.reviewerRetryDelay = readNumber(rawConfig.reviewerRetryDelay);
|
|
@@ -603,7 +603,7 @@ function normalizeConfig(rawConfig) {
|
|
|
603
603
|
if (rawQueue) {
|
|
604
604
|
const queue = {
|
|
605
605
|
enabled: readBoolean(rawQueue.enabled) ?? DEFAULT_QUEUE.enabled,
|
|
606
|
-
maxConcurrency:
|
|
606
|
+
maxConcurrency: DEFAULT_QUEUE.maxConcurrency,
|
|
607
607
|
maxWaitTime: readNumber(rawQueue.maxWaitTime) ?? DEFAULT_QUEUE.maxWaitTime,
|
|
608
608
|
priority: { ...DEFAULT_QUEUE.priority }
|
|
609
609
|
};
|
|
@@ -616,10 +616,13 @@ function normalizeConfig(rawConfig) {
|
|
|
616
616
|
}
|
|
617
617
|
}
|
|
618
618
|
}
|
|
619
|
-
queue.maxConcurrency =
|
|
619
|
+
queue.maxConcurrency = DEFAULT_QUEUE.maxConcurrency;
|
|
620
620
|
queue.maxWaitTime = Math.max(300, Math.min(14400, queue.maxWaitTime));
|
|
621
621
|
normalized.queue = queue;
|
|
622
622
|
}
|
|
623
|
+
if (normalized.schedulingPriority !== void 0) {
|
|
624
|
+
normalized.schedulingPriority = Math.max(1, Math.min(5, normalized.schedulingPriority));
|
|
625
|
+
}
|
|
623
626
|
return normalized;
|
|
624
627
|
}
|
|
625
628
|
function parseBoolean(value) {
|
|
@@ -759,6 +762,12 @@ function loadConfig(projectDir) {
|
|
|
759
762
|
envConfig.cronScheduleOffset = offset;
|
|
760
763
|
}
|
|
761
764
|
}
|
|
765
|
+
if (process.env.NW_SCHEDULING_PRIORITY) {
|
|
766
|
+
const priority = parseInt(process.env.NW_SCHEDULING_PRIORITY, 10);
|
|
767
|
+
if (!isNaN(priority)) {
|
|
768
|
+
envConfig.schedulingPriority = Math.max(1, Math.min(5, priority));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
762
771
|
if (process.env.NW_MAX_RETRIES) {
|
|
763
772
|
const retries = parseInt(process.env.NW_MAX_RETRIES, 10);
|
|
764
773
|
if (!isNaN(retries) && retries >= 1) {
|
|
@@ -969,10 +978,7 @@ function loadConfig(projectDir) {
|
|
|
969
978
|
}
|
|
970
979
|
}
|
|
971
980
|
if (process.env.NW_QUEUE_MAX_CONCURRENCY) {
|
|
972
|
-
|
|
973
|
-
if (!isNaN(maxConcurrency) && maxConcurrency >= 1) {
|
|
974
|
-
envConfig.queue = { ...queueBaseConfig(), maxConcurrency: Math.min(10, maxConcurrency) };
|
|
975
|
-
}
|
|
981
|
+
envConfig.queue = { ...queueBaseConfig(), maxConcurrency: DEFAULT_QUEUE.maxConcurrency };
|
|
976
982
|
}
|
|
977
983
|
if (process.env.NW_QUEUE_MAX_WAIT_TIME) {
|
|
978
984
|
const maxWaitTime = parseInt(process.env.NW_QUEUE_MAX_WAIT_TIME, 10);
|
|
@@ -2978,7 +2984,7 @@ function buildAvatarPrompt(personaName, role) {
|
|
|
2978
2984
|
return `Professional headshot portrait photo of a ${descriptor}, photorealistic, clean soft neutral background, natural diffused window lighting, shot at f/2.8, shallow depth of field, looking directly at camera, candid professional headshot style, no retouching artifacts, natural skin texture`;
|
|
2979
2985
|
}
|
|
2980
2986
|
function sleep(ms) {
|
|
2981
|
-
return new Promise((
|
|
2987
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
2982
2988
|
}
|
|
2983
2989
|
async function generatePersonaAvatar(personaName, personaRole, apiToken) {
|
|
2984
2990
|
const prompt2 = buildAvatarPrompt(personaName, personaRole);
|
|
@@ -3776,7 +3782,7 @@ function getLockFilePaths(projectDir) {
|
|
|
3776
3782
|
};
|
|
3777
3783
|
}
|
|
3778
3784
|
function sleep2(ms) {
|
|
3779
|
-
return new Promise((
|
|
3785
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
3780
3786
|
}
|
|
3781
3787
|
async function cancelProcess(processType, lockPath, force = false) {
|
|
3782
3788
|
const lockStatus = checkLockFile(lockPath);
|
|
@@ -4099,7 +4105,7 @@ var init_config_writer = __esm({
|
|
|
4099
4105
|
"../core/dist/utils/config-writer.js"() {
|
|
4100
4106
|
"use strict";
|
|
4101
4107
|
init_constants();
|
|
4102
|
-
PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set(["notifications", "qa", "audit", "roadmapScanner"]);
|
|
4108
|
+
PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set(["notifications", "qa", "audit", "roadmapScanner", "queue"]);
|
|
4103
4109
|
}
|
|
4104
4110
|
});
|
|
4105
4111
|
function getHistoryPath() {
|
|
@@ -5529,7 +5535,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
|
|
|
5529
5535
|
const logStream = fs14.createWriteStream(logFile, { flags: "w" });
|
|
5530
5536
|
logStream.on("error", () => {
|
|
5531
5537
|
});
|
|
5532
|
-
return new Promise((
|
|
5538
|
+
return new Promise((resolve10) => {
|
|
5533
5539
|
const childEnv = {
|
|
5534
5540
|
...process.env,
|
|
5535
5541
|
...config.providerEnv
|
|
@@ -5547,7 +5553,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
|
|
|
5547
5553
|
});
|
|
5548
5554
|
child.on("error", (error2) => {
|
|
5549
5555
|
logStream.end();
|
|
5550
|
-
|
|
5556
|
+
resolve10({
|
|
5551
5557
|
sliced: false,
|
|
5552
5558
|
error: `Failed to spawn provider: ${error2.message}`,
|
|
5553
5559
|
item
|
|
@@ -5556,7 +5562,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
|
|
|
5556
5562
|
child.on("close", (code) => {
|
|
5557
5563
|
logStream.end();
|
|
5558
5564
|
if (code !== 0) {
|
|
5559
|
-
|
|
5565
|
+
resolve10({
|
|
5560
5566
|
sliced: false,
|
|
5561
5567
|
error: `Provider exited with code ${code ?? 1}`,
|
|
5562
5568
|
item
|
|
@@ -5564,14 +5570,14 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
|
|
|
5564
5570
|
return;
|
|
5565
5571
|
}
|
|
5566
5572
|
if (!fs14.existsSync(filePath)) {
|
|
5567
|
-
|
|
5573
|
+
resolve10({
|
|
5568
5574
|
sliced: false,
|
|
5569
5575
|
error: `Provider did not create expected file: ${filePath}`,
|
|
5570
5576
|
item
|
|
5571
5577
|
});
|
|
5572
5578
|
return;
|
|
5573
5579
|
}
|
|
5574
|
-
|
|
5580
|
+
resolve10({
|
|
5575
5581
|
sliced: true,
|
|
5576
5582
|
file: filename,
|
|
5577
5583
|
item
|
|
@@ -5592,7 +5598,7 @@ async function sliceNextItem(projectDir, config) {
|
|
|
5592
5598
|
if (!roadmapExists && auditItems.length === 0) {
|
|
5593
5599
|
return {
|
|
5594
5600
|
sliced: false,
|
|
5595
|
-
error: "
|
|
5601
|
+
error: "No pending items to process"
|
|
5596
5602
|
};
|
|
5597
5603
|
}
|
|
5598
5604
|
const roadmapItems = roadmapExists ? parseRoadmap(fs14.readFileSync(roadmapPath, "utf-8")) : [];
|
|
@@ -5727,7 +5733,7 @@ async function executeScript(scriptPath, args = [], env = {}, options = {}) {
|
|
|
5727
5733
|
return result.exitCode;
|
|
5728
5734
|
}
|
|
5729
5735
|
async function executeScriptWithOutput(scriptPath, args = [], env = {}, options = {}) {
|
|
5730
|
-
return new Promise((
|
|
5736
|
+
return new Promise((resolve10, reject) => {
|
|
5731
5737
|
const childEnv = {
|
|
5732
5738
|
...process.env,
|
|
5733
5739
|
...env
|
|
@@ -5753,7 +5759,7 @@ async function executeScriptWithOutput(scriptPath, args = [], env = {}, options
|
|
|
5753
5759
|
reject(error2);
|
|
5754
5760
|
});
|
|
5755
5761
|
child.on("close", (code) => {
|
|
5756
|
-
|
|
5762
|
+
resolve10({
|
|
5757
5763
|
exitCode: code ?? 1,
|
|
5758
5764
|
stdout: stdoutChunks.join(""),
|
|
5759
5765
|
stderr: stderrChunks.join("")
|
|
@@ -5766,6 +5772,106 @@ var init_shell = __esm({
|
|
|
5766
5772
|
"use strict";
|
|
5767
5773
|
}
|
|
5768
5774
|
});
|
|
5775
|
+
function normalizeSchedulingPriority(priority) {
|
|
5776
|
+
if (!Number.isFinite(priority)) {
|
|
5777
|
+
return DEFAULT_SCHEDULING_PRIORITY;
|
|
5778
|
+
}
|
|
5779
|
+
return Math.max(1, Math.min(5, Math.floor(priority)));
|
|
5780
|
+
}
|
|
5781
|
+
function isJobTypeEnabled(config, jobType) {
|
|
5782
|
+
switch (jobType) {
|
|
5783
|
+
case "executor":
|
|
5784
|
+
return config.executorEnabled !== false;
|
|
5785
|
+
case "reviewer":
|
|
5786
|
+
return config.reviewerEnabled;
|
|
5787
|
+
case "qa":
|
|
5788
|
+
return config.qa.enabled;
|
|
5789
|
+
case "audit":
|
|
5790
|
+
return config.audit.enabled;
|
|
5791
|
+
case "slicer":
|
|
5792
|
+
return config.roadmapScanner.enabled;
|
|
5793
|
+
default:
|
|
5794
|
+
return true;
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
function loadPeerConfig(projectPath) {
|
|
5798
|
+
if (!fs15.existsSync(projectPath) || !fs15.existsSync(path14.join(projectPath, CONFIG_FILE_NAME))) {
|
|
5799
|
+
return null;
|
|
5800
|
+
}
|
|
5801
|
+
try {
|
|
5802
|
+
return loadConfig(projectPath);
|
|
5803
|
+
} catch {
|
|
5804
|
+
return null;
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
|
|
5808
|
+
const peers = /* @__PURE__ */ new Map();
|
|
5809
|
+
const currentPath = path14.resolve(currentProjectDir);
|
|
5810
|
+
const addPeer = (projectPath, config) => {
|
|
5811
|
+
const resolvedPath = path14.resolve(projectPath);
|
|
5812
|
+
if (!isJobTypeEnabled(config, jobType)) {
|
|
5813
|
+
return;
|
|
5814
|
+
}
|
|
5815
|
+
peers.set(resolvedPath, {
|
|
5816
|
+
path: resolvedPath,
|
|
5817
|
+
config,
|
|
5818
|
+
schedulingPriority: normalizeSchedulingPriority(config.schedulingPriority),
|
|
5819
|
+
sortKey: `${path14.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
|
|
5820
|
+
});
|
|
5821
|
+
};
|
|
5822
|
+
addPeer(currentPath, currentConfig);
|
|
5823
|
+
for (const entry of loadRegistry()) {
|
|
5824
|
+
const resolvedPath = path14.resolve(entry.path);
|
|
5825
|
+
if (resolvedPath === currentPath || peers.has(resolvedPath)) {
|
|
5826
|
+
continue;
|
|
5827
|
+
}
|
|
5828
|
+
const peerConfig = loadPeerConfig(resolvedPath);
|
|
5829
|
+
if (peerConfig) {
|
|
5830
|
+
addPeer(resolvedPath, peerConfig);
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5833
|
+
return Array.from(peers.values()).sort((left, right) => {
|
|
5834
|
+
if (left.schedulingPriority !== right.schedulingPriority) {
|
|
5835
|
+
return right.schedulingPriority - left.schedulingPriority;
|
|
5836
|
+
}
|
|
5837
|
+
return left.sortKey.localeCompare(right.sortKey);
|
|
5838
|
+
});
|
|
5839
|
+
}
|
|
5840
|
+
function getSchedulingPlan(projectDir, config, jobType) {
|
|
5841
|
+
const peers = collectSchedulingPeers(projectDir, config, jobType);
|
|
5842
|
+
const currentPath = path14.resolve(projectDir);
|
|
5843
|
+
const slotIndex = Math.max(0, peers.findIndex((peer) => peer.path === currentPath));
|
|
5844
|
+
const peerCount = Math.max(1, peers.length);
|
|
5845
|
+
const balancedDelayMinutes = peerCount <= 1 ? 0 : Math.floor(slotIndex * 60 / peerCount);
|
|
5846
|
+
const manualDelayMinutes = Math.max(0, Math.floor(config.cronScheduleOffset ?? DEFAULT_CRON_SCHEDULE_OFFSET));
|
|
5847
|
+
return {
|
|
5848
|
+
manualDelayMinutes,
|
|
5849
|
+
balancedDelayMinutes,
|
|
5850
|
+
totalDelayMinutes: manualDelayMinutes + balancedDelayMinutes,
|
|
5851
|
+
peerCount,
|
|
5852
|
+
slotIndex,
|
|
5853
|
+
schedulingPriority: normalizeSchedulingPriority(config.schedulingPriority)
|
|
5854
|
+
};
|
|
5855
|
+
}
|
|
5856
|
+
function addDelayToIsoString(isoString, delayMinutes) {
|
|
5857
|
+
if (!isoString) {
|
|
5858
|
+
return null;
|
|
5859
|
+
}
|
|
5860
|
+
const date = new Date(isoString);
|
|
5861
|
+
if (Number.isNaN(date.getTime())) {
|
|
5862
|
+
return null;
|
|
5863
|
+
}
|
|
5864
|
+
date.setTime(date.getTime() + delayMinutes * 6e4);
|
|
5865
|
+
return date.toISOString();
|
|
5866
|
+
}
|
|
5867
|
+
var init_scheduling = __esm({
|
|
5868
|
+
"../core/dist/utils/scheduling.js"() {
|
|
5869
|
+
"use strict";
|
|
5870
|
+
init_config();
|
|
5871
|
+
init_constants();
|
|
5872
|
+
init_registry();
|
|
5873
|
+
}
|
|
5874
|
+
});
|
|
5769
5875
|
function validateWebhook(webhook) {
|
|
5770
5876
|
const issues = [];
|
|
5771
5877
|
if (!webhook.events || webhook.events.length === 0) {
|
|
@@ -5829,7 +5935,7 @@ function gitExec(args, cwd, logFile) {
|
|
|
5829
5935
|
});
|
|
5830
5936
|
if (logFile && result) {
|
|
5831
5937
|
try {
|
|
5832
|
-
|
|
5938
|
+
fs16.appendFileSync(logFile, result);
|
|
5833
5939
|
} catch {
|
|
5834
5940
|
}
|
|
5835
5941
|
}
|
|
@@ -5838,7 +5944,7 @@ function gitExec(args, cwd, logFile) {
|
|
|
5838
5944
|
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
5839
5945
|
if (logFile) {
|
|
5840
5946
|
try {
|
|
5841
|
-
|
|
5947
|
+
fs16.appendFileSync(logFile, errorMessage + "\n");
|
|
5842
5948
|
} catch {
|
|
5843
5949
|
}
|
|
5844
5950
|
}
|
|
@@ -5897,11 +6003,11 @@ function prepareBranchWorktree(options) {
|
|
|
5897
6003
|
}
|
|
5898
6004
|
function prepareDetachedWorktree(options) {
|
|
5899
6005
|
const { projectDir, worktreeDir, defaultBranch, logFile } = options;
|
|
5900
|
-
if (
|
|
6006
|
+
if (fs16.existsSync(worktreeDir)) {
|
|
5901
6007
|
const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
|
|
5902
6008
|
if (!isRegistered) {
|
|
5903
6009
|
try {
|
|
5904
|
-
|
|
6010
|
+
fs16.rmSync(worktreeDir, { recursive: true, force: true });
|
|
5905
6011
|
} catch {
|
|
5906
6012
|
}
|
|
5907
6013
|
}
|
|
@@ -5943,7 +6049,7 @@ function isWorktreeRegistered(projectDir, worktreePath) {
|
|
|
5943
6049
|
}
|
|
5944
6050
|
}
|
|
5945
6051
|
function cleanupWorktrees(projectDir, scope) {
|
|
5946
|
-
const projectName =
|
|
6052
|
+
const projectName = path15.basename(projectDir);
|
|
5947
6053
|
const matchToken = scope ? scope : `${projectName}-nw`;
|
|
5948
6054
|
const removed = [];
|
|
5949
6055
|
try {
|
|
@@ -5977,12 +6083,12 @@ var init_worktree_manager = __esm({
|
|
|
5977
6083
|
}
|
|
5978
6084
|
});
|
|
5979
6085
|
function getStateDbPath() {
|
|
5980
|
-
const base = process.env.NIGHT_WATCH_HOME ||
|
|
5981
|
-
return
|
|
6086
|
+
const base = process.env.NIGHT_WATCH_HOME || path16.join(os5.homedir(), GLOBAL_CONFIG_DIR);
|
|
6087
|
+
return path16.join(base, STATE_DB_FILE_NAME);
|
|
5982
6088
|
}
|
|
5983
6089
|
function getQueueLockPath() {
|
|
5984
|
-
const base = process.env.NIGHT_WATCH_HOME ||
|
|
5985
|
-
return
|
|
6090
|
+
const base = process.env.NIGHT_WATCH_HOME || path16.join(os5.homedir(), GLOBAL_CONFIG_DIR);
|
|
6091
|
+
return path16.join(base, QUEUE_LOCK_FILE_NAME);
|
|
5986
6092
|
}
|
|
5987
6093
|
function openDb() {
|
|
5988
6094
|
const dbPath = getStateDbPath();
|
|
@@ -6004,6 +6110,54 @@ function rowToEntry(row) {
|
|
|
6004
6110
|
expiredAt: row.expired_at
|
|
6005
6111
|
};
|
|
6006
6112
|
}
|
|
6113
|
+
function getProjectSchedulingPriority(projectPath, cache) {
|
|
6114
|
+
if (cache.has(projectPath)) {
|
|
6115
|
+
return cache.get(projectPath);
|
|
6116
|
+
}
|
|
6117
|
+
let priority = 3;
|
|
6118
|
+
try {
|
|
6119
|
+
priority = normalizeSchedulingPriority(loadConfig(projectPath).schedulingPriority);
|
|
6120
|
+
} catch {
|
|
6121
|
+
priority = 3;
|
|
6122
|
+
}
|
|
6123
|
+
cache.set(projectPath, priority);
|
|
6124
|
+
return priority;
|
|
6125
|
+
}
|
|
6126
|
+
function selectNextPendingEntry(db) {
|
|
6127
|
+
const rows = db.prepare(`SELECT * FROM job_queue
|
|
6128
|
+
WHERE status = 'pending'
|
|
6129
|
+
ORDER BY priority DESC, enqueued_at ASC, id ASC`).all();
|
|
6130
|
+
if (rows.length === 0) {
|
|
6131
|
+
return null;
|
|
6132
|
+
}
|
|
6133
|
+
const headByProject = /* @__PURE__ */ new Map();
|
|
6134
|
+
for (const row of rows) {
|
|
6135
|
+
const entry = rowToEntry(row);
|
|
6136
|
+
if (!headByProject.has(entry.projectPath)) {
|
|
6137
|
+
headByProject.set(entry.projectPath, entry);
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
const priorityCache = /* @__PURE__ */ new Map();
|
|
6141
|
+
const candidates = Array.from(headByProject.values());
|
|
6142
|
+
candidates.sort((left, right) => {
|
|
6143
|
+
if (left.priority !== right.priority) {
|
|
6144
|
+
return right.priority - left.priority;
|
|
6145
|
+
}
|
|
6146
|
+
const leftProjectPriority = getProjectSchedulingPriority(left.projectPath, priorityCache);
|
|
6147
|
+
const rightProjectPriority = getProjectSchedulingPriority(right.projectPath, priorityCache);
|
|
6148
|
+
if (leftProjectPriority !== rightProjectPriority) {
|
|
6149
|
+
return rightProjectPriority - leftProjectPriority;
|
|
6150
|
+
}
|
|
6151
|
+
if (left.enqueuedAt !== right.enqueuedAt) {
|
|
6152
|
+
return left.enqueuedAt - right.enqueuedAt;
|
|
6153
|
+
}
|
|
6154
|
+
if (left.projectName !== right.projectName) {
|
|
6155
|
+
return left.projectName.localeCompare(right.projectName);
|
|
6156
|
+
}
|
|
6157
|
+
return left.id - right.id;
|
|
6158
|
+
});
|
|
6159
|
+
return candidates[0] ?? null;
|
|
6160
|
+
}
|
|
6007
6161
|
function getJobPriority(jobType, config) {
|
|
6008
6162
|
const priorityMap = config?.priority ?? DEFAULT_QUEUE_PRIORITY;
|
|
6009
6163
|
return priorityMap[jobType] ?? 0;
|
|
@@ -6049,33 +6203,38 @@ function removeJob(queueId) {
|
|
|
6049
6203
|
function getNextPendingJob() {
|
|
6050
6204
|
const db = openDb();
|
|
6051
6205
|
try {
|
|
6052
|
-
|
|
6053
|
-
WHERE status = 'pending'
|
|
6054
|
-
ORDER BY priority DESC, enqueued_at ASC
|
|
6055
|
-
LIMIT 1`).get();
|
|
6056
|
-
return row ? rowToEntry(row) : null;
|
|
6206
|
+
return selectNextPendingEntry(db);
|
|
6057
6207
|
} finally {
|
|
6058
6208
|
db.close();
|
|
6059
6209
|
}
|
|
6060
6210
|
}
|
|
6211
|
+
function getInFlightCount() {
|
|
6212
|
+
const db = openDb();
|
|
6213
|
+
try {
|
|
6214
|
+
const running = db.prepare(`SELECT COUNT(*) as count FROM job_queue WHERE status IN ('running', 'dispatched')`).get();
|
|
6215
|
+
return running?.count ?? 0;
|
|
6216
|
+
} finally {
|
|
6217
|
+
db.close();
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
function canStartJob(config) {
|
|
6221
|
+
const maxConcurrency = config?.maxConcurrency ?? 1;
|
|
6222
|
+
return getInFlightCount() < maxConcurrency;
|
|
6223
|
+
}
|
|
6061
6224
|
function dispatchNextJob(config) {
|
|
6062
6225
|
expireStaleJobs(config?.maxWaitTime ?? DEFAULT_QUEUE_MAX_WAIT_TIME);
|
|
6063
6226
|
const db = openDb();
|
|
6064
6227
|
try {
|
|
6228
|
+
const maxConcurrency = config?.maxConcurrency ?? 1;
|
|
6065
6229
|
const running = db.prepare(`SELECT COUNT(*) as count FROM job_queue WHERE status IN ('running', 'dispatched')`).get();
|
|
6066
6230
|
const runningCount = running?.count ?? 0;
|
|
6067
|
-
const maxConcurrency = config?.maxConcurrency ?? 1;
|
|
6068
6231
|
if (runningCount >= maxConcurrency) {
|
|
6069
6232
|
return null;
|
|
6070
6233
|
}
|
|
6071
|
-
const
|
|
6072
|
-
|
|
6073
|
-
ORDER BY priority DESC, enqueued_at ASC
|
|
6074
|
-
LIMIT 1`).get();
|
|
6075
|
-
if (!row) {
|
|
6234
|
+
const entry = selectNextPendingEntry(db);
|
|
6235
|
+
if (!entry) {
|
|
6076
6236
|
return null;
|
|
6077
6237
|
}
|
|
6078
|
-
const entry = rowToEntry(row);
|
|
6079
6238
|
const now = Math.floor(Date.now() / 1e3);
|
|
6080
6239
|
db.prepare(`UPDATE job_queue SET status = 'dispatched', dispatched_at = ? WHERE id = ?`).run(now, entry.id);
|
|
6081
6240
|
return { ...entry, status: "dispatched", dispatchedAt: now };
|
|
@@ -6167,7 +6326,9 @@ function updateJobStatus(id, status) {
|
|
|
6167
6326
|
var init_job_queue = __esm({
|
|
6168
6327
|
"../core/dist/utils/job-queue.js"() {
|
|
6169
6328
|
"use strict";
|
|
6329
|
+
init_config();
|
|
6170
6330
|
init_constants();
|
|
6331
|
+
init_scheduling();
|
|
6171
6332
|
}
|
|
6172
6333
|
});
|
|
6173
6334
|
function renderDependsOn(deps) {
|
|
@@ -6396,6 +6557,7 @@ __export(dist_exports, {
|
|
|
6396
6557
|
DEFAULT_REVIEWER_RETRY_DELAY: () => DEFAULT_REVIEWER_RETRY_DELAY,
|
|
6397
6558
|
DEFAULT_REVIEWER_SCHEDULE: () => DEFAULT_REVIEWER_SCHEDULE,
|
|
6398
6559
|
DEFAULT_ROADMAP_SCANNER: () => DEFAULT_ROADMAP_SCANNER,
|
|
6560
|
+
DEFAULT_SCHEDULING_PRIORITY: () => DEFAULT_SCHEDULING_PRIORITY,
|
|
6399
6561
|
DEFAULT_SLICER_MAX_RUNTIME: () => DEFAULT_SLICER_MAX_RUNTIME,
|
|
6400
6562
|
DEFAULT_SLICER_SCHEDULE: () => DEFAULT_SLICER_SCHEDULE,
|
|
6401
6563
|
DEFAULT_TEMPLATES_DIR: () => DEFAULT_TEMPLATES_DIR,
|
|
@@ -6431,10 +6593,12 @@ __export(dist_exports, {
|
|
|
6431
6593
|
VALID_MERGE_METHODS: () => VALID_MERGE_METHODS,
|
|
6432
6594
|
VALID_PROVIDERS: () => VALID_PROVIDERS,
|
|
6433
6595
|
acquireLock: () => acquireLock,
|
|
6596
|
+
addDelayToIsoString: () => addDelayToIsoString,
|
|
6434
6597
|
addEntry: () => addEntry,
|
|
6435
6598
|
auditLockPath: () => auditLockPath,
|
|
6436
6599
|
buildDescription: () => buildDescription,
|
|
6437
6600
|
calculateStringSimilarity: () => calculateStringSimilarity,
|
|
6601
|
+
canStartJob: () => canStartJob,
|
|
6438
6602
|
cancelProcess: () => cancelProcess,
|
|
6439
6603
|
checkConfigFile: () => checkConfigFile,
|
|
6440
6604
|
checkCrontabAccess: () => checkCrontabAccess,
|
|
@@ -6508,6 +6672,7 @@ __export(dist_exports, {
|
|
|
6508
6672
|
getEventEmoji: () => getEventEmoji,
|
|
6509
6673
|
getEventTitle: () => getEventTitle,
|
|
6510
6674
|
getHistoryPath: () => getHistoryPath,
|
|
6675
|
+
getInFlightCount: () => getInFlightCount,
|
|
6511
6676
|
getJobPriority: () => getJobPriority,
|
|
6512
6677
|
getLabelsForSection: () => getLabelsForSection,
|
|
6513
6678
|
getLastExecution: () => getLastExecution,
|
|
@@ -6528,6 +6693,7 @@ __export(dist_exports, {
|
|
|
6528
6693
|
getRepositories: () => getRepositories,
|
|
6529
6694
|
getRoadmapStatus: () => getRoadmapStatus,
|
|
6530
6695
|
getRunningJob: () => getRunningJob,
|
|
6696
|
+
getSchedulingPlan: () => getSchedulingPlan,
|
|
6531
6697
|
getScriptPath: () => getScriptPath,
|
|
6532
6698
|
getStateFilePath: () => getStateFilePath,
|
|
6533
6699
|
getStateItem: () => getStateItem,
|
|
@@ -6541,6 +6707,7 @@ __export(dist_exports, {
|
|
|
6541
6707
|
isContainerInitialized: () => isContainerInitialized,
|
|
6542
6708
|
isInCooldown: () => isInCooldown,
|
|
6543
6709
|
isItemProcessed: () => isItemProcessed,
|
|
6710
|
+
isJobTypeEnabled: () => isJobTypeEnabled,
|
|
6544
6711
|
isProcessRunning: () => isProcessRunning,
|
|
6545
6712
|
isValidCategory: () => isValidCategory,
|
|
6546
6713
|
isValidHorizon: () => isValidHorizon,
|
|
@@ -6556,6 +6723,7 @@ __export(dist_exports, {
|
|
|
6556
6723
|
markJobRunning: () => markJobRunning,
|
|
6557
6724
|
markPrdDone: () => markPrdDone,
|
|
6558
6725
|
migrateJsonToSqlite: () => migrateJsonToSqlite,
|
|
6726
|
+
normalizeSchedulingPriority: () => normalizeSchedulingPriority,
|
|
6559
6727
|
parsePrdDependencies: () => parsePrdDependencies,
|
|
6560
6728
|
parseRoadmap: () => parseRoadmap,
|
|
6561
6729
|
parseScriptResult: () => parseScriptResult,
|
|
@@ -6643,6 +6811,7 @@ var init_dist = __esm({
|
|
|
6643
6811
|
init_roadmap_state();
|
|
6644
6812
|
init_script_result();
|
|
6645
6813
|
init_shell();
|
|
6814
|
+
init_scheduling();
|
|
6646
6815
|
init_status_data();
|
|
6647
6816
|
init_ui();
|
|
6648
6817
|
init_webhook_validator();
|
|
@@ -6658,22 +6827,22 @@ var __dirname2 = dirname4(__filename);
|
|
|
6658
6827
|
function findTemplatesDir(startDir) {
|
|
6659
6828
|
let d = startDir;
|
|
6660
6829
|
for (let i = 0; i < 8; i++) {
|
|
6661
|
-
const candidate =
|
|
6662
|
-
if (
|
|
6830
|
+
const candidate = join16(d, "templates");
|
|
6831
|
+
if (fs17.existsSync(candidate) && fs17.statSync(candidate).isDirectory()) {
|
|
6663
6832
|
return candidate;
|
|
6664
6833
|
}
|
|
6665
6834
|
d = dirname4(d);
|
|
6666
6835
|
}
|
|
6667
|
-
return
|
|
6836
|
+
return join16(startDir, "templates");
|
|
6668
6837
|
}
|
|
6669
6838
|
var TEMPLATES_DIR = findTemplatesDir(__dirname2);
|
|
6670
6839
|
function hasPlaywrightDependency(cwd) {
|
|
6671
|
-
const packageJsonPath =
|
|
6672
|
-
if (!
|
|
6840
|
+
const packageJsonPath = path17.join(cwd, "package.json");
|
|
6841
|
+
if (!fs17.existsSync(packageJsonPath)) {
|
|
6673
6842
|
return false;
|
|
6674
6843
|
}
|
|
6675
6844
|
try {
|
|
6676
|
-
const packageJson2 = JSON.parse(
|
|
6845
|
+
const packageJson2 = JSON.parse(fs17.readFileSync(packageJsonPath, "utf-8"));
|
|
6677
6846
|
return Boolean(packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright);
|
|
6678
6847
|
} catch {
|
|
6679
6848
|
return false;
|
|
@@ -6683,7 +6852,7 @@ function detectPlaywright(cwd) {
|
|
|
6683
6852
|
if (hasPlaywrightDependency(cwd)) {
|
|
6684
6853
|
return true;
|
|
6685
6854
|
}
|
|
6686
|
-
if (
|
|
6855
|
+
if (fs17.existsSync(path17.join(cwd, "node_modules", ".bin", "playwright"))) {
|
|
6687
6856
|
return true;
|
|
6688
6857
|
}
|
|
6689
6858
|
try {
|
|
@@ -6699,10 +6868,10 @@ function detectPlaywright(cwd) {
|
|
|
6699
6868
|
}
|
|
6700
6869
|
}
|
|
6701
6870
|
function resolvePlaywrightInstallCommand(cwd) {
|
|
6702
|
-
if (
|
|
6871
|
+
if (fs17.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) {
|
|
6703
6872
|
return "pnpm add -D @playwright/test";
|
|
6704
6873
|
}
|
|
6705
|
-
if (
|
|
6874
|
+
if (fs17.existsSync(path17.join(cwd, "yarn.lock"))) {
|
|
6706
6875
|
return "yarn add -D @playwright/test";
|
|
6707
6876
|
}
|
|
6708
6877
|
return "npm install -D @playwright/test";
|
|
@@ -6711,7 +6880,7 @@ function promptYesNo(question, defaultNo = true) {
|
|
|
6711
6880
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
6712
6881
|
return Promise.resolve(false);
|
|
6713
6882
|
}
|
|
6714
|
-
return new Promise((
|
|
6883
|
+
return new Promise((resolve10) => {
|
|
6715
6884
|
const rl = readline.createInterface({
|
|
6716
6885
|
input: process.stdin,
|
|
6717
6886
|
output: process.stdout
|
|
@@ -6721,10 +6890,10 @@ function promptYesNo(question, defaultNo = true) {
|
|
|
6721
6890
|
rl.close();
|
|
6722
6891
|
const normalized = answer.trim().toLowerCase();
|
|
6723
6892
|
if (normalized === "") {
|
|
6724
|
-
|
|
6893
|
+
resolve10(!defaultNo);
|
|
6725
6894
|
return;
|
|
6726
6895
|
}
|
|
6727
|
-
|
|
6896
|
+
resolve10(normalized === "y" || normalized === "yes");
|
|
6728
6897
|
});
|
|
6729
6898
|
});
|
|
6730
6899
|
}
|
|
@@ -6800,7 +6969,7 @@ function getDefaultBranch(cwd) {
|
|
|
6800
6969
|
}
|
|
6801
6970
|
}
|
|
6802
6971
|
function promptProviderSelection(providers) {
|
|
6803
|
-
return new Promise((
|
|
6972
|
+
return new Promise((resolve10, reject) => {
|
|
6804
6973
|
const rl = readline.createInterface({
|
|
6805
6974
|
input: process.stdin,
|
|
6806
6975
|
output: process.stdout
|
|
@@ -6816,41 +6985,91 @@ function promptProviderSelection(providers) {
|
|
|
6816
6985
|
reject(new Error("Invalid selection. Please run init again and select a valid number."));
|
|
6817
6986
|
return;
|
|
6818
6987
|
}
|
|
6819
|
-
|
|
6988
|
+
resolve10(providers[selection - 1]);
|
|
6820
6989
|
});
|
|
6821
6990
|
});
|
|
6822
6991
|
}
|
|
6823
6992
|
function ensureDir(dirPath) {
|
|
6824
|
-
if (!
|
|
6825
|
-
|
|
6993
|
+
if (!fs17.existsSync(dirPath)) {
|
|
6994
|
+
fs17.mkdirSync(dirPath, { recursive: true });
|
|
6826
6995
|
}
|
|
6827
6996
|
}
|
|
6997
|
+
function buildInitConfig(params) {
|
|
6998
|
+
const defaults = getDefaultConfig();
|
|
6999
|
+
return {
|
|
7000
|
+
$schema: "https://json-schema.org/schema",
|
|
7001
|
+
projectName: params.projectName,
|
|
7002
|
+
defaultBranch: params.defaultBranch,
|
|
7003
|
+
prdDir: params.prdDir,
|
|
7004
|
+
maxRuntime: defaults.maxRuntime,
|
|
7005
|
+
reviewerMaxRuntime: defaults.reviewerMaxRuntime,
|
|
7006
|
+
branchPrefix: defaults.branchPrefix,
|
|
7007
|
+
branchPatterns: [...defaults.branchPatterns],
|
|
7008
|
+
minReviewScore: defaults.minReviewScore,
|
|
7009
|
+
maxLogSize: defaults.maxLogSize,
|
|
7010
|
+
cronSchedule: defaults.cronSchedule,
|
|
7011
|
+
reviewerSchedule: defaults.reviewerSchedule,
|
|
7012
|
+
scheduleBundleId: defaults.scheduleBundleId ?? null,
|
|
7013
|
+
cronScheduleOffset: defaults.cronScheduleOffset,
|
|
7014
|
+
schedulingPriority: defaults.schedulingPriority,
|
|
7015
|
+
maxRetries: defaults.maxRetries,
|
|
7016
|
+
reviewerMaxRetries: defaults.reviewerMaxRetries,
|
|
7017
|
+
reviewerRetryDelay: defaults.reviewerRetryDelay,
|
|
7018
|
+
provider: params.provider,
|
|
7019
|
+
providerLabel: "",
|
|
7020
|
+
executorEnabled: defaults.executorEnabled ?? true,
|
|
7021
|
+
reviewerEnabled: params.reviewerEnabled,
|
|
7022
|
+
providerEnv: { ...defaults.providerEnv },
|
|
7023
|
+
notifications: {
|
|
7024
|
+
...defaults.notifications,
|
|
7025
|
+
webhooks: [...defaults.notifications?.webhooks ?? []]
|
|
7026
|
+
},
|
|
7027
|
+
prdPriority: [...defaults.prdPriority],
|
|
7028
|
+
roadmapScanner: { ...defaults.roadmapScanner },
|
|
7029
|
+
templatesDir: defaults.templatesDir,
|
|
7030
|
+
boardProvider: { ...defaults.boardProvider },
|
|
7031
|
+
autoMerge: defaults.autoMerge,
|
|
7032
|
+
autoMergeMethod: defaults.autoMergeMethod,
|
|
7033
|
+
fallbackOnRateLimit: defaults.fallbackOnRateLimit,
|
|
7034
|
+
claudeModel: defaults.claudeModel,
|
|
7035
|
+
qa: {
|
|
7036
|
+
...defaults.qa,
|
|
7037
|
+
branchPatterns: [...defaults.qa.branchPatterns]
|
|
7038
|
+
},
|
|
7039
|
+
audit: { ...defaults.audit },
|
|
7040
|
+
jobProviders: { ...defaults.jobProviders },
|
|
7041
|
+
queue: {
|
|
7042
|
+
...defaults.queue,
|
|
7043
|
+
priority: { ...defaults.queue.priority }
|
|
7044
|
+
}
|
|
7045
|
+
};
|
|
7046
|
+
}
|
|
6828
7047
|
function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
|
|
6829
7048
|
if (customTemplatesDir !== null) {
|
|
6830
|
-
const customPath =
|
|
6831
|
-
if (
|
|
7049
|
+
const customPath = join16(customTemplatesDir, templateName);
|
|
7050
|
+
if (fs17.existsSync(customPath)) {
|
|
6832
7051
|
return { path: customPath, source: "custom" };
|
|
6833
7052
|
}
|
|
6834
7053
|
}
|
|
6835
|
-
return { path:
|
|
7054
|
+
return { path: join16(bundledTemplatesDir, templateName), source: "bundled" };
|
|
6836
7055
|
}
|
|
6837
7056
|
function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
|
|
6838
|
-
if (
|
|
7057
|
+
if (fs17.existsSync(targetPath) && !force) {
|
|
6839
7058
|
console.log(` Skipped (exists): ${targetPath}`);
|
|
6840
7059
|
return { created: false, source: source ?? "bundled" };
|
|
6841
7060
|
}
|
|
6842
|
-
const templatePath = sourcePath ??
|
|
7061
|
+
const templatePath = sourcePath ?? join16(TEMPLATES_DIR, templateName);
|
|
6843
7062
|
const resolvedSource = source ?? "bundled";
|
|
6844
|
-
let content =
|
|
7063
|
+
let content = fs17.readFileSync(templatePath, "utf-8");
|
|
6845
7064
|
for (const [key, value] of Object.entries(replacements)) {
|
|
6846
7065
|
content = content.replaceAll(key, value);
|
|
6847
7066
|
}
|
|
6848
|
-
|
|
7067
|
+
fs17.writeFileSync(targetPath, content);
|
|
6849
7068
|
console.log(` Created: ${targetPath} (${resolvedSource})`);
|
|
6850
7069
|
return { created: true, source: resolvedSource };
|
|
6851
7070
|
}
|
|
6852
7071
|
function addToGitignore(cwd) {
|
|
6853
|
-
const gitignorePath =
|
|
7072
|
+
const gitignorePath = path17.join(cwd, ".gitignore");
|
|
6854
7073
|
const entries = [
|
|
6855
7074
|
{
|
|
6856
7075
|
pattern: "/logs/",
|
|
@@ -6864,13 +7083,13 @@ function addToGitignore(cwd) {
|
|
|
6864
7083
|
},
|
|
6865
7084
|
{ pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
|
|
6866
7085
|
];
|
|
6867
|
-
if (!
|
|
7086
|
+
if (!fs17.existsSync(gitignorePath)) {
|
|
6868
7087
|
const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
|
|
6869
|
-
|
|
7088
|
+
fs17.writeFileSync(gitignorePath, lines.join("\n"));
|
|
6870
7089
|
console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
|
|
6871
7090
|
return;
|
|
6872
7091
|
}
|
|
6873
|
-
const content =
|
|
7092
|
+
const content = fs17.readFileSync(gitignorePath, "utf-8");
|
|
6874
7093
|
const missing = entries.filter((e) => !e.check(content));
|
|
6875
7094
|
if (missing.length === 0) {
|
|
6876
7095
|
console.log(` Skipped (exists): Night Watch entries in .gitignore`);
|
|
@@ -6878,7 +7097,7 @@ function addToGitignore(cwd) {
|
|
|
6878
7097
|
}
|
|
6879
7098
|
const additions = missing.map((e) => e.pattern).join("\n");
|
|
6880
7099
|
const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
|
|
6881
|
-
|
|
7100
|
+
fs17.writeFileSync(gitignorePath, newContent);
|
|
6882
7101
|
console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
|
|
6883
7102
|
}
|
|
6884
7103
|
function initCommand(program2) {
|
|
@@ -6967,54 +7186,56 @@ function initCommand(program2) {
|
|
|
6967
7186
|
"${DEFAULT_BRANCH}": defaultBranch
|
|
6968
7187
|
};
|
|
6969
7188
|
step(5, totalSteps, "Creating PRD directory structure...");
|
|
6970
|
-
const prdDirPath =
|
|
6971
|
-
const doneDirPath =
|
|
7189
|
+
const prdDirPath = path17.join(cwd, prdDir);
|
|
7190
|
+
const doneDirPath = path17.join(prdDirPath, "done");
|
|
6972
7191
|
ensureDir(doneDirPath);
|
|
6973
7192
|
success(`Created ${prdDirPath}/`);
|
|
6974
7193
|
success(`Created ${doneDirPath}/`);
|
|
6975
7194
|
step(6, totalSteps, "Creating logs directory...");
|
|
6976
|
-
const logsPath =
|
|
7195
|
+
const logsPath = path17.join(cwd, LOG_DIR);
|
|
6977
7196
|
ensureDir(logsPath);
|
|
6978
7197
|
success(`Created ${logsPath}/`);
|
|
6979
7198
|
addToGitignore(cwd);
|
|
6980
7199
|
step(7, totalSteps, "Creating instructions directory...");
|
|
6981
|
-
const instructionsDir =
|
|
7200
|
+
const instructionsDir = path17.join(cwd, "instructions");
|
|
6982
7201
|
ensureDir(instructionsDir);
|
|
6983
7202
|
success(`Created ${instructionsDir}/`);
|
|
6984
7203
|
const existingConfig = loadConfig(cwd);
|
|
6985
|
-
const customTemplatesDirPath =
|
|
6986
|
-
const customTemplatesDir =
|
|
7204
|
+
const customTemplatesDirPath = path17.join(cwd, existingConfig.templatesDir);
|
|
7205
|
+
const customTemplatesDir = fs17.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
|
|
6987
7206
|
const templateSources = [];
|
|
6988
7207
|
const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
|
|
6989
|
-
const nwResult = processTemplate("executor.md",
|
|
7208
|
+
const nwResult = processTemplate("executor.md", path17.join(instructionsDir, "executor.md"), replacements, force, nwResolution.path, nwResolution.source);
|
|
6990
7209
|
templateSources.push({ name: "executor.md", source: nwResult.source });
|
|
6991
7210
|
const peResolution = resolveTemplatePath("prd-executor.md", customTemplatesDir, TEMPLATES_DIR);
|
|
6992
|
-
const peResult = processTemplate("prd-executor.md",
|
|
7211
|
+
const peResult = processTemplate("prd-executor.md", path17.join(instructionsDir, "prd-executor.md"), replacements, force, peResolution.path, peResolution.source);
|
|
6993
7212
|
templateSources.push({ name: "prd-executor.md", source: peResult.source });
|
|
6994
7213
|
const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
|
|
6995
|
-
const prResult = processTemplate("pr-reviewer.md",
|
|
7214
|
+
const prResult = processTemplate("pr-reviewer.md", path17.join(instructionsDir, "pr-reviewer.md"), replacements, force, prResolution.path, prResolution.source);
|
|
6996
7215
|
templateSources.push({ name: "pr-reviewer.md", source: prResult.source });
|
|
6997
7216
|
const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
|
|
6998
|
-
const qaResult = processTemplate("qa.md",
|
|
7217
|
+
const qaResult = processTemplate("qa.md", path17.join(instructionsDir, "qa.md"), replacements, force, qaResolution.path, qaResolution.source);
|
|
6999
7218
|
templateSources.push({ name: "qa.md", source: qaResult.source });
|
|
7000
7219
|
const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7001
|
-
const auditResult = processTemplate("audit.md",
|
|
7220
|
+
const auditResult = processTemplate("audit.md", path17.join(instructionsDir, "audit.md"), replacements, force, auditResolution.path, auditResolution.source);
|
|
7002
7221
|
templateSources.push({ name: "audit.md", source: auditResult.source });
|
|
7003
7222
|
step(8, totalSteps, "Creating configuration file...");
|
|
7004
|
-
const configPath =
|
|
7005
|
-
if (
|
|
7223
|
+
const configPath = path17.join(cwd, CONFIG_FILE_NAME);
|
|
7224
|
+
if (fs17.existsSync(configPath) && !force) {
|
|
7006
7225
|
console.log(` Skipped (exists): ${configPath}`);
|
|
7007
7226
|
} else {
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7227
|
+
const config = buildInitConfig({
|
|
7228
|
+
projectName,
|
|
7229
|
+
defaultBranch,
|
|
7230
|
+
provider: selectedProvider,
|
|
7231
|
+
reviewerEnabled,
|
|
7232
|
+
prdDir
|
|
7233
|
+
});
|
|
7234
|
+
fs17.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
7014
7235
|
success(`Created ${configPath}`);
|
|
7015
7236
|
}
|
|
7016
7237
|
step(9, totalSteps, "Setting up GitHub Project board...");
|
|
7017
|
-
const existingRaw = JSON.parse(
|
|
7238
|
+
const existingRaw = JSON.parse(fs17.readFileSync(configPath, "utf-8"));
|
|
7018
7239
|
const existingBoard = existingRaw.boardProvider;
|
|
7019
7240
|
if (existingBoard?.projectNumber && !force) {
|
|
7020
7241
|
info(`Board already configured (#${existingBoard.projectNumber}), skipping.`);
|
|
@@ -7036,13 +7257,13 @@ function initCommand(program2) {
|
|
|
7036
7257
|
const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
|
|
7037
7258
|
const boardTitle = `${projectName} Night Watch`;
|
|
7038
7259
|
const board = await provider.setupBoard(boardTitle);
|
|
7039
|
-
const rawConfig = JSON.parse(
|
|
7260
|
+
const rawConfig = JSON.parse(fs17.readFileSync(configPath, "utf-8"));
|
|
7040
7261
|
rawConfig.boardProvider = {
|
|
7041
7262
|
enabled: true,
|
|
7042
7263
|
provider: "github",
|
|
7043
7264
|
projectNumber: board.number
|
|
7044
7265
|
};
|
|
7045
|
-
|
|
7266
|
+
fs17.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
|
|
7046
7267
|
success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
|
|
7047
7268
|
} catch (boardErr) {
|
|
7048
7269
|
console.warn(` Warning: Could not set up GitHub Project board: ${boardErr instanceof Error ? boardErr.message : String(boardErr)}`);
|
|
@@ -7109,12 +7330,23 @@ function buildBaseEnvVars(config, jobType, isDryRun) {
|
|
|
7109
7330
|
env.NW_QUEUE_MAX_CONCURRENCY = String(queueConfig.maxConcurrency);
|
|
7110
7331
|
env.NW_QUEUE_MAX_WAIT_TIME = String(queueConfig.maxWaitTime);
|
|
7111
7332
|
env.NW_QUEUE_PRIORITY_JSON = JSON.stringify(queueConfig.priority);
|
|
7333
|
+
env.NW_SCHEDULING_PRIORITY = String(config.schedulingPriority ?? 3);
|
|
7112
7334
|
if (isDryRun) {
|
|
7113
7335
|
env.NW_DRY_RUN = "1";
|
|
7114
7336
|
}
|
|
7115
7337
|
env.NW_EXECUTION_CONTEXT = "agent";
|
|
7116
7338
|
return env;
|
|
7117
7339
|
}
|
|
7340
|
+
async function maybeApplyCronSchedulingDelay(config, jobType, projectDir) {
|
|
7341
|
+
const plan = getSchedulingPlan(projectDir, config, jobType);
|
|
7342
|
+
if (process.env.NW_CRON_TRIGGER !== "1" || process.env.NW_QUEUE_DISPATCHED === "1") {
|
|
7343
|
+
return plan;
|
|
7344
|
+
}
|
|
7345
|
+
if (plan.totalDelayMinutes > 0) {
|
|
7346
|
+
await new Promise((resolve10) => setTimeout(resolve10, plan.totalDelayMinutes * 6e4));
|
|
7347
|
+
}
|
|
7348
|
+
return plan;
|
|
7349
|
+
}
|
|
7118
7350
|
function formatProviderDisplay(providerCmd, providerLabel) {
|
|
7119
7351
|
const cmd = providerCmd?.trim();
|
|
7120
7352
|
if (!cmd)
|
|
@@ -7157,12 +7389,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
|
|
|
7157
7389
|
return scriptStatus === "skip_no_eligible_prd";
|
|
7158
7390
|
}
|
|
7159
7391
|
function getCrossProjectFallbackCandidates(currentProjectDir) {
|
|
7160
|
-
const current =
|
|
7392
|
+
const current = path18.resolve(currentProjectDir);
|
|
7161
7393
|
const { valid, invalid } = validateRegistry();
|
|
7162
7394
|
for (const entry of invalid) {
|
|
7163
7395
|
warn(`Skipping invalid registry entry: ${entry.path}`);
|
|
7164
7396
|
}
|
|
7165
|
-
return valid.filter((entry) =>
|
|
7397
|
+
return valid.filter((entry) => path18.resolve(entry.path) !== current);
|
|
7166
7398
|
}
|
|
7167
7399
|
async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
|
|
7168
7400
|
if (isRateLimitFallbackTriggered(scriptResult?.data)) {
|
|
@@ -7170,7 +7402,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
|
|
|
7170
7402
|
if (nonTelegramWebhooks.length > 0) {
|
|
7171
7403
|
const _rateLimitCtx = {
|
|
7172
7404
|
event: "rate_limit_fallback",
|
|
7173
|
-
projectName:
|
|
7405
|
+
projectName: path18.basename(projectDir),
|
|
7174
7406
|
exitCode,
|
|
7175
7407
|
provider: config.provider
|
|
7176
7408
|
};
|
|
@@ -7195,7 +7427,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
|
|
|
7195
7427
|
const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
|
|
7196
7428
|
const _ctx = {
|
|
7197
7429
|
event,
|
|
7198
|
-
projectName:
|
|
7430
|
+
projectName: path18.basename(projectDir),
|
|
7199
7431
|
exitCode,
|
|
7200
7432
|
provider: config.provider,
|
|
7201
7433
|
prdName: scriptResult?.data.prd,
|
|
@@ -7296,20 +7528,20 @@ function applyCliOverrides(config, options) {
|
|
|
7296
7528
|
return overridden;
|
|
7297
7529
|
}
|
|
7298
7530
|
function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
|
|
7299
|
-
const absolutePrdDir =
|
|
7300
|
-
const doneDir =
|
|
7531
|
+
const absolutePrdDir = path18.join(projectDir, prdDir);
|
|
7532
|
+
const doneDir = path18.join(absolutePrdDir, "done");
|
|
7301
7533
|
const pending = [];
|
|
7302
7534
|
const completed = [];
|
|
7303
|
-
if (
|
|
7304
|
-
const entries =
|
|
7535
|
+
if (fs18.existsSync(absolutePrdDir)) {
|
|
7536
|
+
const entries = fs18.readdirSync(absolutePrdDir, { withFileTypes: true });
|
|
7305
7537
|
for (const entry of entries) {
|
|
7306
7538
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7307
|
-
const claimPath =
|
|
7539
|
+
const claimPath = path18.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
|
|
7308
7540
|
let claimed = false;
|
|
7309
7541
|
let claimInfo = null;
|
|
7310
|
-
if (
|
|
7542
|
+
if (fs18.existsSync(claimPath)) {
|
|
7311
7543
|
try {
|
|
7312
|
-
const content =
|
|
7544
|
+
const content = fs18.readFileSync(claimPath, "utf-8");
|
|
7313
7545
|
const data = JSON.parse(content);
|
|
7314
7546
|
const age = Math.floor(Date.now() / 1e3) - data.timestamp;
|
|
7315
7547
|
if (age < maxRuntime) {
|
|
@@ -7323,8 +7555,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
|
|
|
7323
7555
|
}
|
|
7324
7556
|
}
|
|
7325
7557
|
}
|
|
7326
|
-
if (
|
|
7327
|
-
const entries =
|
|
7558
|
+
if (fs18.existsSync(doneDir)) {
|
|
7559
|
+
const entries = fs18.readdirSync(doneDir, { withFileTypes: true });
|
|
7328
7560
|
for (const entry of entries) {
|
|
7329
7561
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7330
7562
|
completed.push(entry.name);
|
|
@@ -7433,6 +7665,7 @@ function runCommand(program2) {
|
|
|
7433
7665
|
const spinner = createSpinner("Running PRD executor...");
|
|
7434
7666
|
spinner.start();
|
|
7435
7667
|
try {
|
|
7668
|
+
await maybeApplyCronSchedulingDelay(config, "executor", projectDir);
|
|
7436
7669
|
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars, { cwd: projectDir });
|
|
7437
7670
|
const scriptResult = parseScriptResult(`${stdout}
|
|
7438
7671
|
${stderr}`);
|
|
@@ -7650,6 +7883,7 @@ function reviewCommand(program2) {
|
|
|
7650
7883
|
const spinner = createSpinner("Running PR reviewer...");
|
|
7651
7884
|
spinner.start();
|
|
7652
7885
|
try {
|
|
7886
|
+
await maybeApplyCronSchedulingDelay(config, "reviewer", projectDir);
|
|
7653
7887
|
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
|
|
7654
7888
|
const scriptResult = parseScriptResult(`${stdout}
|
|
7655
7889
|
${stderr}`);
|
|
@@ -7688,7 +7922,7 @@ ${stderr}`);
|
|
|
7688
7922
|
const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
|
|
7689
7923
|
const _reviewCtx = {
|
|
7690
7924
|
event: "review_completed",
|
|
7691
|
-
projectName:
|
|
7925
|
+
projectName: path19.basename(projectDir),
|
|
7692
7926
|
exitCode,
|
|
7693
7927
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
7694
7928
|
prUrl: prDetails?.url,
|
|
@@ -7709,7 +7943,7 @@ ${stderr}`);
|
|
|
7709
7943
|
const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
|
|
7710
7944
|
const _mergeCtx = {
|
|
7711
7945
|
event: "pr_auto_merged",
|
|
7712
|
-
projectName:
|
|
7946
|
+
projectName: path19.basename(projectDir),
|
|
7713
7947
|
exitCode,
|
|
7714
7948
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
7715
7949
|
prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
|
|
@@ -7833,6 +8067,7 @@ function qaCommand(program2) {
|
|
|
7833
8067
|
const spinner = createSpinner("Running QA process...");
|
|
7834
8068
|
spinner.start();
|
|
7835
8069
|
try {
|
|
8070
|
+
await maybeApplyCronSchedulingDelay(config, "qa", projectDir);
|
|
7836
8071
|
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
|
|
7837
8072
|
const scriptResult = parseScriptResult(`${stdout}
|
|
7838
8073
|
${stderr}`);
|
|
@@ -7861,7 +8096,7 @@ ${stderr}`);
|
|
|
7861
8096
|
const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
|
|
7862
8097
|
const _qaCtx = {
|
|
7863
8098
|
event: "qa_completed",
|
|
7864
|
-
projectName:
|
|
8099
|
+
projectName: path20.basename(projectDir),
|
|
7865
8100
|
exitCode,
|
|
7866
8101
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
7867
8102
|
prNumber: prDetails?.number ?? primaryQaPr,
|
|
@@ -7927,7 +8162,7 @@ function auditCommand(program2) {
|
|
|
7927
8162
|
configTable.push(["Provider", auditProvider]);
|
|
7928
8163
|
configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
|
|
7929
8164
|
configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
|
|
7930
|
-
configTable.push(["Report File",
|
|
8165
|
+
configTable.push(["Report File", path21.join(projectDir, "logs", "audit-report.md")]);
|
|
7931
8166
|
console.log(configTable.toString());
|
|
7932
8167
|
header("Provider Invocation");
|
|
7933
8168
|
const providerCmd = PROVIDER_COMMANDS[auditProvider];
|
|
@@ -7944,6 +8179,7 @@ function auditCommand(program2) {
|
|
|
7944
8179
|
const spinner = createSpinner("Running code audit...");
|
|
7945
8180
|
spinner.start();
|
|
7946
8181
|
try {
|
|
8182
|
+
await maybeApplyCronSchedulingDelay(config, "audit", projectDir);
|
|
7947
8183
|
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
|
|
7948
8184
|
const scriptResult = parseScriptResult(`${stdout}
|
|
7949
8185
|
${stderr}`);
|
|
@@ -7955,8 +8191,8 @@ ${stderr}`);
|
|
|
7955
8191
|
} else if (scriptResult?.status?.startsWith("skip_")) {
|
|
7956
8192
|
spinner.succeed("Code audit skipped");
|
|
7957
8193
|
} else {
|
|
7958
|
-
const reportPath =
|
|
7959
|
-
if (!
|
|
8194
|
+
const reportPath = path21.join(projectDir, "logs", "audit-report.md");
|
|
8195
|
+
if (!fs19.existsSync(reportPath)) {
|
|
7960
8196
|
spinner.fail("Code audit finished without a report file");
|
|
7961
8197
|
process.exit(1);
|
|
7962
8198
|
}
|
|
@@ -7967,9 +8203,9 @@ ${stderr}`);
|
|
|
7967
8203
|
const providerExit = scriptResult?.data?.provider_exit;
|
|
7968
8204
|
const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
|
|
7969
8205
|
spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
|
|
7970
|
-
const logPath =
|
|
7971
|
-
if (
|
|
7972
|
-
const logLines =
|
|
8206
|
+
const logPath = path21.join(projectDir, "logs", "audit.log");
|
|
8207
|
+
if (fs19.existsSync(logPath)) {
|
|
8208
|
+
const logLines = fs19.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
|
|
7973
8209
|
if (logLines.length > 0) {
|
|
7974
8210
|
process.stderr.write(logLines.join("\n") + "\n");
|
|
7975
8211
|
}
|
|
@@ -7989,8 +8225,8 @@ function shellQuote(value) {
|
|
|
7989
8225
|
function getNightWatchBinPath() {
|
|
7990
8226
|
try {
|
|
7991
8227
|
const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
|
|
7992
|
-
const binPath =
|
|
7993
|
-
if (
|
|
8228
|
+
const binPath = path22.join(npmBin, "night-watch");
|
|
8229
|
+
if (fs20.existsSync(binPath)) {
|
|
7994
8230
|
return binPath;
|
|
7995
8231
|
}
|
|
7996
8232
|
} catch {
|
|
@@ -8004,45 +8240,32 @@ function getNightWatchBinPath() {
|
|
|
8004
8240
|
function getNodeBinDir() {
|
|
8005
8241
|
try {
|
|
8006
8242
|
const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
|
|
8007
|
-
return
|
|
8243
|
+
return path22.dirname(nodePath);
|
|
8008
8244
|
} catch {
|
|
8009
8245
|
return "";
|
|
8010
8246
|
}
|
|
8011
8247
|
}
|
|
8012
8248
|
function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
|
|
8013
|
-
const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ?
|
|
8249
|
+
const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path22.dirname(nightWatchBin) : "";
|
|
8014
8250
|
const pathParts = Array.from(new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0)));
|
|
8015
8251
|
if (pathParts.length === 0) {
|
|
8016
8252
|
return "";
|
|
8017
8253
|
}
|
|
8018
8254
|
return `export PATH="${pathParts.join(":")}:$PATH" && `;
|
|
8019
8255
|
}
|
|
8020
|
-
function applyScheduleOffset(schedule, offset) {
|
|
8021
|
-
if (offset === 0)
|
|
8022
|
-
return schedule;
|
|
8023
|
-
const parts = schedule.split(/\s+/);
|
|
8024
|
-
if (parts.length < 5)
|
|
8025
|
-
return schedule;
|
|
8026
|
-
if (/^\d+$/.test(parts[0])) {
|
|
8027
|
-
parts[0] = String(offset);
|
|
8028
|
-
return parts.join(" ");
|
|
8029
|
-
}
|
|
8030
|
-
return schedule;
|
|
8031
|
-
}
|
|
8032
8256
|
function performInstall(projectDir, config, options) {
|
|
8033
8257
|
try {
|
|
8034
|
-
const
|
|
8035
|
-
const
|
|
8036
|
-
const reviewerSchedule = applyScheduleOffset(options?.reviewerSchedule || config.reviewerSchedule, offset);
|
|
8258
|
+
const executorSchedule = options?.schedule || config.cronSchedule;
|
|
8259
|
+
const reviewerSchedule = options?.reviewerSchedule || config.reviewerSchedule;
|
|
8037
8260
|
const nightWatchBin = getNightWatchBinPath();
|
|
8038
8261
|
const projectName = getProjectName(projectDir);
|
|
8039
8262
|
const marker = generateMarker(projectName);
|
|
8040
|
-
const logDir =
|
|
8041
|
-
if (!
|
|
8042
|
-
|
|
8263
|
+
const logDir = path22.join(projectDir, LOG_DIR);
|
|
8264
|
+
if (!fs20.existsSync(logDir)) {
|
|
8265
|
+
fs20.mkdirSync(logDir, { recursive: true });
|
|
8043
8266
|
}
|
|
8044
|
-
const executorLog =
|
|
8045
|
-
const reviewerLog =
|
|
8267
|
+
const executorLog = path22.join(logDir, "executor.log");
|
|
8268
|
+
const reviewerLog = path22.join(logDir, "reviewer.log");
|
|
8046
8269
|
if (!options?.force) {
|
|
8047
8270
|
const existingEntries2 = Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]));
|
|
8048
8271
|
if (existingEntries2.length > 0) {
|
|
@@ -8058,6 +8281,7 @@ function performInstall(projectDir, config, options) {
|
|
|
8058
8281
|
const nodeBinDir = getNodeBinDir();
|
|
8059
8282
|
const pathPrefix = buildCronPathPrefix(nodeBinDir, nightWatchBin);
|
|
8060
8283
|
const cliBinPrefix = `export NW_CLI_BIN=${shellQuote(nightWatchBin)} && `;
|
|
8284
|
+
const cronTriggerPrefix = "export NW_CRON_TRIGGER=1 && ";
|
|
8061
8285
|
let providerEnvPrefix = "";
|
|
8062
8286
|
if (config.providerEnv && Object.keys(config.providerEnv).length > 0) {
|
|
8063
8287
|
const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
|
|
@@ -8065,35 +8289,35 @@ function performInstall(projectDir, config, options) {
|
|
|
8065
8289
|
}
|
|
8066
8290
|
const installExecutor = config.executorEnabled !== false;
|
|
8067
8291
|
if (installExecutor) {
|
|
8068
|
-
const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
|
|
8292
|
+
const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
|
|
8069
8293
|
entries.push(executorEntry);
|
|
8070
8294
|
}
|
|
8071
8295
|
const installReviewer = options?.noReviewer === true ? false : config.reviewerEnabled;
|
|
8072
8296
|
if (installReviewer) {
|
|
8073
|
-
const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
|
|
8297
|
+
const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
|
|
8074
8298
|
entries.push(reviewerEntry);
|
|
8075
8299
|
}
|
|
8076
8300
|
const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
|
|
8077
8301
|
if (installSlicer) {
|
|
8078
|
-
const slicerSchedule =
|
|
8079
|
-
const slicerLog =
|
|
8080
|
-
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
8302
|
+
const slicerSchedule = config.roadmapScanner.slicerSchedule;
|
|
8303
|
+
const slicerLog = path22.join(logDir, "slicer.log");
|
|
8304
|
+
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
8081
8305
|
entries.push(slicerEntry);
|
|
8082
8306
|
}
|
|
8083
8307
|
const disableQa = options?.noQa === true || options?.qa === false;
|
|
8084
8308
|
const installQa = disableQa ? false : config.qa.enabled;
|
|
8085
8309
|
if (installQa) {
|
|
8086
|
-
const qaSchedule =
|
|
8087
|
-
const qaLog =
|
|
8088
|
-
const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
|
|
8310
|
+
const qaSchedule = config.qa.schedule;
|
|
8311
|
+
const qaLog = path22.join(logDir, "qa.log");
|
|
8312
|
+
const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
|
|
8089
8313
|
entries.push(qaEntry);
|
|
8090
8314
|
}
|
|
8091
8315
|
const disableAudit = options?.noAudit === true || options?.audit === false;
|
|
8092
8316
|
const installAudit = disableAudit ? false : config.audit.enabled;
|
|
8093
8317
|
if (installAudit) {
|
|
8094
|
-
const auditSchedule =
|
|
8095
|
-
const auditLog =
|
|
8096
|
-
const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
|
|
8318
|
+
const auditSchedule = config.audit.schedule;
|
|
8319
|
+
const auditLog = path22.join(logDir, "audit.log");
|
|
8320
|
+
const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
|
|
8097
8321
|
entries.push(auditEntry);
|
|
8098
8322
|
}
|
|
8099
8323
|
const existingEntries = new Set(Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])));
|
|
@@ -8116,18 +8340,17 @@ function installCommand(program2) {
|
|
|
8116
8340
|
try {
|
|
8117
8341
|
const projectDir = process.cwd();
|
|
8118
8342
|
const config = loadConfig(projectDir);
|
|
8119
|
-
const
|
|
8120
|
-
const
|
|
8121
|
-
const reviewerSchedule = applyScheduleOffset(options.reviewerSchedule || config.reviewerSchedule, offset);
|
|
8343
|
+
const executorSchedule = options.schedule || config.cronSchedule;
|
|
8344
|
+
const reviewerSchedule = options.reviewerSchedule || config.reviewerSchedule;
|
|
8122
8345
|
const nightWatchBin = getNightWatchBinPath();
|
|
8123
8346
|
const projectName = getProjectName(projectDir);
|
|
8124
8347
|
const marker = generateMarker(projectName);
|
|
8125
|
-
const logDir =
|
|
8126
|
-
if (!
|
|
8127
|
-
|
|
8348
|
+
const logDir = path22.join(projectDir, LOG_DIR);
|
|
8349
|
+
if (!fs20.existsSync(logDir)) {
|
|
8350
|
+
fs20.mkdirSync(logDir, { recursive: true });
|
|
8128
8351
|
}
|
|
8129
|
-
const executorLog =
|
|
8130
|
-
const reviewerLog =
|
|
8352
|
+
const executorLog = path22.join(logDir, "executor.log");
|
|
8353
|
+
const reviewerLog = path22.join(logDir, "reviewer.log");
|
|
8131
8354
|
const existingEntries = Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]));
|
|
8132
8355
|
if (existingEntries.length > 0 && !options.force) {
|
|
8133
8356
|
warn(`Night Watch is already installed for ${projectName}.`);
|
|
@@ -8142,6 +8365,7 @@ function installCommand(program2) {
|
|
|
8142
8365
|
const nodeBinDir = getNodeBinDir();
|
|
8143
8366
|
const pathPrefix = buildCronPathPrefix(nodeBinDir, nightWatchBin);
|
|
8144
8367
|
const cliBinPrefix = `export NW_CLI_BIN=${shellQuote(nightWatchBin)} && `;
|
|
8368
|
+
const cronTriggerPrefix = "export NW_CRON_TRIGGER=1 && ";
|
|
8145
8369
|
let providerEnvPrefix = "";
|
|
8146
8370
|
if (config.providerEnv && Object.keys(config.providerEnv).length > 0) {
|
|
8147
8371
|
const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
|
|
@@ -8149,38 +8373,38 @@ function installCommand(program2) {
|
|
|
8149
8373
|
}
|
|
8150
8374
|
const installExecutor = config.executorEnabled !== false;
|
|
8151
8375
|
if (installExecutor) {
|
|
8152
|
-
const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
|
|
8376
|
+
const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
|
|
8153
8377
|
entries.push(executorEntry);
|
|
8154
8378
|
}
|
|
8155
8379
|
const installReviewer = options.noReviewer === true ? false : config.reviewerEnabled;
|
|
8156
8380
|
if (installReviewer) {
|
|
8157
|
-
const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
|
|
8381
|
+
const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
|
|
8158
8382
|
entries.push(reviewerEntry);
|
|
8159
8383
|
}
|
|
8160
8384
|
const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
|
|
8161
8385
|
let slicerLog;
|
|
8162
8386
|
if (installSlicer) {
|
|
8163
|
-
slicerLog =
|
|
8164
|
-
const slicerSchedule =
|
|
8165
|
-
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
8387
|
+
slicerLog = path22.join(logDir, "slicer.log");
|
|
8388
|
+
const slicerSchedule = config.roadmapScanner.slicerSchedule;
|
|
8389
|
+
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
8166
8390
|
entries.push(slicerEntry);
|
|
8167
8391
|
}
|
|
8168
8392
|
const disableQa = options.noQa === true || options.qa === false;
|
|
8169
8393
|
const installQa = disableQa ? false : config.qa.enabled;
|
|
8170
8394
|
let qaLog;
|
|
8171
8395
|
if (installQa) {
|
|
8172
|
-
qaLog =
|
|
8173
|
-
const qaSchedule =
|
|
8174
|
-
const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
|
|
8396
|
+
qaLog = path22.join(logDir, "qa.log");
|
|
8397
|
+
const qaSchedule = config.qa.schedule;
|
|
8398
|
+
const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
|
|
8175
8399
|
entries.push(qaEntry);
|
|
8176
8400
|
}
|
|
8177
8401
|
const disableAudit = options.noAudit === true || options.audit === false;
|
|
8178
8402
|
const installAudit = disableAudit ? false : config.audit.enabled;
|
|
8179
8403
|
let auditLog;
|
|
8180
8404
|
if (installAudit) {
|
|
8181
|
-
auditLog =
|
|
8182
|
-
const auditSchedule =
|
|
8183
|
-
const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
|
|
8405
|
+
auditLog = path22.join(logDir, "audit.log");
|
|
8406
|
+
const auditSchedule = config.audit.schedule;
|
|
8407
|
+
const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
|
|
8184
8408
|
entries.push(auditEntry);
|
|
8185
8409
|
}
|
|
8186
8410
|
const existingEntrySet = new Set(existingEntries);
|
|
@@ -8231,19 +8455,19 @@ function performUninstall(projectDir, options) {
|
|
|
8231
8455
|
const removedCount = removeEntriesForProject(projectDir, marker);
|
|
8232
8456
|
unregisterProject(projectDir);
|
|
8233
8457
|
if (!options?.keepLogs) {
|
|
8234
|
-
const logDir =
|
|
8235
|
-
if (
|
|
8458
|
+
const logDir = path23.join(projectDir, "logs");
|
|
8459
|
+
if (fs21.existsSync(logDir)) {
|
|
8236
8460
|
const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
|
|
8237
8461
|
logFiles.forEach((logFile) => {
|
|
8238
|
-
const logPath =
|
|
8239
|
-
if (
|
|
8240
|
-
|
|
8462
|
+
const logPath = path23.join(logDir, logFile);
|
|
8463
|
+
if (fs21.existsSync(logPath)) {
|
|
8464
|
+
fs21.unlinkSync(logPath);
|
|
8241
8465
|
}
|
|
8242
8466
|
});
|
|
8243
8467
|
try {
|
|
8244
|
-
const remainingFiles =
|
|
8468
|
+
const remainingFiles = fs21.readdirSync(logDir);
|
|
8245
8469
|
if (remainingFiles.length === 0) {
|
|
8246
|
-
|
|
8470
|
+
fs21.rmdirSync(logDir);
|
|
8247
8471
|
}
|
|
8248
8472
|
} catch {
|
|
8249
8473
|
}
|
|
@@ -8274,21 +8498,21 @@ function uninstallCommand(program2) {
|
|
|
8274
8498
|
existingEntries.forEach((entry) => dim(` ${entry}`));
|
|
8275
8499
|
const removedCount = removeEntriesForProject(projectDir, marker);
|
|
8276
8500
|
if (!options.keepLogs) {
|
|
8277
|
-
const logDir =
|
|
8278
|
-
if (
|
|
8501
|
+
const logDir = path23.join(projectDir, "logs");
|
|
8502
|
+
if (fs21.existsSync(logDir)) {
|
|
8279
8503
|
const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
|
|
8280
8504
|
let logsRemoved = 0;
|
|
8281
8505
|
logFiles.forEach((logFile) => {
|
|
8282
|
-
const logPath =
|
|
8283
|
-
if (
|
|
8284
|
-
|
|
8506
|
+
const logPath = path23.join(logDir, logFile);
|
|
8507
|
+
if (fs21.existsSync(logPath)) {
|
|
8508
|
+
fs21.unlinkSync(logPath);
|
|
8285
8509
|
logsRemoved++;
|
|
8286
8510
|
}
|
|
8287
8511
|
});
|
|
8288
8512
|
try {
|
|
8289
|
-
const remainingFiles =
|
|
8513
|
+
const remainingFiles = fs21.readdirSync(logDir);
|
|
8290
8514
|
if (remainingFiles.length === 0) {
|
|
8291
|
-
|
|
8515
|
+
fs21.rmdirSync(logDir);
|
|
8292
8516
|
}
|
|
8293
8517
|
} catch {
|
|
8294
8518
|
}
|
|
@@ -8514,11 +8738,11 @@ function statusCommand(program2) {
|
|
|
8514
8738
|
}
|
|
8515
8739
|
init_dist();
|
|
8516
8740
|
function getLastLines(filePath, lineCount) {
|
|
8517
|
-
if (!
|
|
8741
|
+
if (!fs22.existsSync(filePath)) {
|
|
8518
8742
|
return `Log file not found: ${filePath}`;
|
|
8519
8743
|
}
|
|
8520
8744
|
try {
|
|
8521
|
-
const content =
|
|
8745
|
+
const content = fs22.readFileSync(filePath, "utf-8");
|
|
8522
8746
|
const lines = content.trim().split("\n");
|
|
8523
8747
|
return lines.slice(-lineCount).join("\n");
|
|
8524
8748
|
} catch (error2) {
|
|
@@ -8526,7 +8750,7 @@ function getLastLines(filePath, lineCount) {
|
|
|
8526
8750
|
}
|
|
8527
8751
|
}
|
|
8528
8752
|
function followLog(filePath) {
|
|
8529
|
-
if (!
|
|
8753
|
+
if (!fs22.existsSync(filePath)) {
|
|
8530
8754
|
console.log(`Log file not found: ${filePath}`);
|
|
8531
8755
|
console.log("The log file will be created when the first execution runs.");
|
|
8532
8756
|
return;
|
|
@@ -8546,13 +8770,13 @@ function logsCommand(program2) {
|
|
|
8546
8770
|
program2.command("logs").description("View night-watch log output").option("-n, --lines <count>", "Number of lines to show", "50").option("-f, --follow", "Follow log output (tail -f)").option("-t, --type <type>", "Log type to view (executor|reviewer|qa|audit|planner|all)", "all").action(async (options) => {
|
|
8547
8771
|
try {
|
|
8548
8772
|
const projectDir = process.cwd();
|
|
8549
|
-
const logDir =
|
|
8773
|
+
const logDir = path24.join(projectDir, LOG_DIR);
|
|
8550
8774
|
const lineCount = parseInt(options.lines || "50", 10);
|
|
8551
|
-
const executorLog =
|
|
8552
|
-
const reviewerLog =
|
|
8553
|
-
const qaLog =
|
|
8554
|
-
const auditLog =
|
|
8555
|
-
const plannerLog =
|
|
8775
|
+
const executorLog = path24.join(logDir, EXECUTOR_LOG_FILE);
|
|
8776
|
+
const reviewerLog = path24.join(logDir, REVIEWER_LOG_FILE);
|
|
8777
|
+
const qaLog = path24.join(logDir, `${QA_LOG_NAME}.log`);
|
|
8778
|
+
const auditLog = path24.join(logDir, `${AUDIT_LOG_NAME}.log`);
|
|
8779
|
+
const plannerLog = path24.join(logDir, `${PLANNER_LOG_NAME}.log`);
|
|
8556
8780
|
const logType = options.type?.toLowerCase() || "all";
|
|
8557
8781
|
const showExecutor = logType === "all" || logType === "run" || logType === "executor";
|
|
8558
8782
|
const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
|
|
@@ -8622,9 +8846,9 @@ function slugify2(name) {
|
|
|
8622
8846
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
8623
8847
|
}
|
|
8624
8848
|
function getNextPrdNumber2(prdDir) {
|
|
8625
|
-
if (!
|
|
8849
|
+
if (!fs23.existsSync(prdDir))
|
|
8626
8850
|
return 1;
|
|
8627
|
-
const files =
|
|
8851
|
+
const files = fs23.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
|
|
8628
8852
|
const numbers = files.map((f) => {
|
|
8629
8853
|
const match = f.match(/^(\d+)-/);
|
|
8630
8854
|
return match ? parseInt(match[1], 10) : 0;
|
|
@@ -8632,9 +8856,9 @@ function getNextPrdNumber2(prdDir) {
|
|
|
8632
8856
|
return Math.max(0, ...numbers) + 1;
|
|
8633
8857
|
}
|
|
8634
8858
|
function prompt(rl, question) {
|
|
8635
|
-
return new Promise((
|
|
8859
|
+
return new Promise((resolve10) => {
|
|
8636
8860
|
rl.question(question, (answer) => {
|
|
8637
|
-
|
|
8861
|
+
resolve10(answer.trim());
|
|
8638
8862
|
});
|
|
8639
8863
|
});
|
|
8640
8864
|
}
|
|
@@ -8646,10 +8870,10 @@ function parseDependencies(content) {
|
|
|
8646
8870
|
}
|
|
8647
8871
|
function isClaimActive(claimPath, maxRuntime) {
|
|
8648
8872
|
try {
|
|
8649
|
-
if (!
|
|
8873
|
+
if (!fs23.existsSync(claimPath)) {
|
|
8650
8874
|
return { active: false };
|
|
8651
8875
|
}
|
|
8652
|
-
const content =
|
|
8876
|
+
const content = fs23.readFileSync(claimPath, "utf-8");
|
|
8653
8877
|
const claim = JSON.parse(content);
|
|
8654
8878
|
const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
|
|
8655
8879
|
if (age < maxRuntime) {
|
|
@@ -8665,9 +8889,9 @@ function prdCommand(program2) {
|
|
|
8665
8889
|
prd.command("create").description("Generate a new PRD markdown file from template").argument("<name>", "PRD name (used for title and filename)").option("-i, --interactive", "Prompt for complexity, dependencies, and phase count", false).option("-t, --template <path>", "Path to a custom template file").option("--deps <files>", "Comma-separated dependency filenames").option("--phases <count>", "Number of execution phases", "3").option("--no-number", "Skip auto-numbering prefix").action(async (name, options) => {
|
|
8666
8890
|
const projectDir = process.cwd();
|
|
8667
8891
|
const config = loadConfig(projectDir);
|
|
8668
|
-
const prdDir =
|
|
8669
|
-
if (!
|
|
8670
|
-
|
|
8892
|
+
const prdDir = path25.join(projectDir, config.prdDir);
|
|
8893
|
+
if (!fs23.existsSync(prdDir)) {
|
|
8894
|
+
fs23.mkdirSync(prdDir, { recursive: true });
|
|
8671
8895
|
}
|
|
8672
8896
|
let complexityScore = 5;
|
|
8673
8897
|
let dependsOn = [];
|
|
@@ -8723,20 +8947,20 @@ function prdCommand(program2) {
|
|
|
8723
8947
|
} else {
|
|
8724
8948
|
filename = `${slug}.md`;
|
|
8725
8949
|
}
|
|
8726
|
-
const filePath =
|
|
8727
|
-
if (
|
|
8950
|
+
const filePath = path25.join(prdDir, filename);
|
|
8951
|
+
if (fs23.existsSync(filePath)) {
|
|
8728
8952
|
error(`File already exists: ${filePath}`);
|
|
8729
8953
|
dim("Use a different name or remove the existing file.");
|
|
8730
8954
|
process.exit(1);
|
|
8731
8955
|
}
|
|
8732
8956
|
let customTemplate;
|
|
8733
8957
|
if (options.template) {
|
|
8734
|
-
const templatePath =
|
|
8735
|
-
if (!
|
|
8958
|
+
const templatePath = path25.resolve(options.template);
|
|
8959
|
+
if (!fs23.existsSync(templatePath)) {
|
|
8736
8960
|
error(`Template file not found: ${templatePath}`);
|
|
8737
8961
|
process.exit(1);
|
|
8738
8962
|
}
|
|
8739
|
-
customTemplate =
|
|
8963
|
+
customTemplate = fs23.readFileSync(templatePath, "utf-8");
|
|
8740
8964
|
}
|
|
8741
8965
|
const vars = {
|
|
8742
8966
|
title: name,
|
|
@@ -8747,7 +8971,7 @@ function prdCommand(program2) {
|
|
|
8747
8971
|
phaseCount
|
|
8748
8972
|
};
|
|
8749
8973
|
const content = renderPrdTemplate(vars, customTemplate);
|
|
8750
|
-
|
|
8974
|
+
fs23.writeFileSync(filePath, content, "utf-8");
|
|
8751
8975
|
header("PRD Created");
|
|
8752
8976
|
success(`Created: ${filePath}`);
|
|
8753
8977
|
info(`Title: ${name}`);
|
|
@@ -8759,15 +8983,15 @@ function prdCommand(program2) {
|
|
|
8759
8983
|
prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
|
|
8760
8984
|
const projectDir = process.cwd();
|
|
8761
8985
|
const config = loadConfig(projectDir);
|
|
8762
|
-
const absolutePrdDir =
|
|
8763
|
-
const doneDir =
|
|
8986
|
+
const absolutePrdDir = path25.join(projectDir, config.prdDir);
|
|
8987
|
+
const doneDir = path25.join(absolutePrdDir, "done");
|
|
8764
8988
|
const pending = [];
|
|
8765
|
-
if (
|
|
8766
|
-
const files =
|
|
8989
|
+
if (fs23.existsSync(absolutePrdDir)) {
|
|
8990
|
+
const files = fs23.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
|
|
8767
8991
|
for (const file of files) {
|
|
8768
|
-
const content =
|
|
8992
|
+
const content = fs23.readFileSync(path25.join(absolutePrdDir, file), "utf-8");
|
|
8769
8993
|
const deps = parseDependencies(content);
|
|
8770
|
-
const claimPath =
|
|
8994
|
+
const claimPath = path25.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
|
|
8771
8995
|
const claimStatus = isClaimActive(claimPath, config.maxRuntime);
|
|
8772
8996
|
pending.push({
|
|
8773
8997
|
name: file,
|
|
@@ -8778,10 +9002,10 @@ function prdCommand(program2) {
|
|
|
8778
9002
|
}
|
|
8779
9003
|
}
|
|
8780
9004
|
const done = [];
|
|
8781
|
-
if (
|
|
8782
|
-
const files =
|
|
9005
|
+
if (fs23.existsSync(doneDir)) {
|
|
9006
|
+
const files = fs23.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
|
|
8783
9007
|
for (const file of files) {
|
|
8784
|
-
const content =
|
|
9008
|
+
const content = fs23.readFileSync(path25.join(doneDir, file), "utf-8");
|
|
8785
9009
|
const deps = parseDependencies(content);
|
|
8786
9010
|
done.push({ name: file, dependencies: deps });
|
|
8787
9011
|
}
|
|
@@ -8911,7 +9135,7 @@ function renderLogPane(projectDir, logs) {
|
|
|
8911
9135
|
let newestMtime = 0;
|
|
8912
9136
|
for (const log of existingLogs) {
|
|
8913
9137
|
try {
|
|
8914
|
-
const stat =
|
|
9138
|
+
const stat = fs24.statSync(log.path);
|
|
8915
9139
|
if (stat.mtimeMs > newestMtime) {
|
|
8916
9140
|
newestMtime = stat.mtimeMs;
|
|
8917
9141
|
newestLog = log;
|
|
@@ -10592,7 +10816,7 @@ function createLogsTab() {
|
|
|
10592
10816
|
let activeKeyHandlers = [];
|
|
10593
10817
|
let activeCtx = null;
|
|
10594
10818
|
function getLogPath(projectDir, logName) {
|
|
10595
|
-
return
|
|
10819
|
+
return path26.join(projectDir, "logs", `${logName}.log`);
|
|
10596
10820
|
}
|
|
10597
10821
|
function updateSelector() {
|
|
10598
10822
|
const tabs = LOG_NAMES.map((name, idx) => {
|
|
@@ -10606,7 +10830,7 @@ function createLogsTab() {
|
|
|
10606
10830
|
function loadLog(ctx) {
|
|
10607
10831
|
const logName = LOG_NAMES[selectedLogIndex];
|
|
10608
10832
|
const logPath = getLogPath(ctx.projectDir, logName);
|
|
10609
|
-
if (!
|
|
10833
|
+
if (!fs25.existsSync(logPath)) {
|
|
10610
10834
|
logContent.setContent(`{yellow-fg}No ${logName}.log file found{/yellow-fg}
|
|
10611
10835
|
|
|
10612
10836
|
Log will appear here once the ${logName} runs.`);
|
|
@@ -10614,7 +10838,7 @@ Log will appear here once the ${logName} runs.`);
|
|
|
10614
10838
|
return;
|
|
10615
10839
|
}
|
|
10616
10840
|
try {
|
|
10617
|
-
const stat =
|
|
10841
|
+
const stat = fs25.statSync(logPath);
|
|
10618
10842
|
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
10619
10843
|
logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
|
|
10620
10844
|
} catch {
|
|
@@ -11179,7 +11403,7 @@ function resolveProject(req, res, next) {
|
|
|
11179
11403
|
res.status(404).json({ error: `Project not found: ${decodedId}` });
|
|
11180
11404
|
return;
|
|
11181
11405
|
}
|
|
11182
|
-
if (!
|
|
11406
|
+
if (!fs26.existsSync(entry.path) || !fs26.existsSync(path27.join(entry.path, CONFIG_FILE_NAME))) {
|
|
11183
11407
|
res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
|
|
11184
11408
|
return;
|
|
11185
11409
|
}
|
|
@@ -11251,17 +11475,17 @@ function getBoardProvider(config, projectDir) {
|
|
|
11251
11475
|
function cleanOrphanedClaims(dir) {
|
|
11252
11476
|
let entries;
|
|
11253
11477
|
try {
|
|
11254
|
-
entries =
|
|
11478
|
+
entries = fs27.readdirSync(dir, { withFileTypes: true });
|
|
11255
11479
|
} catch {
|
|
11256
11480
|
return;
|
|
11257
11481
|
}
|
|
11258
11482
|
for (const entry of entries) {
|
|
11259
|
-
const fullPath =
|
|
11483
|
+
const fullPath = path28.join(dir, entry.name);
|
|
11260
11484
|
if (entry.isDirectory() && entry.name !== "done") {
|
|
11261
11485
|
cleanOrphanedClaims(fullPath);
|
|
11262
11486
|
} else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
|
|
11263
11487
|
try {
|
|
11264
|
-
|
|
11488
|
+
fs27.unlinkSync(fullPath);
|
|
11265
11489
|
} catch {
|
|
11266
11490
|
}
|
|
11267
11491
|
}
|
|
@@ -11309,7 +11533,7 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
|
|
|
11309
11533
|
const config = loadConfig(projectDir);
|
|
11310
11534
|
sendNotifications(config, {
|
|
11311
11535
|
event: "run_started",
|
|
11312
|
-
projectName:
|
|
11536
|
+
projectName: path28.basename(projectDir),
|
|
11313
11537
|
exitCode: 0,
|
|
11314
11538
|
provider: config.provider
|
|
11315
11539
|
}).catch(() => {
|
|
@@ -11421,19 +11645,19 @@ function createActionRouteHandlers(ctx) {
|
|
|
11421
11645
|
res.status(400).json({ error: "Invalid PRD name" });
|
|
11422
11646
|
return;
|
|
11423
11647
|
}
|
|
11424
|
-
const prdDir =
|
|
11648
|
+
const prdDir = path28.join(projectDir, config.prdDir);
|
|
11425
11649
|
const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
|
|
11426
|
-
const pendingPath =
|
|
11427
|
-
const donePath =
|
|
11428
|
-
if (
|
|
11650
|
+
const pendingPath = path28.join(prdDir, normalized);
|
|
11651
|
+
const donePath = path28.join(prdDir, "done", normalized);
|
|
11652
|
+
if (fs27.existsSync(pendingPath)) {
|
|
11429
11653
|
res.json({ message: `"${normalized}" is already pending` });
|
|
11430
11654
|
return;
|
|
11431
11655
|
}
|
|
11432
|
-
if (!
|
|
11656
|
+
if (!fs27.existsSync(donePath)) {
|
|
11433
11657
|
res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
|
|
11434
11658
|
return;
|
|
11435
11659
|
}
|
|
11436
|
-
|
|
11660
|
+
fs27.renameSync(donePath, pendingPath);
|
|
11437
11661
|
res.json({ message: `Moved "${normalized}" back to pending` });
|
|
11438
11662
|
} catch (error2) {
|
|
11439
11663
|
res.status(500).json({
|
|
@@ -11451,11 +11675,11 @@ function createActionRouteHandlers(ctx) {
|
|
|
11451
11675
|
res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
|
|
11452
11676
|
return;
|
|
11453
11677
|
}
|
|
11454
|
-
if (
|
|
11455
|
-
|
|
11678
|
+
if (fs27.existsSync(lockPath)) {
|
|
11679
|
+
fs27.unlinkSync(lockPath);
|
|
11456
11680
|
}
|
|
11457
|
-
const prdDir =
|
|
11458
|
-
if (
|
|
11681
|
+
const prdDir = path28.join(projectDir, config.prdDir);
|
|
11682
|
+
if (fs27.existsSync(prdDir)) {
|
|
11459
11683
|
cleanOrphanedClaims(prdDir);
|
|
11460
11684
|
}
|
|
11461
11685
|
broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
|
|
@@ -11841,6 +12065,9 @@ function validateConfigChanges(changes) {
|
|
|
11841
12065
|
if (changes.cronScheduleOffset !== void 0 && (typeof changes.cronScheduleOffset !== "number" || changes.cronScheduleOffset < 0 || changes.cronScheduleOffset > 59)) {
|
|
11842
12066
|
return "cronScheduleOffset must be a number between 0 and 59";
|
|
11843
12067
|
}
|
|
12068
|
+
if (changes.schedulingPriority !== void 0 && (typeof changes.schedulingPriority !== "number" || !Number.isInteger(changes.schedulingPriority) || changes.schedulingPriority < 1 || changes.schedulingPriority > 5)) {
|
|
12069
|
+
return "schedulingPriority must be an integer between 1 and 5";
|
|
12070
|
+
}
|
|
11844
12071
|
if (changes.fallbackOnRateLimit !== void 0 && typeof changes.fallbackOnRateLimit !== "boolean") {
|
|
11845
12072
|
return "fallbackOnRateLimit must be a boolean";
|
|
11846
12073
|
}
|
|
@@ -11896,6 +12123,35 @@ function validateConfigChanges(changes) {
|
|
|
11896
12123
|
return "audit.maxRuntime must be a number >= 60";
|
|
11897
12124
|
}
|
|
11898
12125
|
}
|
|
12126
|
+
if (changes.queue !== void 0) {
|
|
12127
|
+
if (typeof changes.queue !== "object" || changes.queue === null) {
|
|
12128
|
+
return "queue must be an object";
|
|
12129
|
+
}
|
|
12130
|
+
const queue = changes.queue;
|
|
12131
|
+
if (queue.enabled !== void 0 && typeof queue.enabled !== "boolean") {
|
|
12132
|
+
return "queue.enabled must be a boolean";
|
|
12133
|
+
}
|
|
12134
|
+
if (queue.maxWaitTime !== void 0 && (typeof queue.maxWaitTime !== "number" || queue.maxWaitTime < 300 || queue.maxWaitTime > 14400)) {
|
|
12135
|
+
return "queue.maxWaitTime must be a number between 300 and 14400";
|
|
12136
|
+
}
|
|
12137
|
+
if (queue.maxConcurrency !== void 0 && queue.maxConcurrency !== 1) {
|
|
12138
|
+
return "queue.maxConcurrency is currently fixed at 1";
|
|
12139
|
+
}
|
|
12140
|
+
if (queue.priority !== void 0) {
|
|
12141
|
+
if (typeof queue.priority !== "object" || queue.priority === null) {
|
|
12142
|
+
return "queue.priority must be an object";
|
|
12143
|
+
}
|
|
12144
|
+
const validQueueJobs = ["executor", "reviewer", "qa", "audit", "slicer"];
|
|
12145
|
+
for (const [jobType, value] of Object.entries(queue.priority)) {
|
|
12146
|
+
if (!validQueueJobs.includes(jobType)) {
|
|
12147
|
+
return `queue.priority contains invalid job type: ${jobType}`;
|
|
12148
|
+
}
|
|
12149
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
12150
|
+
return `queue.priority.${jobType} must be a number`;
|
|
12151
|
+
}
|
|
12152
|
+
}
|
|
12153
|
+
}
|
|
12154
|
+
}
|
|
11899
12155
|
if (changes.boardProvider !== void 0) {
|
|
11900
12156
|
if (typeof changes.boardProvider !== "object" || changes.boardProvider === null) {
|
|
11901
12157
|
return "boardProvider must be an object";
|
|
@@ -12003,7 +12259,7 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12003
12259
|
});
|
|
12004
12260
|
}
|
|
12005
12261
|
try {
|
|
12006
|
-
const projectName =
|
|
12262
|
+
const projectName = path29.basename(projectDir);
|
|
12007
12263
|
const marker = generateMarker(projectName);
|
|
12008
12264
|
const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
|
|
12009
12265
|
if (crontabEntries.length > 0) {
|
|
@@ -12026,8 +12282,8 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12026
12282
|
detail: "Failed to check crontab"
|
|
12027
12283
|
});
|
|
12028
12284
|
}
|
|
12029
|
-
const configPath =
|
|
12030
|
-
if (
|
|
12285
|
+
const configPath = path29.join(projectDir, CONFIG_FILE_NAME);
|
|
12286
|
+
if (fs28.existsSync(configPath)) {
|
|
12031
12287
|
checks.push({ name: "config", status: "pass", detail: "Config file exists" });
|
|
12032
12288
|
} else {
|
|
12033
12289
|
checks.push({
|
|
@@ -12036,9 +12292,9 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12036
12292
|
detail: "Config file not found (using defaults)"
|
|
12037
12293
|
});
|
|
12038
12294
|
}
|
|
12039
|
-
const prdDir =
|
|
12040
|
-
if (
|
|
12041
|
-
const prds =
|
|
12295
|
+
const prdDir = path29.join(projectDir, config.prdDir);
|
|
12296
|
+
if (fs28.existsSync(prdDir)) {
|
|
12297
|
+
const prds = fs28.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
|
|
12042
12298
|
checks.push({
|
|
12043
12299
|
name: "prdDir",
|
|
12044
12300
|
status: "pass",
|
|
@@ -12096,7 +12352,7 @@ function createLogRoutes(deps) {
|
|
|
12096
12352
|
const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
|
|
12097
12353
|
const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
|
|
12098
12354
|
const fileName = LOG_FILE_NAMES[name] || name;
|
|
12099
|
-
const logPath =
|
|
12355
|
+
const logPath = path30.join(projectDir, LOG_DIR, `${fileName}.log`);
|
|
12100
12356
|
const logLines = getLastLogLines(logPath, linesToRead);
|
|
12101
12357
|
res.json({ name, lines: logLines });
|
|
12102
12358
|
} catch (error2) {
|
|
@@ -12122,7 +12378,7 @@ function createProjectLogRoutes() {
|
|
|
12122
12378
|
const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
|
|
12123
12379
|
const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
|
|
12124
12380
|
const fileName = LOG_FILE_NAMES[name] || name;
|
|
12125
|
-
const logPath =
|
|
12381
|
+
const logPath = path30.join(projectDir, LOG_DIR, `${fileName}.log`);
|
|
12126
12382
|
const logLines = getLastLogLines(logPath, linesToRead);
|
|
12127
12383
|
res.json({ name, lines: logLines });
|
|
12128
12384
|
} catch (error2) {
|
|
@@ -12160,7 +12416,7 @@ function createRoadmapRouteHandlers(ctx) {
|
|
|
12160
12416
|
const config = ctx.getConfig(req);
|
|
12161
12417
|
const projectDir = ctx.getProjectDir(req);
|
|
12162
12418
|
const status = getRoadmapStatus(projectDir, config);
|
|
12163
|
-
const prdDir =
|
|
12419
|
+
const prdDir = path31.join(projectDir, config.prdDir);
|
|
12164
12420
|
const state = loadRoadmapState(prdDir);
|
|
12165
12421
|
res.json({
|
|
12166
12422
|
...status,
|
|
@@ -12262,17 +12518,6 @@ data: ${JSON.stringify(snapshot)}
|
|
|
12262
12518
|
});
|
|
12263
12519
|
return router;
|
|
12264
12520
|
}
|
|
12265
|
-
function applyScheduleOffset2(schedule, offset) {
|
|
12266
|
-
if (offset === 0) {
|
|
12267
|
-
return schedule;
|
|
12268
|
-
}
|
|
12269
|
-
const parts = schedule.trim().split(/\s+/);
|
|
12270
|
-
if (parts.length < 5 || !/^\d+$/.test(parts[0])) {
|
|
12271
|
-
return schedule.trim();
|
|
12272
|
-
}
|
|
12273
|
-
parts[0] = String(offset);
|
|
12274
|
-
return parts.join(" ");
|
|
12275
|
-
}
|
|
12276
12521
|
function computeNextRun(cronExpr) {
|
|
12277
12522
|
try {
|
|
12278
12523
|
const interval = CronExpressionParser2.parse(cronExpr);
|
|
@@ -12285,13 +12530,12 @@ function hasScheduledCommand(entries, command) {
|
|
|
12285
12530
|
const commandPattern = new RegExp(`\\s${command}\\s+>>`);
|
|
12286
12531
|
return entries.some((entry) => commandPattern.test(entry));
|
|
12287
12532
|
}
|
|
12288
|
-
function buildScheduleInfoResponse(config, entries, installed) {
|
|
12289
|
-
const
|
|
12290
|
-
const
|
|
12291
|
-
const
|
|
12292
|
-
const
|
|
12293
|
-
const
|
|
12294
|
-
const plannerSchedule = applyScheduleOffset2(config.roadmapScanner.slicerSchedule, offset);
|
|
12533
|
+
function buildScheduleInfoResponse(projectDir, config, entries, installed) {
|
|
12534
|
+
const executorPlan = getSchedulingPlan(projectDir, config, "executor");
|
|
12535
|
+
const reviewerPlan = getSchedulingPlan(projectDir, config, "reviewer");
|
|
12536
|
+
const qaPlan = getSchedulingPlan(projectDir, config, "qa");
|
|
12537
|
+
const auditPlan = getSchedulingPlan(projectDir, config, "audit");
|
|
12538
|
+
const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
|
|
12295
12539
|
const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
|
|
12296
12540
|
const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
|
|
12297
12541
|
const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
|
|
@@ -12299,31 +12543,47 @@ function buildScheduleInfoResponse(config, entries, installed) {
|
|
|
12299
12543
|
const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
|
|
12300
12544
|
return {
|
|
12301
12545
|
executor: {
|
|
12302
|
-
schedule:
|
|
12546
|
+
schedule: config.cronSchedule,
|
|
12303
12547
|
installed: executorInstalled,
|
|
12304
|
-
nextRun: executorInstalled ? computeNextRun(
|
|
12548
|
+
nextRun: executorInstalled ? addDelayToIsoString(computeNextRun(config.cronSchedule), executorPlan.totalDelayMinutes) : null,
|
|
12549
|
+
delayMinutes: executorPlan.totalDelayMinutes,
|
|
12550
|
+
manualDelayMinutes: executorPlan.manualDelayMinutes,
|
|
12551
|
+
balancedDelayMinutes: executorPlan.balancedDelayMinutes
|
|
12305
12552
|
},
|
|
12306
12553
|
reviewer: {
|
|
12307
|
-
schedule: reviewerSchedule,
|
|
12554
|
+
schedule: config.reviewerSchedule,
|
|
12308
12555
|
installed: reviewerInstalled,
|
|
12309
|
-
nextRun: reviewerInstalled ? computeNextRun(reviewerSchedule) : null
|
|
12556
|
+
nextRun: reviewerInstalled ? addDelayToIsoString(computeNextRun(config.reviewerSchedule), reviewerPlan.totalDelayMinutes) : null,
|
|
12557
|
+
delayMinutes: reviewerPlan.totalDelayMinutes,
|
|
12558
|
+
manualDelayMinutes: reviewerPlan.manualDelayMinutes,
|
|
12559
|
+
balancedDelayMinutes: reviewerPlan.balancedDelayMinutes
|
|
12310
12560
|
},
|
|
12311
12561
|
qa: {
|
|
12312
|
-
schedule:
|
|
12562
|
+
schedule: config.qa.schedule,
|
|
12313
12563
|
installed: qaInstalled,
|
|
12314
|
-
nextRun: qaInstalled ? computeNextRun(
|
|
12564
|
+
nextRun: qaInstalled ? addDelayToIsoString(computeNextRun(config.qa.schedule), qaPlan.totalDelayMinutes) : null,
|
|
12565
|
+
delayMinutes: qaPlan.totalDelayMinutes,
|
|
12566
|
+
manualDelayMinutes: qaPlan.manualDelayMinutes,
|
|
12567
|
+
balancedDelayMinutes: qaPlan.balancedDelayMinutes
|
|
12315
12568
|
},
|
|
12316
12569
|
audit: {
|
|
12317
|
-
schedule:
|
|
12570
|
+
schedule: config.audit.schedule,
|
|
12318
12571
|
installed: auditInstalled,
|
|
12319
|
-
nextRun: auditInstalled ? computeNextRun(
|
|
12572
|
+
nextRun: auditInstalled ? addDelayToIsoString(computeNextRun(config.audit.schedule), auditPlan.totalDelayMinutes) : null,
|
|
12573
|
+
delayMinutes: auditPlan.totalDelayMinutes,
|
|
12574
|
+
manualDelayMinutes: auditPlan.manualDelayMinutes,
|
|
12575
|
+
balancedDelayMinutes: auditPlan.balancedDelayMinutes
|
|
12320
12576
|
},
|
|
12321
12577
|
planner: {
|
|
12322
|
-
schedule:
|
|
12578
|
+
schedule: config.roadmapScanner.slicerSchedule,
|
|
12323
12579
|
installed: plannerInstalled,
|
|
12324
|
-
nextRun: plannerInstalled ? computeNextRun(
|
|
12580
|
+
nextRun: plannerInstalled ? addDelayToIsoString(computeNextRun(config.roadmapScanner.slicerSchedule), plannerPlan.totalDelayMinutes) : null,
|
|
12581
|
+
delayMinutes: plannerPlan.totalDelayMinutes,
|
|
12582
|
+
manualDelayMinutes: plannerPlan.manualDelayMinutes,
|
|
12583
|
+
balancedDelayMinutes: plannerPlan.balancedDelayMinutes
|
|
12325
12584
|
},
|
|
12326
12585
|
paused: !installed,
|
|
12586
|
+
schedulingPriority: config.schedulingPriority,
|
|
12327
12587
|
entries
|
|
12328
12588
|
};
|
|
12329
12589
|
}
|
|
@@ -12334,7 +12594,7 @@ function createScheduleInfoRoutes(deps) {
|
|
|
12334
12594
|
try {
|
|
12335
12595
|
const config = getConfig();
|
|
12336
12596
|
const snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
12337
|
-
res.json(buildScheduleInfoResponse(config, snapshot.crontab.entries, snapshot.crontab.installed));
|
|
12597
|
+
res.json(buildScheduleInfoResponse(projectDir, config, snapshot.crontab.entries, snapshot.crontab.installed));
|
|
12338
12598
|
} catch (error2) {
|
|
12339
12599
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12340
12600
|
}
|
|
@@ -12390,7 +12650,7 @@ data: ${JSON.stringify(snapshot)}
|
|
|
12390
12650
|
const config = req.projectConfig;
|
|
12391
12651
|
const projectDir = req.projectDir;
|
|
12392
12652
|
const snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
12393
|
-
res.json(buildScheduleInfoResponse(config, snapshot.crontab.entries, snapshot.crontab.installed));
|
|
12653
|
+
res.json(buildScheduleInfoResponse(projectDir, config, snapshot.crontab.entries, snapshot.crontab.installed));
|
|
12394
12654
|
} catch (error2) {
|
|
12395
12655
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12396
12656
|
}
|
|
@@ -12425,14 +12685,14 @@ function createQueueRoutes(deps) {
|
|
|
12425
12685
|
var __filename2 = fileURLToPath3(import.meta.url);
|
|
12426
12686
|
var __dirname3 = dirname7(__filename2);
|
|
12427
12687
|
function resolveWebDistPath() {
|
|
12428
|
-
const bundled =
|
|
12429
|
-
if (
|
|
12688
|
+
const bundled = path32.join(__dirname3, "web");
|
|
12689
|
+
if (fs29.existsSync(path32.join(bundled, "index.html")))
|
|
12430
12690
|
return bundled;
|
|
12431
12691
|
let d = __dirname3;
|
|
12432
12692
|
for (let i = 0; i < 8; i++) {
|
|
12433
|
-
if (
|
|
12434
|
-
const dev =
|
|
12435
|
-
if (
|
|
12693
|
+
if (fs29.existsSync(path32.join(d, "turbo.json"))) {
|
|
12694
|
+
const dev = path32.join(d, "web/dist");
|
|
12695
|
+
if (fs29.existsSync(path32.join(dev, "index.html")))
|
|
12436
12696
|
return dev;
|
|
12437
12697
|
break;
|
|
12438
12698
|
}
|
|
@@ -12442,7 +12702,7 @@ function resolveWebDistPath() {
|
|
|
12442
12702
|
}
|
|
12443
12703
|
function setupStaticFiles(app) {
|
|
12444
12704
|
const webDistPath = resolveWebDistPath();
|
|
12445
|
-
if (
|
|
12705
|
+
if (fs29.existsSync(webDistPath)) {
|
|
12446
12706
|
app.use(express.static(webDistPath));
|
|
12447
12707
|
}
|
|
12448
12708
|
app.use((req, res, next) => {
|
|
@@ -12450,8 +12710,8 @@ function setupStaticFiles(app) {
|
|
|
12450
12710
|
next();
|
|
12451
12711
|
return;
|
|
12452
12712
|
}
|
|
12453
|
-
const indexPath =
|
|
12454
|
-
if (
|
|
12713
|
+
const indexPath = path32.resolve(webDistPath, "index.html");
|
|
12714
|
+
if (fs29.existsSync(indexPath)) {
|
|
12455
12715
|
res.sendFile(indexPath, (err) => {
|
|
12456
12716
|
if (err)
|
|
12457
12717
|
next();
|
|
@@ -12560,7 +12820,7 @@ function createGlobalApp() {
|
|
|
12560
12820
|
return app;
|
|
12561
12821
|
}
|
|
12562
12822
|
function bootContainer() {
|
|
12563
|
-
initContainer(
|
|
12823
|
+
initContainer(path32.dirname(getDbPath()));
|
|
12564
12824
|
}
|
|
12565
12825
|
function startServer(projectDir, port) {
|
|
12566
12826
|
bootContainer();
|
|
@@ -12611,9 +12871,9 @@ function isProcessRunning2(pid) {
|
|
|
12611
12871
|
}
|
|
12612
12872
|
function readPid(lockPath) {
|
|
12613
12873
|
try {
|
|
12614
|
-
if (!
|
|
12874
|
+
if (!fs30.existsSync(lockPath))
|
|
12615
12875
|
return null;
|
|
12616
|
-
const raw =
|
|
12876
|
+
const raw = fs30.readFileSync(lockPath, "utf-8").trim();
|
|
12617
12877
|
const pid = parseInt(raw, 10);
|
|
12618
12878
|
return Number.isFinite(pid) ? pid : null;
|
|
12619
12879
|
} catch {
|
|
@@ -12625,10 +12885,10 @@ function acquireServeLock(mode, port) {
|
|
|
12625
12885
|
let stalePidCleaned;
|
|
12626
12886
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
12627
12887
|
try {
|
|
12628
|
-
const fd =
|
|
12629
|
-
|
|
12888
|
+
const fd = fs30.openSync(lockPath, "wx");
|
|
12889
|
+
fs30.writeFileSync(fd, `${process.pid}
|
|
12630
12890
|
`);
|
|
12631
|
-
|
|
12891
|
+
fs30.closeSync(fd);
|
|
12632
12892
|
return { acquired: true, lockPath, stalePidCleaned };
|
|
12633
12893
|
} catch (error2) {
|
|
12634
12894
|
const err = error2;
|
|
@@ -12649,7 +12909,7 @@ function acquireServeLock(mode, port) {
|
|
|
12649
12909
|
};
|
|
12650
12910
|
}
|
|
12651
12911
|
try {
|
|
12652
|
-
|
|
12912
|
+
fs30.unlinkSync(lockPath);
|
|
12653
12913
|
if (existingPid) {
|
|
12654
12914
|
stalePidCleaned = existingPid;
|
|
12655
12915
|
}
|
|
@@ -12672,12 +12932,12 @@ function acquireServeLock(mode, port) {
|
|
|
12672
12932
|
}
|
|
12673
12933
|
function releaseServeLock(lockPath) {
|
|
12674
12934
|
try {
|
|
12675
|
-
if (!
|
|
12935
|
+
if (!fs30.existsSync(lockPath))
|
|
12676
12936
|
return;
|
|
12677
12937
|
const lockPid = readPid(lockPath);
|
|
12678
12938
|
if (lockPid !== null && lockPid !== process.pid)
|
|
12679
12939
|
return;
|
|
12680
|
-
|
|
12940
|
+
fs30.unlinkSync(lockPath);
|
|
12681
12941
|
} catch {
|
|
12682
12942
|
}
|
|
12683
12943
|
}
|
|
@@ -12763,7 +13023,7 @@ function parseProjectDirs(projects, cwd) {
|
|
|
12763
13023
|
if (!projects || projects.trim().length === 0) {
|
|
12764
13024
|
return [cwd];
|
|
12765
13025
|
}
|
|
12766
|
-
const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) =>
|
|
13026
|
+
const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path33.resolve(cwd, entry));
|
|
12767
13027
|
return Array.from(new Set(dirs));
|
|
12768
13028
|
}
|
|
12769
13029
|
function shouldInstallGlobal(options) {
|
|
@@ -12805,7 +13065,7 @@ function updateCommand(program2) {
|
|
|
12805
13065
|
}
|
|
12806
13066
|
const nightWatchBin = resolveNightWatchBin();
|
|
12807
13067
|
for (const projectDir of projectDirs) {
|
|
12808
|
-
if (!
|
|
13068
|
+
if (!fs31.existsSync(projectDir) || !fs31.statSync(projectDir).isDirectory()) {
|
|
12809
13069
|
warn(`Skipping invalid project directory: ${projectDir}`);
|
|
12810
13070
|
continue;
|
|
12811
13071
|
}
|
|
@@ -12852,26 +13112,26 @@ function normalizePrdName(name) {
|
|
|
12852
13112
|
return name;
|
|
12853
13113
|
}
|
|
12854
13114
|
function getDonePrds(doneDir) {
|
|
12855
|
-
if (!
|
|
13115
|
+
if (!fs32.existsSync(doneDir)) {
|
|
12856
13116
|
return [];
|
|
12857
13117
|
}
|
|
12858
|
-
return
|
|
13118
|
+
return fs32.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
|
|
12859
13119
|
}
|
|
12860
13120
|
function retryCommand(program2) {
|
|
12861
13121
|
program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
|
|
12862
13122
|
const projectDir = process.cwd();
|
|
12863
13123
|
const config = loadConfig(projectDir);
|
|
12864
|
-
const prdDir =
|
|
12865
|
-
const doneDir =
|
|
13124
|
+
const prdDir = path34.join(projectDir, config.prdDir);
|
|
13125
|
+
const doneDir = path34.join(prdDir, "done");
|
|
12866
13126
|
const normalizedPrdName = normalizePrdName(prdName);
|
|
12867
|
-
const pendingPath =
|
|
12868
|
-
if (
|
|
13127
|
+
const pendingPath = path34.join(prdDir, normalizedPrdName);
|
|
13128
|
+
if (fs32.existsSync(pendingPath)) {
|
|
12869
13129
|
info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
|
|
12870
13130
|
return;
|
|
12871
13131
|
}
|
|
12872
|
-
const donePath =
|
|
12873
|
-
if (
|
|
12874
|
-
|
|
13132
|
+
const donePath = path34.join(doneDir, normalizedPrdName);
|
|
13133
|
+
if (fs32.existsSync(donePath)) {
|
|
13134
|
+
fs32.renameSync(donePath, pendingPath);
|
|
12875
13135
|
success(`Moved "${normalizedPrdName}" back to pending.`);
|
|
12876
13136
|
dim(`From: ${donePath}`);
|
|
12877
13137
|
dim(`To: ${pendingPath}`);
|
|
@@ -13123,16 +13383,16 @@ async function promptConfirmation(prompt2) {
|
|
|
13123
13383
|
input: process.stdin,
|
|
13124
13384
|
output: process.stdout
|
|
13125
13385
|
});
|
|
13126
|
-
return new Promise((
|
|
13386
|
+
return new Promise((resolve10) => {
|
|
13127
13387
|
rl.question(`${prompt2} `, (answer) => {
|
|
13128
13388
|
rl.close();
|
|
13129
13389
|
const normalized = answer.toLowerCase().trim();
|
|
13130
|
-
|
|
13390
|
+
resolve10(normalized === "y" || normalized === "yes");
|
|
13131
13391
|
});
|
|
13132
13392
|
});
|
|
13133
13393
|
}
|
|
13134
13394
|
function sleep3(ms) {
|
|
13135
|
-
return new Promise((
|
|
13395
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
13136
13396
|
}
|
|
13137
13397
|
function isProcessRunning3(pid) {
|
|
13138
13398
|
try {
|
|
@@ -13153,7 +13413,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
13153
13413
|
const pid = lockStatus.pid;
|
|
13154
13414
|
if (!lockStatus.running) {
|
|
13155
13415
|
try {
|
|
13156
|
-
|
|
13416
|
+
fs33.unlinkSync(lockPath);
|
|
13157
13417
|
return {
|
|
13158
13418
|
success: true,
|
|
13159
13419
|
message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
|
|
@@ -13191,7 +13451,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
13191
13451
|
await sleep3(3e3);
|
|
13192
13452
|
if (!isProcessRunning3(pid)) {
|
|
13193
13453
|
try {
|
|
13194
|
-
|
|
13454
|
+
fs33.unlinkSync(lockPath);
|
|
13195
13455
|
} catch {
|
|
13196
13456
|
}
|
|
13197
13457
|
return {
|
|
@@ -13226,7 +13486,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
13226
13486
|
await sleep3(500);
|
|
13227
13487
|
if (!isProcessRunning3(pid)) {
|
|
13228
13488
|
try {
|
|
13229
|
-
|
|
13489
|
+
fs33.unlinkSync(lockPath);
|
|
13230
13490
|
} catch {
|
|
13231
13491
|
}
|
|
13232
13492
|
return {
|
|
@@ -13290,24 +13550,24 @@ function plannerLockPath2(projectDir) {
|
|
|
13290
13550
|
}
|
|
13291
13551
|
function acquirePlannerLock(projectDir) {
|
|
13292
13552
|
const lockFile = plannerLockPath2(projectDir);
|
|
13293
|
-
if (
|
|
13294
|
-
const pidRaw =
|
|
13553
|
+
if (fs34.existsSync(lockFile)) {
|
|
13554
|
+
const pidRaw = fs34.readFileSync(lockFile, "utf-8").trim();
|
|
13295
13555
|
const pid = parseInt(pidRaw, 10);
|
|
13296
13556
|
if (!Number.isNaN(pid) && isProcessRunning(pid)) {
|
|
13297
13557
|
return { acquired: false, lockFile, pid };
|
|
13298
13558
|
}
|
|
13299
13559
|
try {
|
|
13300
|
-
|
|
13560
|
+
fs34.unlinkSync(lockFile);
|
|
13301
13561
|
} catch {
|
|
13302
13562
|
}
|
|
13303
13563
|
}
|
|
13304
|
-
|
|
13564
|
+
fs34.writeFileSync(lockFile, String(process.pid));
|
|
13305
13565
|
return { acquired: true, lockFile };
|
|
13306
13566
|
}
|
|
13307
13567
|
function releasePlannerLock(lockFile) {
|
|
13308
13568
|
try {
|
|
13309
|
-
if (
|
|
13310
|
-
|
|
13569
|
+
if (fs34.existsSync(lockFile)) {
|
|
13570
|
+
fs34.unlinkSync(lockFile);
|
|
13311
13571
|
}
|
|
13312
13572
|
} catch {
|
|
13313
13573
|
}
|
|
@@ -13316,12 +13576,12 @@ function resolvePlannerIssueColumn(config) {
|
|
|
13316
13576
|
return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
|
|
13317
13577
|
}
|
|
13318
13578
|
function buildPlannerIssueBody(projectDir, config, result) {
|
|
13319
|
-
const relativePrdPath =
|
|
13320
|
-
const absolutePrdPath =
|
|
13579
|
+
const relativePrdPath = path35.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
|
|
13580
|
+
const absolutePrdPath = path35.join(projectDir, config.prdDir, result.file ?? "");
|
|
13321
13581
|
const sourceItem = result.item;
|
|
13322
13582
|
let prdContent = "";
|
|
13323
13583
|
try {
|
|
13324
|
-
prdContent =
|
|
13584
|
+
prdContent = fs34.readFileSync(absolutePrdPath, "utf-8");
|
|
13325
13585
|
} catch {
|
|
13326
13586
|
prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
|
|
13327
13587
|
}
|
|
@@ -13489,10 +13749,11 @@ function sliceCommand(program2) {
|
|
|
13489
13749
|
const spinner = createSpinner("Running Planner...");
|
|
13490
13750
|
spinner.start();
|
|
13491
13751
|
try {
|
|
13752
|
+
await maybeApplyCronSchedulingDelay(config, "slicer", projectDir);
|
|
13492
13753
|
if (!options.dryRun) {
|
|
13493
13754
|
await sendNotifications(config, {
|
|
13494
13755
|
event: "run_started",
|
|
13495
|
-
projectName:
|
|
13756
|
+
projectName: path35.basename(projectDir),
|
|
13496
13757
|
exitCode: 0,
|
|
13497
13758
|
provider: config.provider
|
|
13498
13759
|
});
|
|
@@ -13525,7 +13786,7 @@ function sliceCommand(program2) {
|
|
|
13525
13786
|
if (!options.dryRun && result.sliced) {
|
|
13526
13787
|
await sendNotifications(config, {
|
|
13527
13788
|
event: "run_succeeded",
|
|
13528
|
-
projectName:
|
|
13789
|
+
projectName: path35.basename(projectDir),
|
|
13529
13790
|
exitCode,
|
|
13530
13791
|
provider: config.provider,
|
|
13531
13792
|
prTitle: result.item?.title
|
|
@@ -13533,7 +13794,7 @@ function sliceCommand(program2) {
|
|
|
13533
13794
|
} else if (!options.dryRun && !nothingPending) {
|
|
13534
13795
|
await sendNotifications(config, {
|
|
13535
13796
|
event: "run_failed",
|
|
13536
|
-
projectName:
|
|
13797
|
+
projectName: path35.basename(projectDir),
|
|
13537
13798
|
exitCode,
|
|
13538
13799
|
provider: config.provider
|
|
13539
13800
|
});
|
|
@@ -13551,13 +13812,13 @@ function createStateCommand() {
|
|
|
13551
13812
|
const state = new Command("state");
|
|
13552
13813
|
state.description("Manage Night Watch state");
|
|
13553
13814
|
state.command("migrate").description("Migrate legacy JSON state files to SQLite").option("--dry-run", "Show what would be migrated without making changes").action((opts) => {
|
|
13554
|
-
const nightWatchHome = process.env.NIGHT_WATCH_HOME ||
|
|
13815
|
+
const nightWatchHome = process.env.NIGHT_WATCH_HOME || path36.join(os6.homedir(), GLOBAL_CONFIG_DIR);
|
|
13555
13816
|
if (opts.dryRun) {
|
|
13556
13817
|
console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
|
|
13557
13818
|
console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
|
|
13558
|
-
console.log(` ${
|
|
13559
|
-
console.log(` ${
|
|
13560
|
-
console.log(` ${
|
|
13819
|
+
console.log(` ${path36.join(nightWatchHome, "projects.json")}`);
|
|
13820
|
+
console.log(` ${path36.join(nightWatchHome, "history.json")}`);
|
|
13821
|
+
console.log(` ${path36.join(nightWatchHome, "prd-states.json")}`);
|
|
13561
13822
|
console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
|
|
13562
13823
|
console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
|
|
13563
13824
|
return;
|
|
@@ -13605,7 +13866,7 @@ function getProvider(config, cwd) {
|
|
|
13605
13866
|
return createBoardProvider(bp, cwd);
|
|
13606
13867
|
}
|
|
13607
13868
|
function defaultBoardTitle(cwd) {
|
|
13608
|
-
return `${
|
|
13869
|
+
return `${path37.basename(cwd)} Night Watch`;
|
|
13609
13870
|
}
|
|
13610
13871
|
async function ensureBoardConfigured(config, cwd, provider, options) {
|
|
13611
13872
|
if (config.boardProvider?.projectNumber) {
|
|
@@ -13636,10 +13897,10 @@ async function confirmPrompt(question) {
|
|
|
13636
13897
|
input: process.stdin,
|
|
13637
13898
|
output: process.stdout
|
|
13638
13899
|
});
|
|
13639
|
-
return new Promise((
|
|
13900
|
+
return new Promise((resolve10) => {
|
|
13640
13901
|
rl.question(question, (answer) => {
|
|
13641
13902
|
rl.close();
|
|
13642
|
-
|
|
13903
|
+
resolve10(answer.trim().toLowerCase() === "y");
|
|
13643
13904
|
});
|
|
13644
13905
|
});
|
|
13645
13906
|
}
|
|
@@ -13785,11 +14046,11 @@ function boardCommand(program2) {
|
|
|
13785
14046
|
let body = options.body ?? "";
|
|
13786
14047
|
if (options.bodyFile) {
|
|
13787
14048
|
const filePath = options.bodyFile;
|
|
13788
|
-
if (!
|
|
14049
|
+
if (!fs35.existsSync(filePath)) {
|
|
13789
14050
|
console.error(`File not found: ${filePath}`);
|
|
13790
14051
|
process.exit(1);
|
|
13791
14052
|
}
|
|
13792
|
-
body =
|
|
14053
|
+
body = fs35.readFileSync(filePath, "utf-8");
|
|
13793
14054
|
}
|
|
13794
14055
|
const labels = [];
|
|
13795
14056
|
if (options.label) {
|
|
@@ -13997,12 +14258,12 @@ function boardCommand(program2) {
|
|
|
13997
14258
|
const config = loadConfig(cwd);
|
|
13998
14259
|
const provider = getProvider(config, cwd);
|
|
13999
14260
|
await ensureBoardConfigured(config, cwd, provider);
|
|
14000
|
-
const roadmapPath = options.roadmap ??
|
|
14001
|
-
if (!
|
|
14261
|
+
const roadmapPath = options.roadmap ?? path37.join(cwd, "ROADMAP.md");
|
|
14262
|
+
if (!fs35.existsSync(roadmapPath)) {
|
|
14002
14263
|
console.error(`Roadmap file not found: ${roadmapPath}`);
|
|
14003
14264
|
process.exit(1);
|
|
14004
14265
|
}
|
|
14005
|
-
const roadmapContent =
|
|
14266
|
+
const roadmapContent = fs35.readFileSync(roadmapPath, "utf-8");
|
|
14006
14267
|
const items = parseRoadmap(roadmapContent);
|
|
14007
14268
|
const uncheckedItems = getUncheckedItems(items);
|
|
14008
14269
|
if (uncheckedItems.length === 0) {
|
|
@@ -14233,8 +14494,9 @@ function createQueueCommand() {
|
|
|
14233
14494
|
process.exit(1);
|
|
14234
14495
|
}
|
|
14235
14496
|
}
|
|
14236
|
-
const projectName =
|
|
14237
|
-
const
|
|
14497
|
+
const projectName = path38.basename(projectDir);
|
|
14498
|
+
const queueConfig = loadConfig(projectDir).queue;
|
|
14499
|
+
const id = enqueueJob(projectDir, projectName, jobType, envVars, queueConfig);
|
|
14238
14500
|
console.log(chalk7.green(`Enqueued ${jobType} for ${projectName} (ID: ${id})`));
|
|
14239
14501
|
});
|
|
14240
14502
|
queue.command("dispatch").description("Dispatch the next pending job (used by cron scripts)").option("--log <file>", "Log file to write dispatch output").action((_opts) => {
|
|
@@ -14257,15 +14519,21 @@ function createQueueCommand() {
|
|
|
14257
14519
|
};
|
|
14258
14520
|
const scriptPath = getScriptPath(scriptName);
|
|
14259
14521
|
logger.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
|
|
14260
|
-
|
|
14261
|
-
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
|
|
14265
|
-
|
|
14266
|
-
|
|
14267
|
-
|
|
14268
|
-
|
|
14522
|
+
try {
|
|
14523
|
+
const child = spawn6("bash", [scriptPath, entry.projectPath], {
|
|
14524
|
+
detached: true,
|
|
14525
|
+
stdio: "ignore",
|
|
14526
|
+
env,
|
|
14527
|
+
cwd: entry.projectPath
|
|
14528
|
+
});
|
|
14529
|
+
child.unref();
|
|
14530
|
+
logger.info(`Spawned PID: ${child.pid}`);
|
|
14531
|
+
markJobRunning(entry.id);
|
|
14532
|
+
} catch (error2) {
|
|
14533
|
+
updateJobStatus(entry.id, "pending");
|
|
14534
|
+
logger.error(`Failed to dispatch ${entry.jobType} for ${entry.projectName}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
14535
|
+
process.exit(1);
|
|
14536
|
+
}
|
|
14269
14537
|
});
|
|
14270
14538
|
queue.command("complete <id>").description("Remove a completed queue entry (used by cron scripts)").action((id) => {
|
|
14271
14539
|
const queueId = parseInt(id, 10);
|
|
@@ -14275,6 +14543,10 @@ function createQueueCommand() {
|
|
|
14275
14543
|
}
|
|
14276
14544
|
removeJob(queueId);
|
|
14277
14545
|
});
|
|
14546
|
+
queue.command("can-start").description("Return a zero exit status when the global queue has an available slot").action(() => {
|
|
14547
|
+
const queueConfig = loadConfig(process.cwd()).queue;
|
|
14548
|
+
process.exit(canStartJob(queueConfig) ? 0 : 1);
|
|
14549
|
+
});
|
|
14278
14550
|
queue.command("expire").description("Expire stale queued jobs").option("--max-wait <seconds>", "Maximum wait time in seconds", String(DEFAULT_QUEUE_MAX_WAIT_TIME)).action((opts) => {
|
|
14279
14551
|
const maxWait = parseInt(opts.maxWait, 10);
|
|
14280
14552
|
if (isNaN(maxWait) || maxWait < 60) {
|
|
@@ -14314,14 +14586,14 @@ var __dirname4 = dirname8(__filename3);
|
|
|
14314
14586
|
function findPackageRoot(dir) {
|
|
14315
14587
|
let d = dir;
|
|
14316
14588
|
for (let i = 0; i < 5; i++) {
|
|
14317
|
-
if (
|
|
14589
|
+
if (existsSync29(join34(d, "package.json")))
|
|
14318
14590
|
return d;
|
|
14319
14591
|
d = dirname8(d);
|
|
14320
14592
|
}
|
|
14321
14593
|
return dir;
|
|
14322
14594
|
}
|
|
14323
14595
|
var packageRoot = findPackageRoot(__dirname4);
|
|
14324
|
-
var packageJson = JSON.parse(readFileSync17(
|
|
14596
|
+
var packageJson = JSON.parse(readFileSync17(join34(packageRoot, "package.json"), "utf-8"));
|
|
14325
14597
|
var program = new Command3();
|
|
14326
14598
|
program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
|
|
14327
14599
|
initCommand(program);
|