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