@jonit-dev/night-watch-cli 1.7.46 → 1.7.48
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 +1097 -705
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +21 -1
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -37
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +20 -12
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +37 -5
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/prd.d.ts.map +1 -1
- package/dist/commands/prd.js +2 -6
- package/dist/commands/prd.js.map +1 -1
- package/dist/commands/prds.d.ts.map +1 -1
- package/dist/commands/prds.js +1 -1
- package/dist/commands/prds.js.map +1 -1
- package/dist/commands/qa.d.ts.map +1 -1
- package/dist/commands/qa.js +20 -0
- package/dist/commands/qa.js.map +1 -1
- package/dist/commands/review.d.ts +10 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +38 -0
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +7 -2
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/slice.d.ts.map +1 -1
- package/dist/commands/slice.js +88 -17
- package/dist/commands/slice.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +75 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/scripts/night-watch-audit-cron.sh +19 -0
- package/dist/scripts/night-watch-cron.sh +2 -2
- package/dist/scripts/night-watch-helpers.sh +47 -1
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +168 -35
- package/dist/scripts/night-watch-qa-cron.sh +19 -0
- package/dist/scripts/night-watch-slicer-cron.sh +10 -1
- package/dist/templates/night-watch-slicer.md +18 -3
- package/dist/templates/night-watch.config.json +1 -0
- package/dist/templates/night-watch.md +10 -37
- package/dist/web/assets/index-Ba-4YvTQ.js +365 -0
- package/dist/web/assets/index-DpVirMEe.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,13 +5,6 @@ import 'reflect-metadata';
|
|
|
5
5
|
import "reflect-metadata";
|
|
6
6
|
import "reflect-metadata";
|
|
7
7
|
import "reflect-metadata";
|
|
8
|
-
import "reflect-metadata";
|
|
9
|
-
import "reflect-metadata";
|
|
10
|
-
import "reflect-metadata";
|
|
11
|
-
import "reflect-metadata";
|
|
12
|
-
import "reflect-metadata";
|
|
13
|
-
import "reflect-metadata";
|
|
14
|
-
import "reflect-metadata";
|
|
15
8
|
import * as fs from "fs";
|
|
16
9
|
import * as path from "path";
|
|
17
10
|
import { fileURLToPath } from "url";
|
|
@@ -34,8 +27,10 @@ import * as path2 from "path";
|
|
|
34
27
|
import Database7 from "better-sqlite3";
|
|
35
28
|
import "reflect-metadata";
|
|
36
29
|
import { container } from "tsyringe";
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
30
|
+
import { execFile } from "child_process";
|
|
31
|
+
import { promisify } from "util";
|
|
32
|
+
import { execFile as execFile2 } from "child_process";
|
|
33
|
+
import { promisify as promisify2 } from "util";
|
|
39
34
|
import * as fs3 from "fs";
|
|
40
35
|
import * as path3 from "path";
|
|
41
36
|
import { execSync } from "child_process";
|
|
@@ -43,18 +38,19 @@ import * as fs4 from "fs";
|
|
|
43
38
|
import * as os2 from "os";
|
|
44
39
|
import * as path4 from "path";
|
|
45
40
|
import { createHash } from "crypto";
|
|
46
|
-
import {
|
|
41
|
+
import { exec } from "child_process";
|
|
42
|
+
import { promisify as promisify3 } from "util";
|
|
47
43
|
import * as fs5 from "fs";
|
|
48
44
|
import * as path5 from "path";
|
|
49
45
|
import * as fs6 from "fs";
|
|
50
46
|
import * as fs7 from "fs";
|
|
51
47
|
import * as path6 from "path";
|
|
52
|
-
import { execSync as
|
|
48
|
+
import { execSync as execSync2 } from "child_process";
|
|
53
49
|
import * as fs8 from "fs";
|
|
54
50
|
import * as path7 from "path";
|
|
55
51
|
import * as os3 from "os";
|
|
56
52
|
import * as path8 from "path";
|
|
57
|
-
import { execFileSync
|
|
53
|
+
import { execFileSync } from "child_process";
|
|
58
54
|
import chalk from "chalk";
|
|
59
55
|
import ora from "ora";
|
|
60
56
|
import Table from "cli-table3";
|
|
@@ -70,26 +66,27 @@ import * as path11 from "path";
|
|
|
70
66
|
import * as fs13 from "fs";
|
|
71
67
|
import * as path12 from "path";
|
|
72
68
|
import { spawn } from "child_process";
|
|
69
|
+
import { createHash as createHash3 } from "crypto";
|
|
73
70
|
import { spawn as spawn2 } from "child_process";
|
|
74
71
|
import "reflect-metadata";
|
|
75
72
|
import { Command as Command2 } from "commander";
|
|
76
|
-
import { existsSync as
|
|
73
|
+
import { existsSync as existsSync26, readFileSync as readFileSync16 } from "fs";
|
|
77
74
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
78
75
|
import { dirname as dirname8, join as join30 } from "path";
|
|
79
76
|
import fs14 from "fs";
|
|
80
77
|
import path13 from "path";
|
|
81
|
-
import { execSync as
|
|
78
|
+
import { execSync as execSync3 } from "child_process";
|
|
82
79
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
83
80
|
import { dirname as dirname4, join as join13 } from "path";
|
|
84
81
|
import * as readline from "readline";
|
|
85
82
|
import * as fs15 from "fs";
|
|
86
83
|
import * as path14 from "path";
|
|
87
|
-
import { execFileSync as
|
|
84
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
88
85
|
import * as path15 from "path";
|
|
89
86
|
import * as path16 from "path";
|
|
90
87
|
import * as fs16 from "fs";
|
|
91
88
|
import * as path17 from "path";
|
|
92
|
-
import { execSync as
|
|
89
|
+
import { execSync as execSync4 } from "child_process";
|
|
93
90
|
import * as path18 from "path";
|
|
94
91
|
import * as fs17 from "fs";
|
|
95
92
|
import * as path19 from "path";
|
|
@@ -130,7 +127,7 @@ import { Router as Router3 } from "express";
|
|
|
130
127
|
import { Router as Router4 } from "express";
|
|
131
128
|
import * as fs25 from "fs";
|
|
132
129
|
import * as path25 from "path";
|
|
133
|
-
import { execSync as
|
|
130
|
+
import { execSync as execSync5 } from "child_process";
|
|
134
131
|
import { Router as Router5 } from "express";
|
|
135
132
|
import * as path26 from "path";
|
|
136
133
|
import { Router as Router6 } from "express";
|
|
@@ -146,16 +143,17 @@ import * as fs29 from "fs";
|
|
|
146
143
|
import * as path30 from "path";
|
|
147
144
|
import chalk3 from "chalk";
|
|
148
145
|
import chalk4 from "chalk";
|
|
149
|
-
import { execSync as
|
|
146
|
+
import { execSync as execSync6 } from "child_process";
|
|
150
147
|
import * as fs30 from "fs";
|
|
151
148
|
import * as readline3 from "readline";
|
|
149
|
+
import * as fs31 from "fs";
|
|
152
150
|
import * as path31 from "path";
|
|
153
151
|
import * as os5 from "os";
|
|
154
152
|
import * as path32 from "path";
|
|
155
153
|
import chalk5 from "chalk";
|
|
156
154
|
import { Command } from "commander";
|
|
157
|
-
import { execFileSync as
|
|
158
|
-
import * as
|
|
155
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
156
|
+
import * as fs32 from "fs";
|
|
159
157
|
import * as path33 from "path";
|
|
160
158
|
import * as readline4 from "readline";
|
|
161
159
|
import chalk6 from "chalk";
|
|
@@ -181,11 +179,14 @@ var DEFAULT_CRON_SCHEDULE;
|
|
|
181
179
|
var DEFAULT_REVIEWER_SCHEDULE;
|
|
182
180
|
var DEFAULT_CRON_SCHEDULE_OFFSET;
|
|
183
181
|
var DEFAULT_MAX_RETRIES;
|
|
182
|
+
var DEFAULT_REVIEWER_MAX_RETRIES;
|
|
183
|
+
var DEFAULT_REVIEWER_RETRY_DELAY;
|
|
184
184
|
var DEFAULT_BRANCH_PREFIX;
|
|
185
185
|
var DEFAULT_BRANCH_PATTERNS;
|
|
186
186
|
var DEFAULT_MIN_REVIEW_SCORE;
|
|
187
187
|
var DEFAULT_MAX_LOG_SIZE;
|
|
188
188
|
var DEFAULT_PROVIDER;
|
|
189
|
+
var DEFAULT_EXECUTOR_ENABLED;
|
|
189
190
|
var DEFAULT_REVIEWER_ENABLED;
|
|
190
191
|
var DEFAULT_PROVIDER_ENV;
|
|
191
192
|
var DEFAULT_FALLBACK_ON_RATE_LIMIT;
|
|
@@ -216,6 +217,7 @@ var DEFAULT_AUDIT_SCHEDULE;
|
|
|
216
217
|
var DEFAULT_AUDIT_MAX_RUNTIME;
|
|
217
218
|
var DEFAULT_AUDIT;
|
|
218
219
|
var AUDIT_LOG_NAME;
|
|
220
|
+
var PLANNER_LOG_NAME;
|
|
219
221
|
var VALID_PROVIDERS;
|
|
220
222
|
var VALID_JOB_TYPES;
|
|
221
223
|
var DEFAULT_JOB_PROVIDERS;
|
|
@@ -239,18 +241,21 @@ var init_constants = __esm({
|
|
|
239
241
|
"../core/dist/constants.js"() {
|
|
240
242
|
"use strict";
|
|
241
243
|
DEFAULT_DEFAULT_BRANCH = "";
|
|
242
|
-
DEFAULT_PRD_DIR = "docs/
|
|
244
|
+
DEFAULT_PRD_DIR = "docs/prds";
|
|
243
245
|
DEFAULT_MAX_RUNTIME = 7200;
|
|
244
246
|
DEFAULT_REVIEWER_MAX_RUNTIME = 3600;
|
|
245
247
|
DEFAULT_CRON_SCHEDULE = "0 0-21 * * *";
|
|
246
248
|
DEFAULT_REVIEWER_SCHEDULE = "0 0,3,6,9,12,15,18,21 * * *";
|
|
247
249
|
DEFAULT_CRON_SCHEDULE_OFFSET = 0;
|
|
248
250
|
DEFAULT_MAX_RETRIES = 3;
|
|
251
|
+
DEFAULT_REVIEWER_MAX_RETRIES = 2;
|
|
252
|
+
DEFAULT_REVIEWER_RETRY_DELAY = 30;
|
|
249
253
|
DEFAULT_BRANCH_PREFIX = "night-watch";
|
|
250
254
|
DEFAULT_BRANCH_PATTERNS = ["feat/", "night-watch/"];
|
|
251
255
|
DEFAULT_MIN_REVIEW_SCORE = 80;
|
|
252
256
|
DEFAULT_MAX_LOG_SIZE = 524288;
|
|
253
257
|
DEFAULT_PROVIDER = "claude";
|
|
258
|
+
DEFAULT_EXECUTOR_ENABLED = true;
|
|
254
259
|
DEFAULT_REVIEWER_ENABLED = true;
|
|
255
260
|
DEFAULT_PROVIDER_ENV = {};
|
|
256
261
|
DEFAULT_FALLBACK_ON_RATE_LIMIT = false;
|
|
@@ -265,7 +270,7 @@ var init_constants = __esm({
|
|
|
265
270
|
DEFAULT_SLICER_SCHEDULE = "0 */6 * * *";
|
|
266
271
|
DEFAULT_SLICER_MAX_RUNTIME = 600;
|
|
267
272
|
DEFAULT_ROADMAP_SCANNER = {
|
|
268
|
-
enabled:
|
|
273
|
+
enabled: true,
|
|
269
274
|
roadmapPath: "ROADMAP.md",
|
|
270
275
|
autoScanInterval: 300,
|
|
271
276
|
slicerSchedule: DEFAULT_SLICER_SCHEDULE,
|
|
@@ -305,6 +310,7 @@ var init_constants = __esm({
|
|
|
305
310
|
maxRuntime: DEFAULT_AUDIT_MAX_RUNTIME
|
|
306
311
|
};
|
|
307
312
|
AUDIT_LOG_NAME = "audit";
|
|
313
|
+
PLANNER_LOG_NAME = "slicer";
|
|
308
314
|
VALID_PROVIDERS = ["claude", "codex"];
|
|
309
315
|
VALID_JOB_TYPES = ["executor", "reviewer", "qa", "audit", "slicer"];
|
|
310
316
|
DEFAULT_JOB_PROVIDERS = {};
|
|
@@ -323,7 +329,9 @@ var init_constants = __esm({
|
|
|
323
329
|
LOG_FILE_NAMES = {
|
|
324
330
|
executor: EXECUTOR_LOG_NAME,
|
|
325
331
|
reviewer: REVIEWER_LOG_NAME,
|
|
326
|
-
qa: QA_LOG_NAME
|
|
332
|
+
qa: QA_LOG_NAME,
|
|
333
|
+
audit: AUDIT_LOG_NAME,
|
|
334
|
+
planner: PLANNER_LOG_NAME
|
|
327
335
|
};
|
|
328
336
|
GLOBAL_CONFIG_DIR = ".night-watch";
|
|
329
337
|
REGISTRY_FILE_NAME = "projects.json";
|
|
@@ -349,8 +357,12 @@ function getDefaultConfig() {
|
|
|
349
357
|
reviewerSchedule: DEFAULT_REVIEWER_SCHEDULE,
|
|
350
358
|
cronScheduleOffset: DEFAULT_CRON_SCHEDULE_OFFSET,
|
|
351
359
|
maxRetries: DEFAULT_MAX_RETRIES,
|
|
360
|
+
// Reviewer retry configuration
|
|
361
|
+
reviewerMaxRetries: DEFAULT_REVIEWER_MAX_RETRIES,
|
|
362
|
+
reviewerRetryDelay: DEFAULT_REVIEWER_RETRY_DELAY,
|
|
352
363
|
// Provider configuration
|
|
353
364
|
provider: DEFAULT_PROVIDER,
|
|
365
|
+
executorEnabled: DEFAULT_EXECUTOR_ENABLED,
|
|
354
366
|
reviewerEnabled: DEFAULT_REVIEWER_ENABLED,
|
|
355
367
|
providerEnv: { ...DEFAULT_PROVIDER_ENV },
|
|
356
368
|
// Notification configuration
|
|
@@ -412,7 +424,10 @@ function normalizeConfig(rawConfig) {
|
|
|
412
424
|
normalized.reviewerSchedule = readString(rawConfig.reviewerSchedule) ?? readString(cron?.reviewerSchedule);
|
|
413
425
|
normalized.cronScheduleOffset = readNumber(rawConfig.cronScheduleOffset);
|
|
414
426
|
normalized.maxRetries = readNumber(rawConfig.maxRetries);
|
|
427
|
+
normalized.reviewerMaxRetries = readNumber(rawConfig.reviewerMaxRetries);
|
|
428
|
+
normalized.reviewerRetryDelay = readNumber(rawConfig.reviewerRetryDelay);
|
|
415
429
|
normalized.provider = validateProvider(String(rawConfig.provider ?? "")) ?? void 0;
|
|
430
|
+
normalized.executorEnabled = readBoolean(rawConfig.executorEnabled);
|
|
416
431
|
normalized.reviewerEnabled = readBoolean(rawConfig.reviewerEnabled);
|
|
417
432
|
const rawProviderEnv = readObject(rawConfig.providerEnv);
|
|
418
433
|
if (rawProviderEnv) {
|
|
@@ -552,6 +567,28 @@ function sanitizeMaxRetries(value, fallback) {
|
|
|
552
567
|
const normalized = Math.floor(value);
|
|
553
568
|
return normalized >= 1 ? normalized : fallback;
|
|
554
569
|
}
|
|
570
|
+
function sanitizeReviewerMaxRetries(value, fallback) {
|
|
571
|
+
if (!Number.isFinite(value)) {
|
|
572
|
+
return fallback;
|
|
573
|
+
}
|
|
574
|
+
const normalized = Math.floor(value);
|
|
575
|
+
if (normalized < 0)
|
|
576
|
+
return 0;
|
|
577
|
+
if (normalized > 10)
|
|
578
|
+
return 10;
|
|
579
|
+
return normalized;
|
|
580
|
+
}
|
|
581
|
+
function sanitizeReviewerRetryDelay(value, fallback) {
|
|
582
|
+
if (!Number.isFinite(value)) {
|
|
583
|
+
return fallback;
|
|
584
|
+
}
|
|
585
|
+
const normalized = Math.floor(value);
|
|
586
|
+
if (normalized < 0)
|
|
587
|
+
return 0;
|
|
588
|
+
if (normalized > 300)
|
|
589
|
+
return 300;
|
|
590
|
+
return normalized;
|
|
591
|
+
}
|
|
555
592
|
function mergeConfigs(base, fileConfig, envConfig) {
|
|
556
593
|
const merged = { ...base };
|
|
557
594
|
if (fileConfig) {
|
|
@@ -579,8 +616,14 @@ function mergeConfigs(base, fileConfig, envConfig) {
|
|
|
579
616
|
merged.cronScheduleOffset = fileConfig.cronScheduleOffset;
|
|
580
617
|
if (fileConfig.maxRetries !== void 0)
|
|
581
618
|
merged.maxRetries = fileConfig.maxRetries;
|
|
619
|
+
if (fileConfig.reviewerMaxRetries !== void 0)
|
|
620
|
+
merged.reviewerMaxRetries = fileConfig.reviewerMaxRetries;
|
|
621
|
+
if (fileConfig.reviewerRetryDelay !== void 0)
|
|
622
|
+
merged.reviewerRetryDelay = fileConfig.reviewerRetryDelay;
|
|
582
623
|
if (fileConfig.provider !== void 0)
|
|
583
624
|
merged.provider = fileConfig.provider;
|
|
625
|
+
if (fileConfig.executorEnabled !== void 0)
|
|
626
|
+
merged.executorEnabled = fileConfig.executorEnabled;
|
|
584
627
|
if (fileConfig.reviewerEnabled !== void 0)
|
|
585
628
|
merged.reviewerEnabled = fileConfig.reviewerEnabled;
|
|
586
629
|
if (fileConfig.providerEnv !== void 0)
|
|
@@ -605,6 +648,8 @@ function mergeConfigs(base, fileConfig, envConfig) {
|
|
|
605
648
|
merged.claudeModel = fileConfig.claudeModel;
|
|
606
649
|
if (fileConfig.qa !== void 0)
|
|
607
650
|
merged.qa = { ...merged.qa, ...fileConfig.qa };
|
|
651
|
+
if (fileConfig.audit !== void 0)
|
|
652
|
+
merged.audit = { ...merged.audit, ...fileConfig.audit };
|
|
608
653
|
if (fileConfig.jobProviders !== void 0)
|
|
609
654
|
merged.jobProviders = { ...fileConfig.jobProviders };
|
|
610
655
|
}
|
|
@@ -632,8 +677,14 @@ function mergeConfigs(base, fileConfig, envConfig) {
|
|
|
632
677
|
merged.cronScheduleOffset = envConfig.cronScheduleOffset;
|
|
633
678
|
if (envConfig.maxRetries !== void 0)
|
|
634
679
|
merged.maxRetries = envConfig.maxRetries;
|
|
680
|
+
if (envConfig.reviewerMaxRetries !== void 0)
|
|
681
|
+
merged.reviewerMaxRetries = envConfig.reviewerMaxRetries;
|
|
682
|
+
if (envConfig.reviewerRetryDelay !== void 0)
|
|
683
|
+
merged.reviewerRetryDelay = envConfig.reviewerRetryDelay;
|
|
635
684
|
if (envConfig.provider !== void 0)
|
|
636
685
|
merged.provider = envConfig.provider;
|
|
686
|
+
if (envConfig.executorEnabled !== void 0)
|
|
687
|
+
merged.executorEnabled = envConfig.executorEnabled;
|
|
637
688
|
if (envConfig.reviewerEnabled !== void 0)
|
|
638
689
|
merged.reviewerEnabled = envConfig.reviewerEnabled;
|
|
639
690
|
if (envConfig.providerEnv !== void 0)
|
|
@@ -658,9 +709,13 @@ function mergeConfigs(base, fileConfig, envConfig) {
|
|
|
658
709
|
merged.claudeModel = envConfig.claudeModel;
|
|
659
710
|
if (envConfig.qa !== void 0)
|
|
660
711
|
merged.qa = { ...merged.qa, ...envConfig.qa };
|
|
712
|
+
if (envConfig.audit !== void 0)
|
|
713
|
+
merged.audit = { ...merged.audit, ...envConfig.audit };
|
|
661
714
|
if (envConfig.jobProviders !== void 0)
|
|
662
715
|
merged.jobProviders = { ...envConfig.jobProviders };
|
|
663
716
|
merged.maxRetries = sanitizeMaxRetries(merged.maxRetries, DEFAULT_MAX_RETRIES);
|
|
717
|
+
merged.reviewerMaxRetries = sanitizeReviewerMaxRetries(merged.reviewerMaxRetries, DEFAULT_REVIEWER_MAX_RETRIES);
|
|
718
|
+
merged.reviewerRetryDelay = sanitizeReviewerRetryDelay(merged.reviewerRetryDelay, DEFAULT_REVIEWER_RETRY_DELAY);
|
|
664
719
|
return merged;
|
|
665
720
|
}
|
|
666
721
|
function loadConfig(projectDir) {
|
|
@@ -726,6 +781,18 @@ function loadConfig(projectDir) {
|
|
|
726
781
|
envConfig.maxRetries = retries;
|
|
727
782
|
}
|
|
728
783
|
}
|
|
784
|
+
if (process.env.NW_REVIEWER_MAX_RETRIES !== void 0) {
|
|
785
|
+
const reviewerMaxRetries = parseInt(process.env.NW_REVIEWER_MAX_RETRIES, 10);
|
|
786
|
+
if (!isNaN(reviewerMaxRetries) && reviewerMaxRetries >= 0) {
|
|
787
|
+
envConfig.reviewerMaxRetries = reviewerMaxRetries;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (process.env.NW_REVIEWER_RETRY_DELAY !== void 0) {
|
|
791
|
+
const reviewerRetryDelay = parseInt(process.env.NW_REVIEWER_RETRY_DELAY, 10);
|
|
792
|
+
if (!isNaN(reviewerRetryDelay) && reviewerRetryDelay >= 0) {
|
|
793
|
+
envConfig.reviewerRetryDelay = reviewerRetryDelay;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
729
796
|
if (process.env.NW_PROVIDER) {
|
|
730
797
|
const provider = validateProvider(process.env.NW_PROVIDER);
|
|
731
798
|
if (provider !== null) {
|
|
@@ -738,6 +805,12 @@ function loadConfig(projectDir) {
|
|
|
738
805
|
envConfig.reviewerEnabled = reviewerEnabled;
|
|
739
806
|
}
|
|
740
807
|
}
|
|
808
|
+
if (process.env.NW_EXECUTOR_ENABLED) {
|
|
809
|
+
const executorEnabled = parseBoolean(process.env.NW_EXECUTOR_ENABLED);
|
|
810
|
+
if (executorEnabled !== null) {
|
|
811
|
+
envConfig.executorEnabled = executorEnabled;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
741
814
|
if (process.env.NW_NOTIFICATIONS) {
|
|
742
815
|
try {
|
|
743
816
|
const parsed = JSON.parse(process.env.NW_NOTIFICATIONS);
|
|
@@ -2361,7 +2434,7 @@ var init_container = __esm({
|
|
|
2361
2434
|
DATABASE_TOKEN = "Database";
|
|
2362
2435
|
}
|
|
2363
2436
|
});
|
|
2364
|
-
function graphql(query, variables, cwd) {
|
|
2437
|
+
async function graphql(query, variables, cwd) {
|
|
2365
2438
|
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
2366
2439
|
for (const [key, value] of Object.entries(variables)) {
|
|
2367
2440
|
if (typeof value === "number") {
|
|
@@ -2370,10 +2443,9 @@ function graphql(query, variables, cwd) {
|
|
|
2370
2443
|
args.push("-f", `${key}=${String(value)}`);
|
|
2371
2444
|
}
|
|
2372
2445
|
}
|
|
2373
|
-
const output =
|
|
2446
|
+
const { stdout: output } = await execFileAsync("gh", args, {
|
|
2374
2447
|
cwd,
|
|
2375
|
-
encoding: "utf-8"
|
|
2376
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2448
|
+
encoding: "utf-8"
|
|
2377
2449
|
});
|
|
2378
2450
|
const parsed = JSON.parse(output);
|
|
2379
2451
|
if (parsed.errors?.length) {
|
|
@@ -2381,25 +2453,29 @@ function graphql(query, variables, cwd) {
|
|
|
2381
2453
|
}
|
|
2382
2454
|
return parsed.data;
|
|
2383
2455
|
}
|
|
2384
|
-
function getRepoNwo(cwd) {
|
|
2385
|
-
const output =
|
|
2456
|
+
async function getRepoNwo(cwd) {
|
|
2457
|
+
const { stdout: output } = await execFileAsync("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], { cwd, encoding: "utf-8" });
|
|
2386
2458
|
return output.trim();
|
|
2387
2459
|
}
|
|
2388
|
-
function getViewerLogin(cwd) {
|
|
2389
|
-
const result = graphql(`query { viewer { login } }`, {}, cwd);
|
|
2460
|
+
async function getViewerLogin(cwd) {
|
|
2461
|
+
const result = await graphql(`query { viewer { login } }`, {}, cwd);
|
|
2390
2462
|
return result.viewer.login;
|
|
2391
2463
|
}
|
|
2464
|
+
var execFileAsync;
|
|
2392
2465
|
var init_github_graphql = __esm({
|
|
2393
2466
|
"../core/dist/board/providers/github-graphql.js"() {
|
|
2394
2467
|
"use strict";
|
|
2468
|
+
execFileAsync = promisify(execFile);
|
|
2395
2469
|
}
|
|
2396
2470
|
});
|
|
2471
|
+
var execFileAsync2;
|
|
2397
2472
|
var GitHubProjectsProvider;
|
|
2398
2473
|
var init_github_projects = __esm({
|
|
2399
2474
|
"../core/dist/board/providers/github-projects.js"() {
|
|
2400
2475
|
"use strict";
|
|
2401
2476
|
init_types2();
|
|
2402
2477
|
init_github_graphql();
|
|
2478
|
+
execFileAsync2 = promisify2(execFile2);
|
|
2403
2479
|
GitHubProjectsProvider = class {
|
|
2404
2480
|
config;
|
|
2405
2481
|
cwd;
|
|
@@ -2415,26 +2491,26 @@ var init_github_projects = __esm({
|
|
|
2415
2491
|
// -------------------------------------------------------------------------
|
|
2416
2492
|
// Helpers
|
|
2417
2493
|
// -------------------------------------------------------------------------
|
|
2418
|
-
getRepo() {
|
|
2494
|
+
async getRepo() {
|
|
2419
2495
|
return this.config.repo ?? getRepoNwo(this.cwd);
|
|
2420
2496
|
}
|
|
2421
|
-
getRepoParts() {
|
|
2422
|
-
const repo = this.getRepo();
|
|
2497
|
+
async getRepoParts() {
|
|
2498
|
+
const repo = await this.getRepo();
|
|
2423
2499
|
const [owner, name] = repo.split("/");
|
|
2424
2500
|
if (!owner || !name) {
|
|
2425
2501
|
throw new Error(`Invalid repository slug: "${repo}". Expected "owner/repo".`);
|
|
2426
2502
|
}
|
|
2427
2503
|
return { owner, name };
|
|
2428
2504
|
}
|
|
2429
|
-
getRepoOwnerLogin() {
|
|
2430
|
-
return this.getRepoParts().owner;
|
|
2505
|
+
async getRepoOwnerLogin() {
|
|
2506
|
+
return (await this.getRepoParts()).owner;
|
|
2431
2507
|
}
|
|
2432
|
-
getRepoOwner() {
|
|
2508
|
+
async getRepoOwner() {
|
|
2433
2509
|
if (this.cachedOwner && this.cachedRepositoryId) {
|
|
2434
2510
|
return this.cachedOwner;
|
|
2435
2511
|
}
|
|
2436
|
-
const { owner, name } = this.getRepoParts();
|
|
2437
|
-
const data = graphql(`query ResolveRepoOwner($owner: String!, $name: String!) {
|
|
2512
|
+
const { owner, name } = await this.getRepoParts();
|
|
2513
|
+
const data = await graphql(`query ResolveRepoOwner($owner: String!, $name: String!) {
|
|
2438
2514
|
repository(owner: $owner, name: $name) {
|
|
2439
2515
|
id
|
|
2440
2516
|
owner {
|
|
@@ -2459,20 +2535,20 @@ var init_github_projects = __esm({
|
|
|
2459
2535
|
};
|
|
2460
2536
|
return this.cachedOwner;
|
|
2461
2537
|
}
|
|
2462
|
-
getRepositoryNodeId() {
|
|
2538
|
+
async getRepositoryNodeId() {
|
|
2463
2539
|
if (this.cachedRepositoryId) {
|
|
2464
2540
|
return this.cachedRepositoryId;
|
|
2465
2541
|
}
|
|
2466
|
-
this.getRepoOwner();
|
|
2542
|
+
await this.getRepoOwner();
|
|
2467
2543
|
if (!this.cachedRepositoryId) {
|
|
2468
|
-
throw new Error(`Failed to resolve repository ID for ${this.getRepo()}.`);
|
|
2544
|
+
throw new Error(`Failed to resolve repository ID for ${await this.getRepo()}.`);
|
|
2469
2545
|
}
|
|
2470
2546
|
return this.cachedRepositoryId;
|
|
2471
2547
|
}
|
|
2472
|
-
linkProjectToRepository(projectId) {
|
|
2473
|
-
const repositoryId = this.getRepositoryNodeId();
|
|
2548
|
+
async linkProjectToRepository(projectId) {
|
|
2549
|
+
const repositoryId = await this.getRepositoryNodeId();
|
|
2474
2550
|
try {
|
|
2475
|
-
graphql(`mutation LinkProjectToRepository($projectId: ID!, $repositoryId: ID!) {
|
|
2551
|
+
await graphql(`mutation LinkProjectToRepository($projectId: ID!, $repositoryId: ID!) {
|
|
2476
2552
|
linkProjectV2ToRepository(input: { projectId: $projectId, repositoryId: $repositoryId }) {
|
|
2477
2553
|
repository {
|
|
2478
2554
|
id
|
|
@@ -2488,8 +2564,8 @@ var init_github_projects = __esm({
|
|
|
2488
2564
|
throw err;
|
|
2489
2565
|
}
|
|
2490
2566
|
}
|
|
2491
|
-
fetchStatusField(projectId) {
|
|
2492
|
-
const fieldData = graphql(`query GetStatusField($projectId: ID!) {
|
|
2567
|
+
async fetchStatusField(projectId) {
|
|
2568
|
+
const fieldData = await graphql(`query GetStatusField($projectId: ID!) {
|
|
2493
2569
|
node(id: $projectId) {
|
|
2494
2570
|
... on ProjectV2 {
|
|
2495
2571
|
field(name: "Status") {
|
|
@@ -2526,7 +2602,7 @@ var init_github_projects = __esm({
|
|
|
2526
2602
|
};
|
|
2527
2603
|
}
|
|
2528
2604
|
if (this.cachedProjectId !== null) {
|
|
2529
|
-
const statusField2 = this.fetchStatusField(this.cachedProjectId);
|
|
2605
|
+
const statusField2 = await this.fetchStatusField(this.cachedProjectId);
|
|
2530
2606
|
this.cachedFieldId = statusField2.fieldId;
|
|
2531
2607
|
this.cachedOptionIds = statusField2.optionIds;
|
|
2532
2608
|
return {
|
|
@@ -2539,23 +2615,23 @@ var init_github_projects = __esm({
|
|
|
2539
2615
|
if (!projectNumber) {
|
|
2540
2616
|
throw new Error("No projectNumber configured. Run `night-watch board setup` first.");
|
|
2541
2617
|
}
|
|
2542
|
-
const ownerLogins = /* @__PURE__ */ new Set([this.getRepoOwnerLogin()]);
|
|
2618
|
+
const ownerLogins = /* @__PURE__ */ new Set([await this.getRepoOwnerLogin()]);
|
|
2543
2619
|
try {
|
|
2544
|
-
ownerLogins.add(getViewerLogin(this.cwd));
|
|
2620
|
+
ownerLogins.add(await getViewerLogin(this.cwd));
|
|
2545
2621
|
} catch {
|
|
2546
2622
|
}
|
|
2547
2623
|
let projectNode = null;
|
|
2548
2624
|
for (const login of ownerLogins) {
|
|
2549
|
-
projectNode = this.fetchProjectNode(login, projectNumber);
|
|
2625
|
+
projectNode = await this.fetchProjectNode(login, projectNumber);
|
|
2550
2626
|
if (projectNode) {
|
|
2551
2627
|
break;
|
|
2552
2628
|
}
|
|
2553
2629
|
}
|
|
2554
2630
|
if (!projectNode) {
|
|
2555
|
-
throw new Error(`GitHub Project #${projectNumber} not found for repository owner "${this.getRepoOwnerLogin()}".`);
|
|
2631
|
+
throw new Error(`GitHub Project #${projectNumber} not found for repository owner "${await this.getRepoOwnerLogin()}".`);
|
|
2556
2632
|
}
|
|
2557
2633
|
this.cachedProjectId = projectNode.id;
|
|
2558
|
-
const statusField = this.fetchStatusField(projectNode.id);
|
|
2634
|
+
const statusField = await this.fetchStatusField(projectNode.id);
|
|
2559
2635
|
this.cachedFieldId = statusField.fieldId;
|
|
2560
2636
|
this.cachedOptionIds = statusField.optionIds;
|
|
2561
2637
|
return {
|
|
@@ -2565,9 +2641,9 @@ var init_github_projects = __esm({
|
|
|
2565
2641
|
};
|
|
2566
2642
|
}
|
|
2567
2643
|
/** Try user query first, fall back to org query. */
|
|
2568
|
-
fetchProjectNode(login, projectNumber) {
|
|
2644
|
+
async fetchProjectNode(login, projectNumber) {
|
|
2569
2645
|
try {
|
|
2570
|
-
const userData = graphql(`query GetProject($login: String!, $number: Int!) {
|
|
2646
|
+
const userData = await graphql(`query GetProject($login: String!, $number: Int!) {
|
|
2571
2647
|
user(login: $login) {
|
|
2572
2648
|
projectV2(number: $number) {
|
|
2573
2649
|
id
|
|
@@ -2583,7 +2659,7 @@ var init_github_projects = __esm({
|
|
|
2583
2659
|
} catch {
|
|
2584
2660
|
}
|
|
2585
2661
|
try {
|
|
2586
|
-
const orgData = graphql(`query GetOrgProject($login: String!, $number: Int!) {
|
|
2662
|
+
const orgData = await graphql(`query GetOrgProject($login: String!, $number: Int!) {
|
|
2587
2663
|
organization(login: $login) {
|
|
2588
2664
|
projectV2(number: $number) {
|
|
2589
2665
|
id
|
|
@@ -2629,6 +2705,117 @@ var init_github_projects = __esm({
|
|
|
2629
2705
|
assignees: content.assignees?.nodes.map((a) => a.login) ?? []
|
|
2630
2706
|
};
|
|
2631
2707
|
}
|
|
2708
|
+
/**
|
|
2709
|
+
* Fetch ALL items from a GitHub ProjectV2 using cursor-based pagination.
|
|
2710
|
+
*
|
|
2711
|
+
* The API caps each page at 100 items. We loop until `hasNextPage` is false,
|
|
2712
|
+
* accumulating every item node so callers never see a truncated board.
|
|
2713
|
+
*/
|
|
2714
|
+
async fetchAllProjectItems(projectId) {
|
|
2715
|
+
const allNodes = [];
|
|
2716
|
+
let cursor = null;
|
|
2717
|
+
const query = `query GetProjectItems($projectId: ID!, $cursor: String) {
|
|
2718
|
+
node(id: $projectId) {
|
|
2719
|
+
... on ProjectV2 {
|
|
2720
|
+
items(first: 100, after: $cursor) {
|
|
2721
|
+
pageInfo {
|
|
2722
|
+
hasNextPage
|
|
2723
|
+
endCursor
|
|
2724
|
+
}
|
|
2725
|
+
nodes {
|
|
2726
|
+
id
|
|
2727
|
+
content {
|
|
2728
|
+
... on Issue {
|
|
2729
|
+
number
|
|
2730
|
+
title
|
|
2731
|
+
body
|
|
2732
|
+
url
|
|
2733
|
+
id
|
|
2734
|
+
labels(first: 10) { nodes { name } }
|
|
2735
|
+
assignees(first: 10) { nodes { login } }
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
fieldValues(first: 10) {
|
|
2739
|
+
nodes {
|
|
2740
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
2741
|
+
name
|
|
2742
|
+
field {
|
|
2743
|
+
... on ProjectV2SingleSelectField {
|
|
2744
|
+
name
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
}`;
|
|
2755
|
+
do {
|
|
2756
|
+
const variables = { projectId };
|
|
2757
|
+
if (cursor !== null) {
|
|
2758
|
+
variables.cursor = cursor;
|
|
2759
|
+
}
|
|
2760
|
+
const data = await graphql(query, variables, this.cwd);
|
|
2761
|
+
const page = data.node.items;
|
|
2762
|
+
allNodes.push(...page.nodes);
|
|
2763
|
+
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
|
|
2764
|
+
} while (cursor !== null);
|
|
2765
|
+
return allNodes;
|
|
2766
|
+
}
|
|
2767
|
+
/**
|
|
2768
|
+
* Fetch project items for moveIssue — only needs id, content.number, and
|
|
2769
|
+
* fieldValues. Uses the same paginated approach to ensure items beyond
|
|
2770
|
+
* position 100 are reachable.
|
|
2771
|
+
*/
|
|
2772
|
+
async fetchAllProjectItemsForMove(projectId) {
|
|
2773
|
+
const allNodes = [];
|
|
2774
|
+
let cursor = null;
|
|
2775
|
+
const query = `query GetProjectItemsForMove($projectId: ID!, $cursor: String) {
|
|
2776
|
+
node(id: $projectId) {
|
|
2777
|
+
... on ProjectV2 {
|
|
2778
|
+
items(first: 100, after: $cursor) {
|
|
2779
|
+
pageInfo {
|
|
2780
|
+
hasNextPage
|
|
2781
|
+
endCursor
|
|
2782
|
+
}
|
|
2783
|
+
nodes {
|
|
2784
|
+
id
|
|
2785
|
+
content {
|
|
2786
|
+
... on Issue {
|
|
2787
|
+
number
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
fieldValues(first: 10) {
|
|
2791
|
+
nodes {
|
|
2792
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
2793
|
+
name
|
|
2794
|
+
field {
|
|
2795
|
+
... on ProjectV2SingleSelectField {
|
|
2796
|
+
name
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}`;
|
|
2807
|
+
do {
|
|
2808
|
+
const variables = { projectId };
|
|
2809
|
+
if (cursor !== null) {
|
|
2810
|
+
variables.cursor = cursor;
|
|
2811
|
+
}
|
|
2812
|
+
const data = await graphql(query, variables, this.cwd);
|
|
2813
|
+
const page = data.node.items;
|
|
2814
|
+
allNodes.push(...page.nodes);
|
|
2815
|
+
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
|
|
2816
|
+
} while (cursor !== null);
|
|
2817
|
+
return allNodes;
|
|
2818
|
+
}
|
|
2632
2819
|
// -------------------------------------------------------------------------
|
|
2633
2820
|
// IBoardProvider implementation
|
|
2634
2821
|
// -------------------------------------------------------------------------
|
|
@@ -2636,10 +2823,10 @@ var init_github_projects = __esm({
|
|
|
2636
2823
|
* Find an existing project by title among the repository owner's first 50 projects.
|
|
2637
2824
|
* Returns null if not found.
|
|
2638
2825
|
*/
|
|
2639
|
-
findExistingProject(owner, title) {
|
|
2826
|
+
async findExistingProject(owner, title) {
|
|
2640
2827
|
try {
|
|
2641
2828
|
if (owner.type === "User") {
|
|
2642
|
-
const data2 = graphql(`query ListUserProjects($login: String!) {
|
|
2829
|
+
const data2 = await graphql(`query ListUserProjects($login: String!) {
|
|
2643
2830
|
user(login: $login) {
|
|
2644
2831
|
projectsV2(first: 50) {
|
|
2645
2832
|
nodes { id number title url }
|
|
@@ -2648,7 +2835,7 @@ var init_github_projects = __esm({
|
|
|
2648
2835
|
}`, { login: owner.login }, this.cwd);
|
|
2649
2836
|
return data2.user?.projectsV2.nodes.find((p) => p.title === title) ?? null;
|
|
2650
2837
|
}
|
|
2651
|
-
const data = graphql(`query ListOrgProjects($login: String!) {
|
|
2838
|
+
const data = await graphql(`query ListOrgProjects($login: String!) {
|
|
2652
2839
|
organization(login: $login) {
|
|
2653
2840
|
projectsV2(first: 50) {
|
|
2654
2841
|
nodes { id number title url }
|
|
@@ -2664,8 +2851,8 @@ var init_github_projects = __esm({
|
|
|
2664
2851
|
* Ensure the Status field on an existing project has all five Night Watch
|
|
2665
2852
|
* lifecycle columns, updating it via GraphQL if any are missing.
|
|
2666
2853
|
*/
|
|
2667
|
-
ensureStatusColumns(projectId) {
|
|
2668
|
-
const fieldData = graphql(`query GetStatusField($projectId: ID!) {
|
|
2854
|
+
async ensureStatusColumns(projectId) {
|
|
2855
|
+
const fieldData = await graphql(`query GetStatusField($projectId: ID!) {
|
|
2669
2856
|
node(id: $projectId) {
|
|
2670
2857
|
... on ProjectV2 {
|
|
2671
2858
|
field(name: "Status") {
|
|
@@ -2697,7 +2884,7 @@ var init_github_projects = __esm({
|
|
|
2697
2884
|
color: colorMap[name],
|
|
2698
2885
|
description: ""
|
|
2699
2886
|
}));
|
|
2700
|
-
graphql(`mutation UpdateField($fieldId: ID!) {
|
|
2887
|
+
await graphql(`mutation UpdateField($fieldId: ID!) {
|
|
2701
2888
|
updateProjectV2Field(input: {
|
|
2702
2889
|
fieldId: $fieldId,
|
|
2703
2890
|
singleSelectOptions: [
|
|
@@ -2718,15 +2905,15 @@ var init_github_projects = __esm({
|
|
|
2718
2905
|
}`, { fieldId: field.id, allOptions }, this.cwd);
|
|
2719
2906
|
}
|
|
2720
2907
|
async setupBoard(title) {
|
|
2721
|
-
const owner = this.getRepoOwner();
|
|
2722
|
-
const existing = this.findExistingProject(owner, title);
|
|
2908
|
+
const owner = await this.getRepoOwner();
|
|
2909
|
+
const existing = await this.findExistingProject(owner, title);
|
|
2723
2910
|
if (existing) {
|
|
2724
2911
|
this.cachedProjectId = existing.id;
|
|
2725
|
-
this.linkProjectToRepository(existing.id);
|
|
2726
|
-
this.ensureStatusColumns(existing.id);
|
|
2912
|
+
await this.linkProjectToRepository(existing.id);
|
|
2913
|
+
await this.ensureStatusColumns(existing.id);
|
|
2727
2914
|
return { id: existing.id, number: existing.number, title: existing.title, url: existing.url };
|
|
2728
2915
|
}
|
|
2729
|
-
const createData = graphql(`mutation CreateProject($ownerId: ID!, $title: String!) {
|
|
2916
|
+
const createData = await graphql(`mutation CreateProject($ownerId: ID!, $title: String!) {
|
|
2730
2917
|
createProjectV2(input: { ownerId: $ownerId, title: $title }) {
|
|
2731
2918
|
projectV2 {
|
|
2732
2919
|
id
|
|
@@ -2738,13 +2925,13 @@ var init_github_projects = __esm({
|
|
|
2738
2925
|
}`, { ownerId: owner.id, title }, this.cwd);
|
|
2739
2926
|
const project = createData.createProjectV2.projectV2;
|
|
2740
2927
|
this.cachedProjectId = project.id;
|
|
2741
|
-
this.linkProjectToRepository(project.id);
|
|
2928
|
+
await this.linkProjectToRepository(project.id);
|
|
2742
2929
|
try {
|
|
2743
|
-
const statusField = this.fetchStatusField(project.id);
|
|
2930
|
+
const statusField = await this.fetchStatusField(project.id);
|
|
2744
2931
|
this.cachedFieldId = statusField.fieldId;
|
|
2745
2932
|
this.cachedOptionIds = statusField.optionIds;
|
|
2746
|
-
this.ensureStatusColumns(project.id);
|
|
2747
|
-
const refreshed = this.fetchStatusField(project.id);
|
|
2933
|
+
await this.ensureStatusColumns(project.id);
|
|
2934
|
+
const refreshed = await this.fetchStatusField(project.id);
|
|
2748
2935
|
this.cachedFieldId = refreshed.fieldId;
|
|
2749
2936
|
this.cachedOptionIds = refreshed.optionIds;
|
|
2750
2937
|
} catch (err) {
|
|
@@ -2752,7 +2939,7 @@ var init_github_projects = __esm({
|
|
|
2752
2939
|
if (!message.includes("Status field not found")) {
|
|
2753
2940
|
throw err;
|
|
2754
2941
|
}
|
|
2755
|
-
const createFieldData = graphql(`mutation CreateStatusField($projectId: ID!) {
|
|
2942
|
+
const createFieldData = await graphql(`mutation CreateStatusField($projectId: ID!) {
|
|
2756
2943
|
createProjectV2Field(input: {
|
|
2757
2944
|
projectId: $projectId,
|
|
2758
2945
|
dataType: SINGLE_SELECT,
|
|
@@ -2785,14 +2972,14 @@ var init_github_projects = __esm({
|
|
|
2785
2972
|
return null;
|
|
2786
2973
|
}
|
|
2787
2974
|
try {
|
|
2788
|
-
const ownerLogins = /* @__PURE__ */ new Set([this.getRepoOwnerLogin()]);
|
|
2975
|
+
const ownerLogins = /* @__PURE__ */ new Set([await this.getRepoOwnerLogin()]);
|
|
2789
2976
|
try {
|
|
2790
|
-
ownerLogins.add(getViewerLogin(this.cwd));
|
|
2977
|
+
ownerLogins.add(await getViewerLogin(this.cwd));
|
|
2791
2978
|
} catch {
|
|
2792
2979
|
}
|
|
2793
2980
|
let node = null;
|
|
2794
2981
|
for (const login of ownerLogins) {
|
|
2795
|
-
node = this.fetchProjectNode(login, projectNumber);
|
|
2982
|
+
node = await this.fetchProjectNode(login, projectNumber);
|
|
2796
2983
|
if (node) {
|
|
2797
2984
|
break;
|
|
2798
2985
|
}
|
|
@@ -2813,7 +3000,7 @@ var init_github_projects = __esm({
|
|
|
2813
3000
|
}));
|
|
2814
3001
|
}
|
|
2815
3002
|
async createIssue(input) {
|
|
2816
|
-
const repo = this.getRepo();
|
|
3003
|
+
const repo = await this.getRepo();
|
|
2817
3004
|
const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
|
|
2818
3005
|
const issueArgs = [
|
|
2819
3006
|
"issue",
|
|
@@ -2828,19 +3015,20 @@ var init_github_projects = __esm({
|
|
|
2828
3015
|
if (input.labels && input.labels.length > 0) {
|
|
2829
3016
|
issueArgs.push("--label", input.labels.join(","));
|
|
2830
3017
|
}
|
|
2831
|
-
const
|
|
3018
|
+
const { stdout: issueUrlRaw } = await execFileAsync2("gh", issueArgs, {
|
|
2832
3019
|
cwd: this.cwd,
|
|
2833
|
-
encoding: "utf-8"
|
|
2834
|
-
|
|
2835
|
-
|
|
3020
|
+
encoding: "utf-8"
|
|
3021
|
+
});
|
|
3022
|
+
const issueUrl = issueUrlRaw.trim();
|
|
2836
3023
|
const issueNumber = parseInt(issueUrl.split("/").pop() ?? "", 10);
|
|
2837
3024
|
if (!issueNumber) {
|
|
2838
3025
|
throw new Error(`Failed to parse issue number from URL: ${issueUrl}`);
|
|
2839
3026
|
}
|
|
2840
3027
|
const [owner, repoName] = repo.split("/");
|
|
2841
|
-
const
|
|
3028
|
+
const { stdout: nodeIdRaw } = await execFileAsync2("gh", ["api", `repos/${owner}/${repoName}/issues/${issueNumber}`, "--jq", ".node_id"], { cwd: this.cwd, encoding: "utf-8" });
|
|
3029
|
+
const nodeIdOutput = nodeIdRaw.trim();
|
|
2842
3030
|
const issueJson = { number: issueNumber, id: nodeIdOutput, url: issueUrl };
|
|
2843
|
-
const addData = graphql(`mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
|
|
3031
|
+
const addData = await graphql(`mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
|
|
2844
3032
|
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
|
|
2845
3033
|
item {
|
|
2846
3034
|
id
|
|
@@ -2851,7 +3039,7 @@ var init_github_projects = __esm({
|
|
|
2851
3039
|
const targetColumn = input.column ?? "Draft";
|
|
2852
3040
|
const optionId = optionIds.get(targetColumn);
|
|
2853
3041
|
if (optionId) {
|
|
2854
|
-
graphql(`mutation UpdateItemField(
|
|
3042
|
+
await graphql(`mutation UpdateItemField(
|
|
2855
3043
|
$projectId: ID!,
|
|
2856
3044
|
$itemId: ID!,
|
|
2857
3045
|
$fieldId: ID!,
|
|
@@ -2885,10 +3073,10 @@ var init_github_projects = __esm({
|
|
|
2885
3073
|
};
|
|
2886
3074
|
}
|
|
2887
3075
|
async getIssue(issueNumber) {
|
|
2888
|
-
const repo = this.getRepo();
|
|
3076
|
+
const repo = await this.getRepo();
|
|
2889
3077
|
let rawIssue;
|
|
2890
3078
|
try {
|
|
2891
|
-
const output =
|
|
3079
|
+
const { stdout: output } = await execFileAsync2("gh", [
|
|
2892
3080
|
"issue",
|
|
2893
3081
|
"view",
|
|
2894
3082
|
String(issueNumber),
|
|
@@ -2896,7 +3084,7 @@ var init_github_projects = __esm({
|
|
|
2896
3084
|
repo,
|
|
2897
3085
|
"--json",
|
|
2898
3086
|
"number,title,body,url,id,labels,assignees"
|
|
2899
|
-
], { cwd: this.cwd, encoding: "utf-8"
|
|
3087
|
+
], { cwd: this.cwd, encoding: "utf-8" });
|
|
2900
3088
|
rawIssue = JSON.parse(output);
|
|
2901
3089
|
} catch {
|
|
2902
3090
|
return null;
|
|
@@ -2927,42 +3115,9 @@ var init_github_projects = __esm({
|
|
|
2927
3115
|
}
|
|
2928
3116
|
async getAllIssues() {
|
|
2929
3117
|
const { projectId } = await this.ensureProjectCache();
|
|
2930
|
-
const
|
|
2931
|
-
node(id: $projectId) {
|
|
2932
|
-
... on ProjectV2 {
|
|
2933
|
-
items(first: 100) {
|
|
2934
|
-
nodes {
|
|
2935
|
-
id
|
|
2936
|
-
content {
|
|
2937
|
-
... on Issue {
|
|
2938
|
-
number
|
|
2939
|
-
title
|
|
2940
|
-
body
|
|
2941
|
-
url
|
|
2942
|
-
id
|
|
2943
|
-
labels(first: 10) { nodes { name } }
|
|
2944
|
-
assignees(first: 10) { nodes { login } }
|
|
2945
|
-
}
|
|
2946
|
-
}
|
|
2947
|
-
fieldValues(first: 10) {
|
|
2948
|
-
nodes {
|
|
2949
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
2950
|
-
name
|
|
2951
|
-
field {
|
|
2952
|
-
... on ProjectV2SingleSelectField {
|
|
2953
|
-
name
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
}
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
}
|
|
2961
|
-
}
|
|
2962
|
-
}
|
|
2963
|
-
}`, { projectId }, this.cwd);
|
|
3118
|
+
const allNodes = await this.fetchAllProjectItems(projectId);
|
|
2964
3119
|
const results = [];
|
|
2965
|
-
for (const item of
|
|
3120
|
+
for (const item of allNodes) {
|
|
2966
3121
|
const parsed = this.parseItem(item);
|
|
2967
3122
|
if (parsed) {
|
|
2968
3123
|
results.push(parsed);
|
|
@@ -2972,35 +3127,8 @@ var init_github_projects = __esm({
|
|
|
2972
3127
|
}
|
|
2973
3128
|
async moveIssue(issueNumber, targetColumn) {
|
|
2974
3129
|
const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
|
|
2975
|
-
const
|
|
2976
|
-
|
|
2977
|
-
... on ProjectV2 {
|
|
2978
|
-
items(first: 100) {
|
|
2979
|
-
nodes {
|
|
2980
|
-
id
|
|
2981
|
-
content {
|
|
2982
|
-
... on Issue {
|
|
2983
|
-
number
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
fieldValues(first: 10) {
|
|
2987
|
-
nodes {
|
|
2988
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
2989
|
-
name
|
|
2990
|
-
field {
|
|
2991
|
-
... on ProjectV2SingleSelectField {
|
|
2992
|
-
name
|
|
2993
|
-
}
|
|
2994
|
-
}
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
}
|
|
3002
|
-
}`, { projectId }, this.cwd);
|
|
3003
|
-
const itemNode = data.node.items.nodes.find((n) => n.content?.number === issueNumber);
|
|
3130
|
+
const allNodes = await this.fetchAllProjectItemsForMove(projectId);
|
|
3131
|
+
const itemNode = allNodes.find((n) => n.content?.number === issueNumber);
|
|
3004
3132
|
if (!itemNode) {
|
|
3005
3133
|
throw new Error(`Issue #${issueNumber} not found on the project board.`);
|
|
3006
3134
|
}
|
|
@@ -3008,7 +3136,7 @@ var init_github_projects = __esm({
|
|
|
3008
3136
|
if (!optionId) {
|
|
3009
3137
|
throw new Error(`Column "${targetColumn}" not found on the project board.`);
|
|
3010
3138
|
}
|
|
3011
|
-
graphql(`mutation UpdateItemField(
|
|
3139
|
+
await graphql(`mutation UpdateItemField(
|
|
3012
3140
|
$projectId: ID!,
|
|
3013
3141
|
$itemId: ID!,
|
|
3014
3142
|
$fieldId: ID!,
|
|
@@ -3027,12 +3155,12 @@ var init_github_projects = __esm({
|
|
|
3027
3155
|
}`, { projectId, itemId: itemNode.id, fieldId, optionId }, this.cwd);
|
|
3028
3156
|
}
|
|
3029
3157
|
async closeIssue(issueNumber) {
|
|
3030
|
-
const repo = this.getRepo();
|
|
3031
|
-
|
|
3158
|
+
const repo = await this.getRepo();
|
|
3159
|
+
await execFileAsync2("gh", ["issue", "close", String(issueNumber), "--repo", repo], { cwd: this.cwd, encoding: "utf-8" });
|
|
3032
3160
|
}
|
|
3033
3161
|
async commentOnIssue(issueNumber, body) {
|
|
3034
|
-
const repo = this.getRepo();
|
|
3035
|
-
|
|
3162
|
+
const repo = await this.getRepo();
|
|
3163
|
+
await execFileAsync2("gh", ["issue", "comment", String(issueNumber), "--repo", repo, "--body", body], { cwd: this.cwd, encoding: "utf-8" });
|
|
3036
3164
|
}
|
|
3037
3165
|
};
|
|
3038
3166
|
}
|
|
@@ -3493,7 +3621,7 @@ function collectPrdDirs(projectPaths) {
|
|
|
3493
3621
|
const prdDirs = [];
|
|
3494
3622
|
for (const projectPath of projectPaths) {
|
|
3495
3623
|
const configPath = path3.join(projectPath, CONFIG_FILE_NAME);
|
|
3496
|
-
let prdDir = "docs/
|
|
3624
|
+
let prdDir = "docs/prds";
|
|
3497
3625
|
if (fs3.existsSync(configPath)) {
|
|
3498
3626
|
try {
|
|
3499
3627
|
const config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
@@ -4123,9 +4251,15 @@ function executorLockPath(projectDir) {
|
|
|
4123
4251
|
function reviewerLockPath(projectDir) {
|
|
4124
4252
|
return `${LOCK_FILE_PREFIX}pr-reviewer-${projectRuntimeKey(projectDir)}.lock`;
|
|
4125
4253
|
}
|
|
4254
|
+
function qaLockPath(projectDir) {
|
|
4255
|
+
return `${LOCK_FILE_PREFIX}qa-${projectRuntimeKey(projectDir)}.lock`;
|
|
4256
|
+
}
|
|
4126
4257
|
function auditLockPath(projectDir) {
|
|
4127
4258
|
return `${LOCK_FILE_PREFIX}audit-${projectRuntimeKey(projectDir)}.lock`;
|
|
4128
4259
|
}
|
|
4260
|
+
function plannerLockPath(projectDir) {
|
|
4261
|
+
return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
|
|
4262
|
+
}
|
|
4129
4263
|
function isProcessRunning(pid) {
|
|
4130
4264
|
try {
|
|
4131
4265
|
process.kill(pid, 0);
|
|
@@ -4302,24 +4436,26 @@ function collectPrdInfo(projectDir, prdDir, maxRuntime) {
|
|
|
4302
4436
|
}
|
|
4303
4437
|
return prds;
|
|
4304
4438
|
}
|
|
4305
|
-
function countOpenPRs(projectDir, branchPatterns) {
|
|
4439
|
+
async function countOpenPRs(projectDir, branchPatterns) {
|
|
4306
4440
|
try {
|
|
4307
|
-
|
|
4441
|
+
await execAsync("git rev-parse --git-dir", {
|
|
4308
4442
|
cwd: projectDir,
|
|
4309
|
-
encoding: "utf-8"
|
|
4310
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4443
|
+
encoding: "utf-8"
|
|
4311
4444
|
});
|
|
4312
4445
|
try {
|
|
4313
|
-
|
|
4446
|
+
await execAsync("which gh", { encoding: "utf-8" });
|
|
4314
4447
|
} catch {
|
|
4315
4448
|
return 0;
|
|
4316
4449
|
}
|
|
4317
|
-
const output =
|
|
4450
|
+
const { stdout: output } = await execAsync("gh pr list --state open --json headRefName,number", {
|
|
4318
4451
|
cwd: projectDir,
|
|
4319
|
-
encoding: "utf-8"
|
|
4320
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4452
|
+
encoding: "utf-8"
|
|
4321
4453
|
});
|
|
4322
|
-
const
|
|
4454
|
+
const trimmed = output.trim();
|
|
4455
|
+
if (!trimmed || trimmed === "[]") {
|
|
4456
|
+
return 0;
|
|
4457
|
+
}
|
|
4458
|
+
const prs = JSON.parse(trimmed);
|
|
4323
4459
|
const matchingPRs = prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern)));
|
|
4324
4460
|
return matchingPRs.length;
|
|
4325
4461
|
} catch {
|
|
@@ -4385,27 +4521,29 @@ function deriveReviewScore(reviewDecision) {
|
|
|
4385
4521
|
return null;
|
|
4386
4522
|
}
|
|
4387
4523
|
}
|
|
4388
|
-
function collectPrInfo(projectDir, branchPatterns) {
|
|
4524
|
+
async function collectPrInfo(projectDir, branchPatterns) {
|
|
4389
4525
|
try {
|
|
4390
|
-
|
|
4526
|
+
await execAsync("git rev-parse --git-dir", {
|
|
4391
4527
|
cwd: projectDir,
|
|
4392
|
-
encoding: "utf-8"
|
|
4393
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4528
|
+
encoding: "utf-8"
|
|
4394
4529
|
});
|
|
4395
4530
|
try {
|
|
4396
|
-
|
|
4531
|
+
await execAsync("which gh", { encoding: "utf-8" });
|
|
4397
4532
|
} catch {
|
|
4398
4533
|
return [];
|
|
4399
4534
|
}
|
|
4400
|
-
const output =
|
|
4535
|
+
const { stdout: output } = await execAsync("gh pr list --state open --json headRefName,number,title,url,statusCheckRollup,reviewDecision", {
|
|
4401
4536
|
cwd: projectDir,
|
|
4402
|
-
encoding: "utf-8"
|
|
4403
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4537
|
+
encoding: "utf-8"
|
|
4404
4538
|
});
|
|
4405
4539
|
if (process.env.DEBUG_PR_DATA === "1") {
|
|
4406
4540
|
console.error("[DEBUG] Raw gh pr list output:", output);
|
|
4407
4541
|
}
|
|
4408
|
-
const
|
|
4542
|
+
const trimmed = output.trim();
|
|
4543
|
+
if (!trimmed || trimmed === "[]") {
|
|
4544
|
+
return [];
|
|
4545
|
+
}
|
|
4546
|
+
const prs = JSON.parse(trimmed);
|
|
4409
4547
|
return prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern))).map((pr) => {
|
|
4410
4548
|
if (process.env.DEBUG_PR_DATA === "1") {
|
|
4411
4549
|
console.error(`[DEBUG] PR #${pr.number}:`);
|
|
@@ -4450,7 +4588,9 @@ function collectLogInfo(projectDir) {
|
|
|
4450
4588
|
const logEntries = [
|
|
4451
4589
|
{ name: "executor", fileName: "executor.log" },
|
|
4452
4590
|
{ name: "reviewer", fileName: "reviewer.log" },
|
|
4453
|
-
{ name: "qa", fileName: `${QA_LOG_NAME}.log` }
|
|
4591
|
+
{ name: "qa", fileName: `${QA_LOG_NAME}.log` },
|
|
4592
|
+
{ name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
|
|
4593
|
+
{ name: "planner", fileName: `${PLANNER_LOG_NAME}.log` }
|
|
4454
4594
|
];
|
|
4455
4595
|
return logEntries.map(({ name, fileName }) => {
|
|
4456
4596
|
const logPath = path5.join(projectDir, LOG_DIR, fileName);
|
|
@@ -4472,16 +4612,22 @@ function getCrontabInfo(projectName, projectDir) {
|
|
|
4472
4612
|
entries: crontabEntries
|
|
4473
4613
|
};
|
|
4474
4614
|
}
|
|
4475
|
-
function fetchStatusSnapshot(projectDir, config) {
|
|
4615
|
+
async function fetchStatusSnapshot(projectDir, config) {
|
|
4476
4616
|
const projectName = getProjectName(projectDir);
|
|
4477
4617
|
const executorLock = checkLockFile(executorLockPath(projectDir));
|
|
4478
4618
|
const reviewerLock = checkLockFile(reviewerLockPath(projectDir));
|
|
4619
|
+
const qaLock = checkLockFile(qaLockPath(projectDir));
|
|
4620
|
+
const auditLock = checkLockFile(auditLockPath(projectDir));
|
|
4621
|
+
const plannerLock = checkLockFile(plannerLockPath(projectDir));
|
|
4479
4622
|
const processes = [
|
|
4480
4623
|
{ name: "executor", running: executorLock.running, pid: executorLock.pid },
|
|
4481
|
-
{ name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid }
|
|
4624
|
+
{ name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
|
|
4625
|
+
{ name: "qa", running: qaLock.running, pid: qaLock.pid },
|
|
4626
|
+
{ name: "audit", running: auditLock.running, pid: auditLock.pid },
|
|
4627
|
+
{ name: "planner", running: plannerLock.running, pid: plannerLock.pid }
|
|
4482
4628
|
];
|
|
4483
4629
|
const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
|
|
4484
|
-
const prs = collectPrInfo(projectDir, config.branchPatterns);
|
|
4630
|
+
const prs = await collectPrInfo(projectDir, config.branchPatterns);
|
|
4485
4631
|
const logs = collectLogInfo(projectDir);
|
|
4486
4632
|
const crontab = getCrontabInfo(projectName, projectDir);
|
|
4487
4633
|
const activePrd = prds.find((p) => p.status === "in-progress")?.name ?? null;
|
|
@@ -4498,12 +4644,14 @@ function fetchStatusSnapshot(projectDir, config) {
|
|
|
4498
4644
|
timestamp: /* @__PURE__ */ new Date()
|
|
4499
4645
|
};
|
|
4500
4646
|
}
|
|
4647
|
+
var execAsync;
|
|
4501
4648
|
var init_status_data = __esm({
|
|
4502
4649
|
"../core/dist/utils/status-data.js"() {
|
|
4503
4650
|
"use strict";
|
|
4504
4651
|
init_constants();
|
|
4505
4652
|
init_prd_states();
|
|
4506
4653
|
init_crontab();
|
|
4654
|
+
execAsync = promisify3(exec);
|
|
4507
4655
|
}
|
|
4508
4656
|
});
|
|
4509
4657
|
function getLockFilePaths(projectDir) {
|
|
@@ -4622,7 +4770,7 @@ function checkGitRepo(cwd) {
|
|
|
4622
4770
|
}
|
|
4623
4771
|
function checkGhCli() {
|
|
4624
4772
|
try {
|
|
4625
|
-
|
|
4773
|
+
execSync2("gh auth status", {
|
|
4626
4774
|
encoding: "utf-8",
|
|
4627
4775
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4628
4776
|
});
|
|
@@ -4641,7 +4789,7 @@ function checkGhCli() {
|
|
|
4641
4789
|
}
|
|
4642
4790
|
function checkProviderCli(provider) {
|
|
4643
4791
|
try {
|
|
4644
|
-
|
|
4792
|
+
execSync2(`which ${provider}`, {
|
|
4645
4793
|
encoding: "utf-8",
|
|
4646
4794
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4647
4795
|
});
|
|
@@ -4662,7 +4810,7 @@ function detectProviders() {
|
|
|
4662
4810
|
const providers = [];
|
|
4663
4811
|
for (const provider of VALID_PROVIDERS) {
|
|
4664
4812
|
try {
|
|
4665
|
-
|
|
4813
|
+
execSync2(`which ${provider}`, {
|
|
4666
4814
|
encoding: "utf-8",
|
|
4667
4815
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4668
4816
|
});
|
|
@@ -4756,7 +4904,7 @@ function checkLogsDirectory(projectDir) {
|
|
|
4756
4904
|
}
|
|
4757
4905
|
function checkCrontabAccess() {
|
|
4758
4906
|
try {
|
|
4759
|
-
|
|
4907
|
+
execSync2("crontab -l", {
|
|
4760
4908
|
encoding: "utf-8",
|
|
4761
4909
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4762
4910
|
});
|
|
@@ -4906,7 +5054,7 @@ function parsePrDetails(raw) {
|
|
|
4906
5054
|
}
|
|
4907
5055
|
function fetchPrBySelector(selector, cwd) {
|
|
4908
5056
|
try {
|
|
4909
|
-
const output =
|
|
5057
|
+
const output = execFileSync("gh", ["pr", "view", selector, "--json", "number,title,url,body,additions,deletions,changedFiles"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
4910
5058
|
return parsePrDetails(output);
|
|
4911
5059
|
} catch {
|
|
4912
5060
|
return null;
|
|
@@ -4920,7 +5068,7 @@ function fetchPrDetailsByNumber(prNumber, cwd) {
|
|
|
4920
5068
|
}
|
|
4921
5069
|
function fetchPrDetails(branchPrefix, cwd) {
|
|
4922
5070
|
try {
|
|
4923
|
-
const listOutput =
|
|
5071
|
+
const listOutput = execFileSync("gh", ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "20"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
4924
5072
|
const prs = JSON.parse(listOutput);
|
|
4925
5073
|
const matching = prs.filter((pr) => pr.headRefName.startsWith(branchPrefix + "/"));
|
|
4926
5074
|
if (matching.length === 0) {
|
|
@@ -4934,7 +5082,7 @@ function fetchPrDetails(branchPrefix, cwd) {
|
|
|
4934
5082
|
}
|
|
4935
5083
|
function fetchReviewedPrDetails(branchPatterns, cwd) {
|
|
4936
5084
|
try {
|
|
4937
|
-
const listOutput =
|
|
5085
|
+
const listOutput = execFileSync("gh", ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "20"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
4938
5086
|
const prs = JSON.parse(listOutput);
|
|
4939
5087
|
const matching = prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern)));
|
|
4940
5088
|
if (matching.length === 0) {
|
|
@@ -5134,6 +5282,14 @@ function buildDescription(ctx) {
|
|
|
5134
5282
|
if (ctx.duration !== void 0) {
|
|
5135
5283
|
lines.push(`Duration: ${ctx.duration}s`);
|
|
5136
5284
|
}
|
|
5285
|
+
if (ctx.event === "review_completed" && ctx.attempts !== void 0 && ctx.attempts > 1) {
|
|
5286
|
+
const retryInfo = `Attempts: ${ctx.attempts}`;
|
|
5287
|
+
if (ctx.finalScore !== void 0) {
|
|
5288
|
+
lines.push(`${retryInfo} (final score: ${ctx.finalScore}/100)`);
|
|
5289
|
+
} else {
|
|
5290
|
+
lines.push(retryInfo);
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5137
5293
|
return lines.join("\n");
|
|
5138
5294
|
}
|
|
5139
5295
|
function escapeMarkdownV2(text) {
|
|
@@ -5188,7 +5344,7 @@ function formatDiscordPayload(ctx) {
|
|
|
5188
5344
|
}
|
|
5189
5345
|
function formatTelegramPayload(ctx) {
|
|
5190
5346
|
const emoji = getEventEmoji(ctx.event);
|
|
5191
|
-
const title = getEventTitle(ctx.event);
|
|
5347
|
+
const title = ctx.event === "run_succeeded" ? "PR Opened" : getEventTitle(ctx.event);
|
|
5192
5348
|
if (ctx.prUrl && ctx.prTitle) {
|
|
5193
5349
|
const lines = [];
|
|
5194
5350
|
lines.push(`*${escapeMarkdownV2(emoji + " " + title)}*`);
|
|
@@ -5215,6 +5371,14 @@ function formatTelegramPayload(ctx) {
|
|
|
5215
5371
|
}
|
|
5216
5372
|
lines.push(escapeMarkdownV2(stats.join(" | ")));
|
|
5217
5373
|
}
|
|
5374
|
+
if (ctx.event === "review_completed" && ctx.attempts !== void 0 && ctx.attempts > 1) {
|
|
5375
|
+
lines.push("");
|
|
5376
|
+
if (ctx.finalScore !== void 0) {
|
|
5377
|
+
lines.push(escapeMarkdownV2(`\u{1F501} Attempts: ${ctx.attempts} (final score: ${ctx.finalScore}/100)`));
|
|
5378
|
+
} else {
|
|
5379
|
+
lines.push(escapeMarkdownV2(`\u{1F501} Attempts: ${ctx.attempts}`));
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5218
5382
|
lines.push("");
|
|
5219
5383
|
lines.push(escapeMarkdownV2(`\u2699\uFE0F Project: ${ctx.projectName} | Provider: ${ctx.provider}`));
|
|
5220
5384
|
return {
|
|
@@ -5692,6 +5856,8 @@ The PRD directory is: \`{{PRD_DIR}}\`
|
|
|
5692
5856
|
|
|
5693
5857
|
## Your Task
|
|
5694
5858
|
|
|
5859
|
+
0. **Load Planner Skill** - Read and apply \`.claude/skills/prd-creator/SKILL.md\` before writing the PRD. If unavailable, continue with this template.
|
|
5860
|
+
|
|
5695
5861
|
1. **Explore the Codebase** - Read relevant existing files to understand the project structure, patterns, and conventions.
|
|
5696
5862
|
|
|
5697
5863
|
2. **Assess Complexity** - Score the complexity using the rubric and determine whether this is LOW, MEDIUM, or HIGH complexity.
|
|
@@ -5751,6 +5917,116 @@ DO NOT forget to write the file.
|
|
|
5751
5917
|
cachedTemplate = null;
|
|
5752
5918
|
}
|
|
5753
5919
|
});
|
|
5920
|
+
function normalizeAuditSeverity(raw) {
|
|
5921
|
+
const normalized = raw.trim().toLowerCase();
|
|
5922
|
+
if (normalized === "critical")
|
|
5923
|
+
return "critical";
|
|
5924
|
+
if (normalized === "high")
|
|
5925
|
+
return "high";
|
|
5926
|
+
if (normalized === "low")
|
|
5927
|
+
return "low";
|
|
5928
|
+
return "medium";
|
|
5929
|
+
}
|
|
5930
|
+
function extractAuditField(block, field) {
|
|
5931
|
+
const pattern = new RegExp(`- \\*\\*${field}\\*\\*:\\s*([\\s\\S]*?)(?=\\n- \\*\\*|\\n###\\s+Finding\\s+\\d+|$)`, "i");
|
|
5932
|
+
const match = block.match(pattern);
|
|
5933
|
+
if (!match)
|
|
5934
|
+
return "";
|
|
5935
|
+
return match[1].replace(/`/g, "").replace(/\r/g, "").trim();
|
|
5936
|
+
}
|
|
5937
|
+
function parseAuditFindings(reportContent) {
|
|
5938
|
+
const headingRegex = /^###\s+Finding\s+(\d+)\s*$/gm;
|
|
5939
|
+
const headings = [];
|
|
5940
|
+
let match;
|
|
5941
|
+
while ((match = headingRegex.exec(reportContent)) !== null) {
|
|
5942
|
+
const number = parseInt(match[1], 10);
|
|
5943
|
+
if (!Number.isNaN(number)) {
|
|
5944
|
+
headings.push({
|
|
5945
|
+
number,
|
|
5946
|
+
bodyStart: headingRegex.lastIndex,
|
|
5947
|
+
headingStart: match.index
|
|
5948
|
+
});
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
if (headings.length === 0) {
|
|
5952
|
+
return [];
|
|
5953
|
+
}
|
|
5954
|
+
const findings = [];
|
|
5955
|
+
for (let i = 0; i < headings.length; i++) {
|
|
5956
|
+
const current = headings[i];
|
|
5957
|
+
const next = headings[i + 1];
|
|
5958
|
+
const block = reportContent.slice(current.bodyStart, next?.headingStart ?? reportContent.length);
|
|
5959
|
+
const severityRaw = extractAuditField(block, "Severity");
|
|
5960
|
+
const category = extractAuditField(block, "Category") || "uncategorized";
|
|
5961
|
+
const location = extractAuditField(block, "Location") || "unknown location";
|
|
5962
|
+
const description = extractAuditField(block, "Description") || "No description provided";
|
|
5963
|
+
const suggestedFix = extractAuditField(block, "Suggested Fix") || "No suggested fix provided";
|
|
5964
|
+
findings.push({
|
|
5965
|
+
number: current.number,
|
|
5966
|
+
severity: normalizeAuditSeverity(severityRaw),
|
|
5967
|
+
category,
|
|
5968
|
+
location,
|
|
5969
|
+
description,
|
|
5970
|
+
suggestedFix
|
|
5971
|
+
});
|
|
5972
|
+
}
|
|
5973
|
+
return findings;
|
|
5974
|
+
}
|
|
5975
|
+
function auditSeverityRank(severity) {
|
|
5976
|
+
switch (severity) {
|
|
5977
|
+
case "critical":
|
|
5978
|
+
return 0;
|
|
5979
|
+
case "high":
|
|
5980
|
+
return 1;
|
|
5981
|
+
case "medium":
|
|
5982
|
+
return 2;
|
|
5983
|
+
case "low":
|
|
5984
|
+
return 3;
|
|
5985
|
+
default:
|
|
5986
|
+
return 4;
|
|
5987
|
+
}
|
|
5988
|
+
}
|
|
5989
|
+
function auditFindingToRoadmapItem(finding) {
|
|
5990
|
+
const title = `Audit Finding ${finding.number}: ${finding.category} (${finding.severity}) at ${finding.location}`;
|
|
5991
|
+
const hashSource = [
|
|
5992
|
+
finding.severity,
|
|
5993
|
+
finding.category,
|
|
5994
|
+
finding.location,
|
|
5995
|
+
finding.description,
|
|
5996
|
+
finding.suggestedFix
|
|
5997
|
+
].join("|");
|
|
5998
|
+
const hash = createHash3("sha256").update(hashSource).digest("hex").slice(0, 8);
|
|
5999
|
+
const description = [
|
|
6000
|
+
"Source: logs/audit-report.md",
|
|
6001
|
+
`Severity: ${finding.severity}`,
|
|
6002
|
+
`Category: ${finding.category}`,
|
|
6003
|
+
`Location: ${finding.location}`,
|
|
6004
|
+
"",
|
|
6005
|
+
finding.description,
|
|
6006
|
+
"",
|
|
6007
|
+
`Suggested fix: ${finding.suggestedFix}`
|
|
6008
|
+
].join("\n");
|
|
6009
|
+
return {
|
|
6010
|
+
hash,
|
|
6011
|
+
title,
|
|
6012
|
+
description,
|
|
6013
|
+
checked: false,
|
|
6014
|
+
section: "Audit Findings"
|
|
6015
|
+
};
|
|
6016
|
+
}
|
|
6017
|
+
function collectAuditPlannerItems(projectDir) {
|
|
6018
|
+
const reportPath = path12.join(projectDir, "logs", "audit-report.md");
|
|
6019
|
+
if (!fs13.existsSync(reportPath)) {
|
|
6020
|
+
return [];
|
|
6021
|
+
}
|
|
6022
|
+
const reportContent = fs13.readFileSync(reportPath, "utf-8");
|
|
6023
|
+
if (!reportContent.trim() || /\bNO_ISSUES_FOUND\b/.test(reportContent)) {
|
|
6024
|
+
return [];
|
|
6025
|
+
}
|
|
6026
|
+
const findings = parseAuditFindings(reportContent);
|
|
6027
|
+
findings.sort((a, b) => auditSeverityRank(a.severity) - auditSeverityRank(b.severity) || a.number - b.number);
|
|
6028
|
+
return findings.map(auditFindingToRoadmapItem);
|
|
6029
|
+
}
|
|
5754
6030
|
function getRoadmapStatus(projectDir, config) {
|
|
5755
6031
|
const roadmapPath = path12.join(projectDir, config.roadmapScanner.roadmapPath);
|
|
5756
6032
|
const scannerEnabled = config.roadmapScanner.enabled;
|
|
@@ -5916,15 +6192,16 @@ async function sliceNextItem(projectDir, config) {
|
|
|
5916
6192
|
};
|
|
5917
6193
|
}
|
|
5918
6194
|
const roadmapPath = path12.join(projectDir, config.roadmapScanner.roadmapPath);
|
|
5919
|
-
|
|
6195
|
+
const auditItems = collectAuditPlannerItems(projectDir);
|
|
6196
|
+
const roadmapExists = fs13.existsSync(roadmapPath);
|
|
6197
|
+
if (!roadmapExists && auditItems.length === 0) {
|
|
5920
6198
|
return {
|
|
5921
6199
|
sliced: false,
|
|
5922
6200
|
error: "ROADMAP.md not found"
|
|
5923
6201
|
};
|
|
5924
6202
|
}
|
|
5925
|
-
const
|
|
5926
|
-
|
|
5927
|
-
if (items.length === 0) {
|
|
6203
|
+
const roadmapItems = roadmapExists ? parseRoadmap(fs13.readFileSync(roadmapPath, "utf-8")) : [];
|
|
6204
|
+
if (roadmapExists && roadmapItems.length === 0 && auditItems.length === 0) {
|
|
5928
6205
|
return {
|
|
5929
6206
|
sliced: false,
|
|
5930
6207
|
error: "No items in roadmap"
|
|
@@ -5933,21 +6210,23 @@ async function sliceNextItem(projectDir, config) {
|
|
|
5933
6210
|
const prdDir = path12.join(projectDir, config.prdDir);
|
|
5934
6211
|
const state = loadRoadmapState(prdDir);
|
|
5935
6212
|
const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
6213
|
+
const pickEligibleItem = (items) => {
|
|
6214
|
+
for (const item of items) {
|
|
6215
|
+
if (item.checked) {
|
|
6216
|
+
continue;
|
|
6217
|
+
}
|
|
6218
|
+
if (isItemProcessed(state, item.hash)) {
|
|
6219
|
+
continue;
|
|
6220
|
+
}
|
|
6221
|
+
const itemSlug = slugify(item.title);
|
|
6222
|
+
if (existingPrdSlugs.has(itemSlug)) {
|
|
6223
|
+
continue;
|
|
6224
|
+
}
|
|
6225
|
+
return item;
|
|
5947
6226
|
}
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
6227
|
+
return void 0;
|
|
6228
|
+
};
|
|
6229
|
+
const targetItem = pickEligibleItem(auditItems) ?? pickEligibleItem(roadmapItems);
|
|
5951
6230
|
if (!targetItem) {
|
|
5952
6231
|
return {
|
|
5953
6232
|
sliced: false,
|
|
@@ -6046,11 +6325,11 @@ var init_script_result = __esm({
|
|
|
6046
6325
|
RESULT_PREFIX = "NIGHT_WATCH_RESULT:";
|
|
6047
6326
|
}
|
|
6048
6327
|
});
|
|
6049
|
-
async function executeScript(scriptPath, args = [], env = {}) {
|
|
6050
|
-
const result = await executeScriptWithOutput(scriptPath, args, env);
|
|
6328
|
+
async function executeScript(scriptPath, args = [], env = {}, options = {}) {
|
|
6329
|
+
const result = await executeScriptWithOutput(scriptPath, args, env, options);
|
|
6051
6330
|
return result.exitCode;
|
|
6052
6331
|
}
|
|
6053
|
-
async function executeScriptWithOutput(scriptPath, args = [], env = {}) {
|
|
6332
|
+
async function executeScriptWithOutput(scriptPath, args = [], env = {}, options = {}) {
|
|
6054
6333
|
return new Promise((resolve9, reject) => {
|
|
6055
6334
|
const childEnv = {
|
|
6056
6335
|
...process.env,
|
|
@@ -6060,6 +6339,7 @@ async function executeScriptWithOutput(scriptPath, args = [], env = {}) {
|
|
|
6060
6339
|
const stderrChunks = [];
|
|
6061
6340
|
const child = spawn2("bash", [scriptPath, ...args], {
|
|
6062
6341
|
env: childEnv,
|
|
6342
|
+
cwd: options.cwd,
|
|
6063
6343
|
stdio: ["inherit", "pipe", "pipe"]
|
|
6064
6344
|
});
|
|
6065
6345
|
child.stdout?.on("data", (data) => {
|
|
@@ -6338,6 +6618,7 @@ __export(dist_exports, {
|
|
|
6338
6618
|
DEFAULT_CRON_SCHEDULE: () => DEFAULT_CRON_SCHEDULE,
|
|
6339
6619
|
DEFAULT_CRON_SCHEDULE_OFFSET: () => DEFAULT_CRON_SCHEDULE_OFFSET,
|
|
6340
6620
|
DEFAULT_DEFAULT_BRANCH: () => DEFAULT_DEFAULT_BRANCH,
|
|
6621
|
+
DEFAULT_EXECUTOR_ENABLED: () => DEFAULT_EXECUTOR_ENABLED,
|
|
6341
6622
|
DEFAULT_FALLBACK_ON_RATE_LIMIT: () => DEFAULT_FALLBACK_ON_RATE_LIMIT,
|
|
6342
6623
|
DEFAULT_JOB_PROVIDERS: () => DEFAULT_JOB_PROVIDERS,
|
|
6343
6624
|
DEFAULT_LOCAL_BOARD_INFO: () => DEFAULT_LOCAL_BOARD_INFO,
|
|
@@ -6358,7 +6639,9 @@ __export(dist_exports, {
|
|
|
6358
6639
|
DEFAULT_QA_SCHEDULE: () => DEFAULT_QA_SCHEDULE,
|
|
6359
6640
|
DEFAULT_QA_SKIP_LABEL: () => DEFAULT_QA_SKIP_LABEL,
|
|
6360
6641
|
DEFAULT_REVIEWER_ENABLED: () => DEFAULT_REVIEWER_ENABLED,
|
|
6642
|
+
DEFAULT_REVIEWER_MAX_RETRIES: () => DEFAULT_REVIEWER_MAX_RETRIES,
|
|
6361
6643
|
DEFAULT_REVIEWER_MAX_RUNTIME: () => DEFAULT_REVIEWER_MAX_RUNTIME,
|
|
6644
|
+
DEFAULT_REVIEWER_RETRY_DELAY: () => DEFAULT_REVIEWER_RETRY_DELAY,
|
|
6362
6645
|
DEFAULT_REVIEWER_SCHEDULE: () => DEFAULT_REVIEWER_SCHEDULE,
|
|
6363
6646
|
DEFAULT_ROADMAP_SCANNER: () => DEFAULT_ROADMAP_SCANNER,
|
|
6364
6647
|
DEFAULT_SLICER_MAX_RUNTIME: () => DEFAULT_SLICER_MAX_RUNTIME,
|
|
@@ -6377,6 +6660,7 @@ __export(dist_exports, {
|
|
|
6377
6660
|
Logger: () => Logger,
|
|
6378
6661
|
MAX_HISTORY_RECORDS_PER_PRD: () => MAX_HISTORY_RECORDS_PER_PRD,
|
|
6379
6662
|
NIGHT_WATCH_LABELS: () => NIGHT_WATCH_LABELS,
|
|
6663
|
+
PLANNER_LOG_NAME: () => PLANNER_LOG_NAME,
|
|
6380
6664
|
PRD_STATES_FILE_NAME: () => PRD_STATES_FILE_NAME,
|
|
6381
6665
|
PRD_TEMPLATE: () => PRD_TEMPLATE,
|
|
6382
6666
|
PRIORITY_LABELS: () => PRIORITY_LABELS,
|
|
@@ -6505,7 +6789,9 @@ __export(dist_exports, {
|
|
|
6505
6789
|
parseRoadmap: () => parseRoadmap,
|
|
6506
6790
|
parseScriptResult: () => parseScriptResult,
|
|
6507
6791
|
performCancel: () => performCancel,
|
|
6792
|
+
plannerLockPath: () => plannerLockPath,
|
|
6508
6793
|
projectRuntimeKey: () => projectRuntimeKey,
|
|
6794
|
+
qaLockPath: () => qaLockPath,
|
|
6509
6795
|
readCrontab: () => readCrontab,
|
|
6510
6796
|
readPrdStates: () => readPrdStates,
|
|
6511
6797
|
recordExecution: () => recordExecution,
|
|
@@ -6620,7 +6906,7 @@ function detectPlaywright(cwd) {
|
|
|
6620
6906
|
return true;
|
|
6621
6907
|
}
|
|
6622
6908
|
try {
|
|
6623
|
-
|
|
6909
|
+
execSync3("playwright --version", {
|
|
6624
6910
|
cwd,
|
|
6625
6911
|
encoding: "utf-8",
|
|
6626
6912
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -6664,12 +6950,12 @@ function promptYesNo(question, defaultNo = true) {
|
|
|
6664
6950
|
function installPlaywrightForQa(cwd) {
|
|
6665
6951
|
try {
|
|
6666
6952
|
const installCmd = resolvePlaywrightInstallCommand(cwd);
|
|
6667
|
-
|
|
6953
|
+
execSync3(installCmd, {
|
|
6668
6954
|
cwd,
|
|
6669
6955
|
encoding: "utf-8",
|
|
6670
6956
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6671
6957
|
});
|
|
6672
|
-
|
|
6958
|
+
execSync3("npx playwright install chromium", {
|
|
6673
6959
|
cwd,
|
|
6674
6960
|
encoding: "utf-8",
|
|
6675
6961
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6682,7 +6968,7 @@ function installPlaywrightForQa(cwd) {
|
|
|
6682
6968
|
function getDefaultBranch(cwd) {
|
|
6683
6969
|
const getRefTimestamp = (ref) => {
|
|
6684
6970
|
try {
|
|
6685
|
-
const timestamp =
|
|
6971
|
+
const timestamp = execSync3(`git log -1 --format=%ct ${ref}`, {
|
|
6686
6972
|
encoding: "utf-8",
|
|
6687
6973
|
cwd,
|
|
6688
6974
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6716,7 +7002,7 @@ function getDefaultBranch(cwd) {
|
|
|
6716
7002
|
if (masterTimestamp !== null) {
|
|
6717
7003
|
return "master";
|
|
6718
7004
|
}
|
|
6719
|
-
const remoteRef =
|
|
7005
|
+
const remoteRef = execSync3('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo ""', {
|
|
6720
7006
|
encoding: "utf-8",
|
|
6721
7007
|
cwd,
|
|
6722
7008
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6972,7 +7258,7 @@ function initCommand(program2) {
|
|
|
6972
7258
|
} else {
|
|
6973
7259
|
let hasGitHubRemote = false;
|
|
6974
7260
|
try {
|
|
6975
|
-
const remoteUrl =
|
|
7261
|
+
const remoteUrl = execSync3("git remote get-url origin", {
|
|
6976
7262
|
cwd,
|
|
6977
7263
|
encoding: "utf-8",
|
|
6978
7264
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7034,7 +7320,7 @@ function initCommand(program2) {
|
|
|
7034
7320
|
label("Reviewer", reviewerEnabled ? "Enabled" : "Disabled");
|
|
7035
7321
|
console.log();
|
|
7036
7322
|
header("Next Steps");
|
|
7037
|
-
info(
|
|
7323
|
+
info(`1. Add your PRD files to ${prdDir}/`);
|
|
7038
7324
|
info("2. Run `night-watch install` to set up cron jobs");
|
|
7039
7325
|
info("3. Or run `night-watch run` to execute PRDs manually");
|
|
7040
7326
|
console.log();
|
|
@@ -7054,10 +7340,10 @@ function resolveRunNotificationEvent(exitCode, scriptStatus) {
|
|
|
7054
7340
|
return null;
|
|
7055
7341
|
}
|
|
7056
7342
|
function shouldAttemptCrossProjectFallback(options, scriptStatus) {
|
|
7057
|
-
if (options.
|
|
7343
|
+
if (options.crossProjectFallback !== true) {
|
|
7058
7344
|
return false;
|
|
7059
7345
|
}
|
|
7060
|
-
if (options.
|
|
7346
|
+
if (options.dryRun) {
|
|
7061
7347
|
return false;
|
|
7062
7348
|
}
|
|
7063
7349
|
if (process.env.NW_CROSS_PROJECT_FALLBACK_ACTIVE === "1") {
|
|
@@ -7132,7 +7418,7 @@ async function runCrossProjectFallback(currentProjectDir, options) {
|
|
|
7132
7418
|
const envVars = buildEnvVars(candidateConfig, options);
|
|
7133
7419
|
envVars.NW_CROSS_PROJECT_FALLBACK_ACTIVE = "1";
|
|
7134
7420
|
try {
|
|
7135
|
-
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [candidate.path], envVars);
|
|
7421
|
+
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [candidate.path], envVars, { cwd: candidate.path });
|
|
7136
7422
|
const scriptResult = parseScriptResult(`${stdout}
|
|
7137
7423
|
${stderr}`);
|
|
7138
7424
|
if (!options.dryRun) {
|
|
@@ -7251,10 +7537,14 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
|
|
|
7251
7537
|
return { pending, completed };
|
|
7252
7538
|
}
|
|
7253
7539
|
function runCommand(program2) {
|
|
7254
|
-
program2.command("run").description("Run PRD executor now").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds").option("--provider <string>", "AI provider to use (claude or codex)").option("--
|
|
7540
|
+
program2.command("run").description("Run PRD executor now").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds").option("--provider <string>", "AI provider to use (claude or codex)").option("--cross-project-fallback", "Check other registered projects when this project has no eligible work").option("--no-cross-project-fallback", "Deprecated alias; cross-project fallback is disabled by default").action(async (options) => {
|
|
7255
7541
|
const projectDir = process.cwd();
|
|
7256
7542
|
let config = loadConfig(projectDir);
|
|
7257
7543
|
config = applyCliOverrides(config, options);
|
|
7544
|
+
if (config.executorEnabled === false && !options.dryRun) {
|
|
7545
|
+
info("Executor is disabled in config; skipping run.");
|
|
7546
|
+
process.exit(0);
|
|
7547
|
+
}
|
|
7258
7548
|
const envVars = buildEnvVars(config, options);
|
|
7259
7549
|
const scriptPath = getScriptPath("night-watch-cron.sh");
|
|
7260
7550
|
if (options.dryRun) {
|
|
@@ -7344,7 +7634,7 @@ function runCommand(program2) {
|
|
|
7344
7634
|
const spinner = createSpinner("Running PRD executor...");
|
|
7345
7635
|
spinner.start();
|
|
7346
7636
|
try {
|
|
7347
|
-
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
|
|
7637
|
+
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars, { cwd: projectDir });
|
|
7348
7638
|
const scriptResult = parseScriptResult(`${stdout}
|
|
7349
7639
|
${stderr}`);
|
|
7350
7640
|
if (exitCode === 0) {
|
|
@@ -7388,6 +7678,23 @@ function parseAutoMergedPrNumbers(raw) {
|
|
|
7388
7678
|
}
|
|
7389
7679
|
return raw.split(",").map((token) => parseInt(token.trim().replace(/^#/, ""), 10)).filter((value) => !Number.isNaN(value));
|
|
7390
7680
|
}
|
|
7681
|
+
function parseRetryAttempts(raw) {
|
|
7682
|
+
if (!raw) {
|
|
7683
|
+
return 1;
|
|
7684
|
+
}
|
|
7685
|
+
const parsed = parseInt(raw, 10);
|
|
7686
|
+
return Number.isNaN(parsed) || parsed < 1 ? 1 : parsed;
|
|
7687
|
+
}
|
|
7688
|
+
function parseFinalReviewScore(raw) {
|
|
7689
|
+
if (!raw) {
|
|
7690
|
+
return void 0;
|
|
7691
|
+
}
|
|
7692
|
+
const parsed = parseInt(raw, 10);
|
|
7693
|
+
if (Number.isNaN(parsed)) {
|
|
7694
|
+
return void 0;
|
|
7695
|
+
}
|
|
7696
|
+
return parsed;
|
|
7697
|
+
}
|
|
7391
7698
|
function buildEnvVars2(config, options) {
|
|
7392
7699
|
const env = {};
|
|
7393
7700
|
const reviewerProvider = resolveJobProvider(config, "reviewer");
|
|
@@ -7396,6 +7703,8 @@ function buildEnvVars2(config, options) {
|
|
|
7396
7703
|
env.NW_DEFAULT_BRANCH = config.defaultBranch;
|
|
7397
7704
|
}
|
|
7398
7705
|
env.NW_REVIEWER_MAX_RUNTIME = String(config.reviewerMaxRuntime);
|
|
7706
|
+
env.NW_REVIEWER_MAX_RETRIES = String(config.reviewerMaxRetries);
|
|
7707
|
+
env.NW_REVIEWER_RETRY_DELAY = String(config.reviewerRetryDelay);
|
|
7399
7708
|
env.NW_MIN_REVIEW_SCORE = String(config.minReviewScore);
|
|
7400
7709
|
env.NW_BRANCH_PATTERNS = config.branchPatterns.join(",");
|
|
7401
7710
|
if (config.providerEnv) {
|
|
@@ -7408,10 +7717,6 @@ function buildEnvVars2(config, options) {
|
|
|
7408
7717
|
if (options.dryRun) {
|
|
7409
7718
|
env.NW_DRY_RUN = "1";
|
|
7410
7719
|
}
|
|
7411
|
-
if (config.autoMerge) {
|
|
7412
|
-
env.NW_AUTO_MERGE = "1";
|
|
7413
|
-
}
|
|
7414
|
-
env.NW_AUTO_MERGE_METHOD = config.autoMergeMethod;
|
|
7415
7720
|
env.NW_EXECUTION_CONTEXT = "agent";
|
|
7416
7721
|
return env;
|
|
7417
7722
|
}
|
|
@@ -7437,7 +7742,7 @@ function getOpenPrsNeedingWork(branchPatterns) {
|
|
|
7437
7742
|
for (const pattern of branchPatterns) {
|
|
7438
7743
|
args.push("--head", pattern);
|
|
7439
7744
|
}
|
|
7440
|
-
const result =
|
|
7745
|
+
const result = execFileSync2("gh", args, {
|
|
7441
7746
|
encoding: "utf-8",
|
|
7442
7747
|
stdio: ["pipe", "pipe", "pipe"]
|
|
7443
7748
|
});
|
|
@@ -7456,6 +7761,10 @@ function reviewCommand(program2) {
|
|
|
7456
7761
|
const projectDir = process.cwd();
|
|
7457
7762
|
let config = loadConfig(projectDir);
|
|
7458
7763
|
config = applyCliOverrides2(config, options);
|
|
7764
|
+
if (!config.reviewerEnabled && !options.dryRun) {
|
|
7765
|
+
info("Reviewer is disabled in config; skipping review.");
|
|
7766
|
+
process.exit(0);
|
|
7767
|
+
}
|
|
7459
7768
|
const envVars = buildEnvVars2(config, options);
|
|
7460
7769
|
const scriptPath = getScriptPath("night-watch-pr-reviewer-cron.sh");
|
|
7461
7770
|
if (options.dryRun) {
|
|
@@ -7475,6 +7784,8 @@ function reviewCommand(program2) {
|
|
|
7475
7784
|
"Auto-merge",
|
|
7476
7785
|
config.autoMerge ? `Enabled (${config.autoMergeMethod})` : "Disabled"
|
|
7477
7786
|
]);
|
|
7787
|
+
configTable.push(["Max Retry Attempts", String(config.reviewerMaxRetries)]);
|
|
7788
|
+
configTable.push(["Retry Delay", `${config.reviewerRetryDelay}s`]);
|
|
7478
7789
|
console.log(configTable.toString());
|
|
7479
7790
|
header("Open PRs Needing Work");
|
|
7480
7791
|
const openPrs = getOpenPrsNeedingWork(config.branchPatterns);
|
|
@@ -7534,6 +7845,8 @@ ${stderr}`);
|
|
|
7534
7845
|
}
|
|
7535
7846
|
}
|
|
7536
7847
|
if (!skipNotification) {
|
|
7848
|
+
const attempts = parseRetryAttempts(scriptResult?.data.attempts);
|
|
7849
|
+
const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
|
|
7537
7850
|
const _reviewCtx = {
|
|
7538
7851
|
event: "review_completed",
|
|
7539
7852
|
projectName: path15.basename(projectDir),
|
|
@@ -7545,7 +7858,9 @@ ${stderr}`);
|
|
|
7545
7858
|
prNumber: prDetails?.number,
|
|
7546
7859
|
filesChanged: prDetails?.changedFiles,
|
|
7547
7860
|
additions: prDetails?.additions,
|
|
7548
|
-
deletions: prDetails?.deletions
|
|
7861
|
+
deletions: prDetails?.deletions,
|
|
7862
|
+
attempts,
|
|
7863
|
+
finalScore
|
|
7549
7864
|
};
|
|
7550
7865
|
await sendNotifications(config, _reviewCtx);
|
|
7551
7866
|
}
|
|
@@ -7599,6 +7914,9 @@ function parseQaPrNumbers(prsRaw) {
|
|
|
7599
7914
|
}
|
|
7600
7915
|
return numbers;
|
|
7601
7916
|
}
|
|
7917
|
+
function getTelegramStatusWebhooks(config) {
|
|
7918
|
+
return (config.notifications?.webhooks ?? []).filter((wh) => wh.type === "telegram" && typeof wh.botToken === "string" && wh.botToken.trim().length > 0 && typeof wh.chatId === "string" && wh.chatId.trim().length > 0).map((wh) => ({ botToken: wh.botToken, chatId: wh.chatId }));
|
|
7919
|
+
}
|
|
7602
7920
|
function buildEnvVars3(config, options) {
|
|
7603
7921
|
const env = {};
|
|
7604
7922
|
const qaProvider = resolveJobProvider(config, "qa");
|
|
@@ -7615,6 +7933,12 @@ function buildEnvVars3(config, options) {
|
|
|
7615
7933
|
if (config.providerEnv) {
|
|
7616
7934
|
Object.assign(env, config.providerEnv);
|
|
7617
7935
|
}
|
|
7936
|
+
const telegramWebhooks = getTelegramStatusWebhooks(config);
|
|
7937
|
+
if (telegramWebhooks.length > 0) {
|
|
7938
|
+
env.NW_TELEGRAM_STATUS_WEBHOOKS = JSON.stringify(telegramWebhooks);
|
|
7939
|
+
env.NW_TELEGRAM_BOT_TOKEN = telegramWebhooks[0].botToken;
|
|
7940
|
+
env.NW_TELEGRAM_CHAT_ID = telegramWebhooks[0].chatId;
|
|
7941
|
+
}
|
|
7618
7942
|
if (options.dryRun) {
|
|
7619
7943
|
env.NW_DRY_RUN = "1";
|
|
7620
7944
|
}
|
|
@@ -7639,6 +7963,10 @@ function qaCommand(program2) {
|
|
|
7639
7963
|
const projectDir = process.cwd();
|
|
7640
7964
|
let config = loadConfig(projectDir);
|
|
7641
7965
|
config = applyCliOverrides3(config, options);
|
|
7966
|
+
if (!config.qa.enabled && !options.dryRun) {
|
|
7967
|
+
info("QA is disabled in config; skipping run.");
|
|
7968
|
+
process.exit(0);
|
|
7969
|
+
}
|
|
7642
7970
|
const envVars = buildEnvVars3(config, options);
|
|
7643
7971
|
const scriptPath = getScriptPath("night-watch-qa-cron.sh");
|
|
7644
7972
|
if (options.dryRun) {
|
|
@@ -7721,6 +8049,9 @@ ${stderr}`);
|
|
|
7721
8049
|
});
|
|
7722
8050
|
}
|
|
7723
8051
|
init_dist();
|
|
8052
|
+
function getTelegramStatusWebhooks2(config) {
|
|
8053
|
+
return (config.notifications?.webhooks ?? []).filter((wh) => wh.type === "telegram" && typeof wh.botToken === "string" && wh.botToken.trim().length > 0 && typeof wh.chatId === "string" && wh.chatId.trim().length > 0).map((wh) => ({ botToken: wh.botToken, chatId: wh.chatId }));
|
|
8054
|
+
}
|
|
7724
8055
|
function buildEnvVars4(config, options) {
|
|
7725
8056
|
const env = {};
|
|
7726
8057
|
const auditProvider = resolveJobProvider(config, "audit");
|
|
@@ -7732,6 +8063,12 @@ function buildEnvVars4(config, options) {
|
|
|
7732
8063
|
if (config.providerEnv) {
|
|
7733
8064
|
Object.assign(env, config.providerEnv);
|
|
7734
8065
|
}
|
|
8066
|
+
const telegramWebhooks = getTelegramStatusWebhooks2(config);
|
|
8067
|
+
if (telegramWebhooks.length > 0) {
|
|
8068
|
+
env.NW_TELEGRAM_STATUS_WEBHOOKS = JSON.stringify(telegramWebhooks);
|
|
8069
|
+
env.NW_TELEGRAM_BOT_TOKEN = telegramWebhooks[0].botToken;
|
|
8070
|
+
env.NW_TELEGRAM_CHAT_ID = telegramWebhooks[0].chatId;
|
|
8071
|
+
}
|
|
7735
8072
|
if (options.dryRun) {
|
|
7736
8073
|
env.NW_DRY_RUN = "1";
|
|
7737
8074
|
}
|
|
@@ -7754,6 +8091,10 @@ function auditCommand(program2) {
|
|
|
7754
8091
|
_cliProviderOverride: options.provider
|
|
7755
8092
|
};
|
|
7756
8093
|
}
|
|
8094
|
+
if (!config.audit.enabled && !options.dryRun) {
|
|
8095
|
+
info("Audit is disabled in config; skipping run.");
|
|
8096
|
+
process.exit(0);
|
|
8097
|
+
}
|
|
7757
8098
|
const envVars = buildEnvVars4(config, options);
|
|
7758
8099
|
const scriptPath = getScriptPath("night-watch-audit-cron.sh");
|
|
7759
8100
|
if (options.dryRun) {
|
|
@@ -7823,7 +8164,7 @@ function shellQuote(value) {
|
|
|
7823
8164
|
}
|
|
7824
8165
|
function getNightWatchBinPath() {
|
|
7825
8166
|
try {
|
|
7826
|
-
const npmBin =
|
|
8167
|
+
const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
|
|
7827
8168
|
const binPath = path18.join(npmBin, "night-watch");
|
|
7828
8169
|
if (fs17.existsSync(binPath)) {
|
|
7829
8170
|
return binPath;
|
|
@@ -7831,14 +8172,14 @@ function getNightWatchBinPath() {
|
|
|
7831
8172
|
} catch {
|
|
7832
8173
|
}
|
|
7833
8174
|
try {
|
|
7834
|
-
return
|
|
8175
|
+
return execSync4("which night-watch", { encoding: "utf-8" }).trim();
|
|
7835
8176
|
} catch {
|
|
7836
8177
|
return "night-watch";
|
|
7837
8178
|
}
|
|
7838
8179
|
}
|
|
7839
8180
|
function getNodeBinDir() {
|
|
7840
8181
|
try {
|
|
7841
|
-
const nodePath =
|
|
8182
|
+
const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
|
|
7842
8183
|
return path18.dirname(nodePath);
|
|
7843
8184
|
} catch {
|
|
7844
8185
|
return "";
|
|
@@ -7897,8 +8238,11 @@ function performInstall(projectDir, config, options) {
|
|
|
7897
8238
|
const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
|
|
7898
8239
|
providerEnvPrefix = exports + " && ";
|
|
7899
8240
|
}
|
|
7900
|
-
const
|
|
7901
|
-
|
|
8241
|
+
const installExecutor = config.executorEnabled !== false;
|
|
8242
|
+
if (installExecutor) {
|
|
8243
|
+
const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
|
|
8244
|
+
entries.push(executorEntry);
|
|
8245
|
+
}
|
|
7902
8246
|
const installReviewer = options?.noReviewer === true ? false : config.reviewerEnabled;
|
|
7903
8247
|
if (installReviewer) {
|
|
7904
8248
|
const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
|
|
@@ -7908,7 +8252,7 @@ function performInstall(projectDir, config, options) {
|
|
|
7908
8252
|
if (installSlicer) {
|
|
7909
8253
|
const slicerSchedule = applyScheduleOffset(config.roadmapScanner.slicerSchedule, offset);
|
|
7910
8254
|
const slicerLog = path18.join(logDir, "slicer.log");
|
|
7911
|
-
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)}
|
|
8255
|
+
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
7912
8256
|
entries.push(slicerEntry);
|
|
7913
8257
|
}
|
|
7914
8258
|
const disableQa = options?.noQa === true || options?.qa === false;
|
|
@@ -7975,8 +8319,11 @@ function installCommand(program2) {
|
|
|
7975
8319
|
const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
|
|
7976
8320
|
providerEnvPrefix = exports + " && ";
|
|
7977
8321
|
}
|
|
7978
|
-
const
|
|
7979
|
-
|
|
8322
|
+
const installExecutor = config.executorEnabled !== false;
|
|
8323
|
+
if (installExecutor) {
|
|
8324
|
+
const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
|
|
8325
|
+
entries.push(executorEntry);
|
|
8326
|
+
}
|
|
7980
8327
|
const installReviewer = options.noReviewer === true ? false : config.reviewerEnabled;
|
|
7981
8328
|
if (installReviewer) {
|
|
7982
8329
|
const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
|
|
@@ -7987,7 +8334,7 @@ function installCommand(program2) {
|
|
|
7987
8334
|
if (installSlicer) {
|
|
7988
8335
|
slicerLog = path18.join(logDir, "slicer.log");
|
|
7989
8336
|
const slicerSchedule = applyScheduleOffset(config.roadmapScanner.slicerSchedule, offset);
|
|
7990
|
-
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)}
|
|
8337
|
+
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
7991
8338
|
entries.push(slicerEntry);
|
|
7992
8339
|
}
|
|
7993
8340
|
const disableQa = options.noQa === true || options.qa === false;
|
|
@@ -8017,12 +8364,14 @@ function installCommand(program2) {
|
|
|
8017
8364
|
entries.forEach((entry) => dim(` ${entry}`));
|
|
8018
8365
|
console.log();
|
|
8019
8366
|
header("Log Files");
|
|
8020
|
-
|
|
8367
|
+
if (installExecutor) {
|
|
8368
|
+
dim(` Executor: ${executorLog}`);
|
|
8369
|
+
}
|
|
8021
8370
|
if (installReviewer) {
|
|
8022
8371
|
dim(` Reviewer: ${reviewerLog}`);
|
|
8023
8372
|
}
|
|
8024
8373
|
if (installSlicer && slicerLog) {
|
|
8025
|
-
dim(`
|
|
8374
|
+
dim(` Planner: ${slicerLog}`);
|
|
8026
8375
|
}
|
|
8027
8376
|
if (installQa && qaLog) {
|
|
8028
8377
|
dim(` QA: ${qaLog}`);
|
|
@@ -8141,11 +8490,17 @@ function statusCommand(program2) {
|
|
|
8141
8490
|
try {
|
|
8142
8491
|
const projectDir = process.cwd();
|
|
8143
8492
|
const config = loadConfig(projectDir);
|
|
8144
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
8493
|
+
const snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
8145
8494
|
const executorProc = snapshot.processes.find((p) => p.name === "executor");
|
|
8146
8495
|
const reviewerProc = snapshot.processes.find((p) => p.name === "reviewer");
|
|
8496
|
+
const qaProc = snapshot.processes.find((p) => p.name === "qa");
|
|
8497
|
+
const auditProc = snapshot.processes.find((p) => p.name === "audit");
|
|
8498
|
+
const plannerProc = snapshot.processes.find((p) => p.name === "planner");
|
|
8147
8499
|
const executorLog = snapshot.logs.find((l) => l.name === "executor");
|
|
8148
8500
|
const reviewerLog = snapshot.logs.find((l) => l.name === "reviewer");
|
|
8501
|
+
const qaLog = snapshot.logs.find((l) => l.name === "qa");
|
|
8502
|
+
const auditLog = snapshot.logs.find((l) => l.name === "audit");
|
|
8503
|
+
const plannerLog = snapshot.logs.find((l) => l.name === "planner");
|
|
8149
8504
|
const pendingPrds = snapshot.prds.filter((p) => p.status === "ready" || p.status === "blocked").length;
|
|
8150
8505
|
const claimedPrds = snapshot.prds.filter((p) => p.status === "in-progress").length;
|
|
8151
8506
|
const donePrds = snapshot.prds.filter((p) => p.status === "done").length;
|
|
@@ -8158,6 +8513,9 @@ function statusCommand(program2) {
|
|
|
8158
8513
|
autoMergeMethod: config.autoMergeMethod,
|
|
8159
8514
|
executor: { running: executorProc?.running ?? false, pid: executorProc?.pid ?? null },
|
|
8160
8515
|
reviewer: { running: reviewerProc?.running ?? false, pid: reviewerProc?.pid ?? null },
|
|
8516
|
+
qa: { running: qaProc?.running ?? false, pid: qaProc?.pid ?? null },
|
|
8517
|
+
audit: { running: auditProc?.running ?? false, pid: auditProc?.pid ?? null },
|
|
8518
|
+
planner: { running: plannerProc?.running ?? false, pid: plannerProc?.pid ?? null },
|
|
8161
8519
|
prds: { pending: pendingPrds, claimed: claimedPrds, done: donePrds },
|
|
8162
8520
|
prs: { open: snapshot.prs.length },
|
|
8163
8521
|
crontab: snapshot.crontab,
|
|
@@ -8173,6 +8531,24 @@ function statusCommand(program2) {
|
|
|
8173
8531
|
lastLines: reviewerLog.lastLines,
|
|
8174
8532
|
exists: reviewerLog.exists,
|
|
8175
8533
|
size: reviewerLog.size
|
|
8534
|
+
} : void 0,
|
|
8535
|
+
qa: qaLog ? {
|
|
8536
|
+
path: qaLog.path,
|
|
8537
|
+
lastLines: qaLog.lastLines,
|
|
8538
|
+
exists: qaLog.exists,
|
|
8539
|
+
size: qaLog.size
|
|
8540
|
+
} : void 0,
|
|
8541
|
+
audit: auditLog ? {
|
|
8542
|
+
path: auditLog.path,
|
|
8543
|
+
lastLines: auditLog.lastLines,
|
|
8544
|
+
exists: auditLog.exists,
|
|
8545
|
+
size: auditLog.size
|
|
8546
|
+
} : void 0,
|
|
8547
|
+
planner: plannerLog ? {
|
|
8548
|
+
path: plannerLog.path,
|
|
8549
|
+
lastLines: plannerLog.lastLines,
|
|
8550
|
+
exists: plannerLog.exists,
|
|
8551
|
+
size: plannerLog.size
|
|
8176
8552
|
} : void 0
|
|
8177
8553
|
}
|
|
8178
8554
|
};
|
|
@@ -8204,6 +8580,12 @@ function statusCommand(program2) {
|
|
|
8204
8580
|
"Reviewer",
|
|
8205
8581
|
formatRunningStatus(status.reviewer.running, status.reviewer.pid)
|
|
8206
8582
|
]);
|
|
8583
|
+
processTable.push(["QA", formatRunningStatus(status.qa.running, status.qa.pid)]);
|
|
8584
|
+
processTable.push(["Audit", formatRunningStatus(status.audit.running, status.audit.pid)]);
|
|
8585
|
+
processTable.push([
|
|
8586
|
+
"Planner",
|
|
8587
|
+
formatRunningStatus(status.planner.running, status.planner.pid)
|
|
8588
|
+
]);
|
|
8207
8589
|
console.log(processTable.toString());
|
|
8208
8590
|
header("PRD Status");
|
|
8209
8591
|
const prdTable = createTable({ head: ["Status", "Count"] });
|
|
@@ -8239,6 +8621,27 @@ function statusCommand(program2) {
|
|
|
8239
8621
|
status.logs.reviewer.exists ? "Exists" : "Not found"
|
|
8240
8622
|
]);
|
|
8241
8623
|
}
|
|
8624
|
+
if (status.logs.qa) {
|
|
8625
|
+
logTable.push([
|
|
8626
|
+
"QA",
|
|
8627
|
+
status.logs.qa.exists ? formatBytes(status.logs.qa.size) : "-",
|
|
8628
|
+
status.logs.qa.exists ? "Exists" : "Not found"
|
|
8629
|
+
]);
|
|
8630
|
+
}
|
|
8631
|
+
if (status.logs.audit) {
|
|
8632
|
+
logTable.push([
|
|
8633
|
+
"Audit",
|
|
8634
|
+
status.logs.audit.exists ? formatBytes(status.logs.audit.size) : "-",
|
|
8635
|
+
status.logs.audit.exists ? "Exists" : "Not found"
|
|
8636
|
+
]);
|
|
8637
|
+
}
|
|
8638
|
+
if (status.logs.planner) {
|
|
8639
|
+
logTable.push([
|
|
8640
|
+
"Planner",
|
|
8641
|
+
status.logs.planner.exists ? formatBytes(status.logs.planner.size) : "-",
|
|
8642
|
+
status.logs.planner.exists ? "Exists" : "Not found"
|
|
8643
|
+
]);
|
|
8644
|
+
}
|
|
8242
8645
|
console.log(logTable.toString());
|
|
8243
8646
|
if (options.verbose) {
|
|
8244
8647
|
if (status.logs.executor?.exists && status.logs.executor.lastLines.length > 0) {
|
|
@@ -8249,12 +8652,27 @@ function statusCommand(program2) {
|
|
|
8249
8652
|
dim(" Reviewer last 5 lines:");
|
|
8250
8653
|
status.logs.reviewer.lastLines.forEach((line) => dim(` ${line}`));
|
|
8251
8654
|
}
|
|
8655
|
+
if (status.logs.qa?.exists && status.logs.qa.lastLines.length > 0) {
|
|
8656
|
+
dim(" QA last 5 lines:");
|
|
8657
|
+
status.logs.qa.lastLines.forEach((line) => dim(` ${line}`));
|
|
8658
|
+
}
|
|
8659
|
+
if (status.logs.audit?.exists && status.logs.audit.lastLines.length > 0) {
|
|
8660
|
+
dim(" Audit last 5 lines:");
|
|
8661
|
+
status.logs.audit.lastLines.forEach((line) => dim(` ${line}`));
|
|
8662
|
+
}
|
|
8663
|
+
if (status.logs.planner?.exists && status.logs.planner.lastLines.length > 0) {
|
|
8664
|
+
dim(" Planner last 5 lines:");
|
|
8665
|
+
status.logs.planner.lastLines.forEach((line) => dim(` ${line}`));
|
|
8666
|
+
}
|
|
8252
8667
|
}
|
|
8253
8668
|
header("Commands");
|
|
8254
8669
|
dim(" night-watch install - Install crontab entries");
|
|
8255
8670
|
dim(" night-watch logs - View logs");
|
|
8256
8671
|
dim(" night-watch run - Run executor now");
|
|
8257
8672
|
dim(" night-watch review - Run reviewer now");
|
|
8673
|
+
dim(" night-watch qa - Run QA now");
|
|
8674
|
+
dim(" night-watch audit - Run audit now");
|
|
8675
|
+
dim(" night-watch planner - Run planner now");
|
|
8258
8676
|
console.log();
|
|
8259
8677
|
} catch (error2) {
|
|
8260
8678
|
console.error(`Error getting status: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
@@ -8293,22 +8711,36 @@ function followLog(filePath) {
|
|
|
8293
8711
|
});
|
|
8294
8712
|
}
|
|
8295
8713
|
function logsCommand(program2) {
|
|
8296
|
-
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 (
|
|
8714
|
+
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) => {
|
|
8297
8715
|
try {
|
|
8298
8716
|
const projectDir = process.cwd();
|
|
8299
8717
|
const logDir = path20.join(projectDir, LOG_DIR);
|
|
8300
8718
|
const lineCount = parseInt(options.lines || "50", 10);
|
|
8301
8719
|
const executorLog = path20.join(logDir, EXECUTOR_LOG_FILE);
|
|
8302
8720
|
const reviewerLog = path20.join(logDir, REVIEWER_LOG_FILE);
|
|
8721
|
+
const qaLog = path20.join(logDir, `${QA_LOG_NAME}.log`);
|
|
8722
|
+
const auditLog = path20.join(logDir, `${AUDIT_LOG_NAME}.log`);
|
|
8723
|
+
const plannerLog = path20.join(logDir, `${PLANNER_LOG_NAME}.log`);
|
|
8303
8724
|
const logType = options.type?.toLowerCase() || "all";
|
|
8304
8725
|
const showExecutor = logType === "all" || logType === "run" || logType === "executor";
|
|
8305
8726
|
const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
|
|
8727
|
+
const showQa = logType === "all" || logType === "qa";
|
|
8728
|
+
const showAudit = logType === "all" || logType === "audit";
|
|
8729
|
+
const showPlanner = logType === "all" || logType === "planner" || logType === "slice" || logType === "slicer";
|
|
8306
8730
|
if (options.follow) {
|
|
8307
8731
|
if (logType === "all") {
|
|
8308
8732
|
dim("Note: Following all logs is not supported. Showing executor log.");
|
|
8309
|
-
dim("Use --type
|
|
8310
|
-
}
|
|
8311
|
-
|
|
8733
|
+
dim("Use --type reviewer|qa|audit|planner for other logs.\n");
|
|
8734
|
+
}
|
|
8735
|
+
let targetLog = executorLog;
|
|
8736
|
+
if (showReviewer)
|
|
8737
|
+
targetLog = reviewerLog;
|
|
8738
|
+
else if (showQa)
|
|
8739
|
+
targetLog = qaLog;
|
|
8740
|
+
else if (showAudit)
|
|
8741
|
+
targetLog = auditLog;
|
|
8742
|
+
else if (showPlanner)
|
|
8743
|
+
targetLog = plannerLog;
|
|
8312
8744
|
followLog(targetLog);
|
|
8313
8745
|
return;
|
|
8314
8746
|
}
|
|
@@ -8325,10 +8757,28 @@ function logsCommand(program2) {
|
|
|
8325
8757
|
console.log();
|
|
8326
8758
|
console.log(getLastLines(reviewerLog, lineCount));
|
|
8327
8759
|
}
|
|
8760
|
+
if (showQa) {
|
|
8761
|
+
header("QA Log");
|
|
8762
|
+
dim(`File: ${qaLog}`);
|
|
8763
|
+
console.log();
|
|
8764
|
+
console.log(getLastLines(qaLog, lineCount));
|
|
8765
|
+
}
|
|
8766
|
+
if (showAudit) {
|
|
8767
|
+
header("Audit Log");
|
|
8768
|
+
dim(`File: ${auditLog}`);
|
|
8769
|
+
console.log();
|
|
8770
|
+
console.log(getLastLines(auditLog, lineCount));
|
|
8771
|
+
}
|
|
8772
|
+
if (showPlanner) {
|
|
8773
|
+
header("Planner Log");
|
|
8774
|
+
dim(`File: ${plannerLog}`);
|
|
8775
|
+
console.log();
|
|
8776
|
+
console.log(getLastLines(plannerLog, lineCount));
|
|
8777
|
+
}
|
|
8328
8778
|
console.log();
|
|
8329
8779
|
dim("---");
|
|
8330
8780
|
dim("Tip: Use -f to follow logs in real-time");
|
|
8331
|
-
dim(" Use --type
|
|
8781
|
+
dim(" Use --type executor|reviewer|qa|audit|planner to view specific logs");
|
|
8332
8782
|
} catch (err) {
|
|
8333
8783
|
console.error(`Error reading logs: ${err instanceof Error ? err.message : String(err)}`);
|
|
8334
8784
|
process.exit(1);
|
|
@@ -9841,10 +10291,14 @@ function createSchedulesTab() {
|
|
|
9841
10291
|
ctx.showMessage(`Saved but cron install failed: ${installResult.error}`, "error");
|
|
9842
10292
|
}
|
|
9843
10293
|
ctx.config = newConfig;
|
|
9844
|
-
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
10294
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10295
|
+
ctx.snapshot = snap;
|
|
10296
|
+
renderCrontab(ctx);
|
|
10297
|
+
renderScheduleSettings(ctx);
|
|
10298
|
+
}).catch(() => {
|
|
10299
|
+
renderCrontab(ctx);
|
|
10300
|
+
renderScheduleSettings(ctx);
|
|
10301
|
+
});
|
|
9848
10302
|
}
|
|
9849
10303
|
function showCustomCronInput(ctx, field, label2) {
|
|
9850
10304
|
const currentValue = ctx.config[field];
|
|
@@ -9942,10 +10396,13 @@ function createSchedulesTab() {
|
|
|
9942
10396
|
} else {
|
|
9943
10397
|
ctx.showMessage(`Install failed: ${result.error}`, "error");
|
|
9944
10398
|
}
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
10399
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10400
|
+
ctx.snapshot = snap;
|
|
10401
|
+
renderCrontab(ctx);
|
|
10402
|
+
ctx.screen.render();
|
|
10403
|
+
}).catch(() => {
|
|
10404
|
+
ctx.screen.render();
|
|
10405
|
+
});
|
|
9949
10406
|
}
|
|
9950
10407
|
],
|
|
9951
10408
|
[
|
|
@@ -9957,10 +10414,13 @@ function createSchedulesTab() {
|
|
|
9957
10414
|
} else {
|
|
9958
10415
|
ctx.showMessage(`Uninstall failed: ${result.error}`, "error");
|
|
9959
10416
|
}
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
10417
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10418
|
+
ctx.snapshot = snap;
|
|
10419
|
+
renderCrontab(ctx);
|
|
10420
|
+
ctx.screen.render();
|
|
10421
|
+
}).catch(() => {
|
|
10422
|
+
ctx.screen.render();
|
|
10423
|
+
});
|
|
9964
10424
|
}
|
|
9965
10425
|
],
|
|
9966
10426
|
[
|
|
@@ -9973,10 +10433,13 @@ function createSchedulesTab() {
|
|
|
9973
10433
|
} else {
|
|
9974
10434
|
ctx.showMessage(`Reinstall failed: ${result.error}`, "error");
|
|
9975
10435
|
}
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
10436
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10437
|
+
ctx.snapshot = snap;
|
|
10438
|
+
renderCrontab(ctx);
|
|
10439
|
+
ctx.screen.render();
|
|
10440
|
+
}).catch(() => {
|
|
10441
|
+
ctx.screen.render();
|
|
10442
|
+
});
|
|
9980
10443
|
}
|
|
9981
10444
|
]
|
|
9982
10445
|
];
|
|
@@ -10104,9 +10567,12 @@ ${result.entries.map((e) => ` ${e}`).join("\n")}`);
|
|
|
10104
10567
|
outputBox.setContent(`{red-fg}Install failed: ${result.error}{/red-fg}`);
|
|
10105
10568
|
ctx.showMessage("Install failed", "error");
|
|
10106
10569
|
}
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10570
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10571
|
+
ctx.snapshot = snap;
|
|
10572
|
+
ctx.screen.render();
|
|
10573
|
+
}).catch(() => {
|
|
10574
|
+
ctx.screen.render();
|
|
10575
|
+
});
|
|
10110
10576
|
}
|
|
10111
10577
|
},
|
|
10112
10578
|
{
|
|
@@ -10122,9 +10588,12 @@ Removed ${result.removedCount} entries.`);
|
|
|
10122
10588
|
outputBox.setContent(`{red-fg}Uninstall failed: ${result.error}{/red-fg}`);
|
|
10123
10589
|
ctx.showMessage("Uninstall failed", "error");
|
|
10124
10590
|
}
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10591
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10592
|
+
ctx.snapshot = snap;
|
|
10593
|
+
ctx.screen.render();
|
|
10594
|
+
}).catch(() => {
|
|
10595
|
+
ctx.screen.render();
|
|
10596
|
+
});
|
|
10128
10597
|
}
|
|
10129
10598
|
},
|
|
10130
10599
|
{
|
|
@@ -10517,7 +10986,7 @@ function dashboardCommand(program2) {
|
|
|
10517
10986
|
}
|
|
10518
10987
|
let activeTabIndex = 0;
|
|
10519
10988
|
let isEditing = false;
|
|
10520
|
-
let snapshot = fetchStatusSnapshot(projectDir, config);
|
|
10989
|
+
let snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
10521
10990
|
const ctx = {
|
|
10522
10991
|
screen,
|
|
10523
10992
|
projectDir,
|
|
@@ -10528,8 +10997,8 @@ function dashboardCommand(program2) {
|
|
|
10528
10997
|
ctx.config = config;
|
|
10529
10998
|
return config;
|
|
10530
10999
|
},
|
|
10531
|
-
refreshSnapshot: () => {
|
|
10532
|
-
snapshot = fetchStatusSnapshot(projectDir, config);
|
|
11000
|
+
refreshSnapshot: async () => {
|
|
11001
|
+
snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
10533
11002
|
ctx.snapshot = snapshot;
|
|
10534
11003
|
return snapshot;
|
|
10535
11004
|
},
|
|
@@ -10573,10 +11042,10 @@ function dashboardCommand(program2) {
|
|
|
10573
11042
|
function updateHeader() {
|
|
10574
11043
|
headerBox.setContent(`{center}Night Watch: ${snapshot.projectName} | Provider: ${config.provider} | Last: ${snapshot.timestamp.toLocaleTimeString()} | Next: ${countdown}s{/center}`);
|
|
10575
11044
|
}
|
|
10576
|
-
function refreshData() {
|
|
11045
|
+
async function refreshData() {
|
|
10577
11046
|
config = loadConfig(projectDir);
|
|
10578
11047
|
ctx.config = config;
|
|
10579
|
-
snapshot = fetchStatusSnapshot(projectDir, config);
|
|
11048
|
+
snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
10580
11049
|
ctx.snapshot = snapshot;
|
|
10581
11050
|
countdown = intervalSeconds;
|
|
10582
11051
|
updateHeader();
|
|
@@ -10588,7 +11057,8 @@ function dashboardCommand(program2) {
|
|
|
10588
11057
|
updateHeader();
|
|
10589
11058
|
screen.render();
|
|
10590
11059
|
if (countdown <= 0) {
|
|
10591
|
-
refreshData()
|
|
11060
|
+
refreshData().catch(() => {
|
|
11061
|
+
});
|
|
10592
11062
|
}
|
|
10593
11063
|
}, 1e3);
|
|
10594
11064
|
screen.key(["q", "escape"], () => {
|
|
@@ -10604,7 +11074,8 @@ function dashboardCommand(program2) {
|
|
|
10604
11074
|
screen.key(["r"], () => {
|
|
10605
11075
|
if (isEditing)
|
|
10606
11076
|
return;
|
|
10607
|
-
refreshData()
|
|
11077
|
+
refreshData().catch(() => {
|
|
11078
|
+
});
|
|
10608
11079
|
});
|
|
10609
11080
|
for (let i = 0; i < tabs.length; i++) {
|
|
10610
11081
|
const idx = i;
|
|
@@ -10903,8 +11374,7 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
|
|
|
10903
11374
|
return setInterval(() => {
|
|
10904
11375
|
if (clients.size === 0)
|
|
10905
11376
|
return;
|
|
10906
|
-
|
|
10907
|
-
const snapshot = fetchStatusSnapshot(projectDir, getConfig());
|
|
11377
|
+
fetchStatusSnapshot(projectDir, getConfig()).then((snapshot) => {
|
|
10908
11378
|
const hash = JSON.stringify({
|
|
10909
11379
|
processes: snapshot.processes,
|
|
10910
11380
|
prds: snapshot.prds.map((p) => ({ n: p.name, s: p.status }))
|
|
@@ -10913,8 +11383,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
|
|
|
10913
11383
|
lastSnapshotHash = hash;
|
|
10914
11384
|
broadcastSSE(clients, "status_changed", snapshot);
|
|
10915
11385
|
}
|
|
10916
|
-
}
|
|
10917
|
-
}
|
|
11386
|
+
}).catch(() => {
|
|
11387
|
+
});
|
|
10918
11388
|
}, 2e3);
|
|
10919
11389
|
}
|
|
10920
11390
|
init_dist();
|
|
@@ -10985,11 +11455,17 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
|
|
|
10985
11455
|
lockPath = executorLockPath(projectDir);
|
|
10986
11456
|
} else if (command[0] === "review") {
|
|
10987
11457
|
lockPath = reviewerLockPath(projectDir);
|
|
11458
|
+
} else if (command[0] === "planner") {
|
|
11459
|
+
lockPath = plannerLockPath(projectDir);
|
|
10988
11460
|
}
|
|
10989
11461
|
if (lockPath) {
|
|
10990
11462
|
const lock = checkLockFile(lockPath);
|
|
10991
11463
|
if (lock.running) {
|
|
10992
|
-
|
|
11464
|
+
let processType = "Planner";
|
|
11465
|
+
if (command[0] === "run")
|
|
11466
|
+
processType = "Executor";
|
|
11467
|
+
else if (command[0] === "review")
|
|
11468
|
+
processType = "Reviewer";
|
|
10993
11469
|
res.status(409).json({
|
|
10994
11470
|
error: `${processType} is already running (PID ${lock.pid})`,
|
|
10995
11471
|
pid: lock.pid
|
|
@@ -11033,25 +11509,36 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
|
|
|
11033
11509
|
});
|
|
11034
11510
|
}
|
|
11035
11511
|
}
|
|
11036
|
-
function
|
|
11037
|
-
const {
|
|
11038
|
-
const
|
|
11039
|
-
router.post(
|
|
11512
|
+
function createActionRouteHandlers(ctx) {
|
|
11513
|
+
const router = Router({ mergeParams: true });
|
|
11514
|
+
const p = ctx.pathPrefix;
|
|
11515
|
+
router.post(`/${p}run`, (req, res) => {
|
|
11516
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11040
11517
|
spawnAction2(projectDir, ["run"], req, res, (pid) => {
|
|
11041
|
-
broadcastSSE(
|
|
11518
|
+
broadcastSSE(ctx.getSseClients(req), "executor_started", { pid });
|
|
11042
11519
|
});
|
|
11043
11520
|
});
|
|
11044
|
-
router.post(
|
|
11045
|
-
spawnAction2(
|
|
11521
|
+
router.post(`/${p}review`, (req, res) => {
|
|
11522
|
+
spawnAction2(ctx.getProjectDir(req), ["review"], req, res);
|
|
11046
11523
|
});
|
|
11047
|
-
router.post(
|
|
11048
|
-
spawnAction2(
|
|
11524
|
+
router.post(`/${p}qa`, (req, res) => {
|
|
11525
|
+
spawnAction2(ctx.getProjectDir(req), ["qa"], req, res);
|
|
11049
11526
|
});
|
|
11050
|
-
router.post(
|
|
11051
|
-
spawnAction2(
|
|
11527
|
+
router.post(`/${p}audit`, (req, res) => {
|
|
11528
|
+
spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
|
|
11052
11529
|
});
|
|
11053
|
-
router.post(
|
|
11530
|
+
router.post(`/${p}planner`, (req, res) => {
|
|
11531
|
+
spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
|
|
11532
|
+
});
|
|
11533
|
+
router.post(`/${p}install-cron`, (req, res) => {
|
|
11534
|
+
spawnAction2(ctx.getProjectDir(req), ["install"], req, res);
|
|
11535
|
+
});
|
|
11536
|
+
router.post(`/${p}uninstall-cron`, (req, res) => {
|
|
11537
|
+
spawnAction2(ctx.getProjectDir(req), ["uninstall"], req, res);
|
|
11538
|
+
});
|
|
11539
|
+
router.post(`/${p}cancel`, async (req, res) => {
|
|
11054
11540
|
try {
|
|
11541
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11055
11542
|
const { type = "all" } = req.body;
|
|
11056
11543
|
const validTypes = ["run", "review", "all"];
|
|
11057
11544
|
if (!validTypes.includes(type)) {
|
|
@@ -11072,9 +11559,10 @@ function createActionRoutes(deps) {
|
|
|
11072
11559
|
});
|
|
11073
11560
|
}
|
|
11074
11561
|
});
|
|
11075
|
-
router.post(
|
|
11562
|
+
router.post(`/${p}retry`, (req, res) => {
|
|
11076
11563
|
try {
|
|
11077
|
-
const
|
|
11564
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11565
|
+
const config = ctx.getConfig(req);
|
|
11078
11566
|
const { prdName } = req.body;
|
|
11079
11567
|
if (!prdName || typeof prdName !== "string") {
|
|
11080
11568
|
res.status(400).json({ error: "prdName is required" });
|
|
@@ -11104,9 +11592,10 @@ function createActionRoutes(deps) {
|
|
|
11104
11592
|
});
|
|
11105
11593
|
}
|
|
11106
11594
|
});
|
|
11107
|
-
router.post(
|
|
11595
|
+
router.post(`/${p}clear-lock`, async (req, res) => {
|
|
11108
11596
|
try {
|
|
11109
|
-
const
|
|
11597
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11598
|
+
const config = ctx.getConfig(req);
|
|
11110
11599
|
const lockPath = executorLockPath(projectDir);
|
|
11111
11600
|
const lock = checkLockFile(lockPath);
|
|
11112
11601
|
if (lock.running) {
|
|
@@ -11120,7 +11609,7 @@ function createActionRoutes(deps) {
|
|
|
11120
11609
|
if (fs24.existsSync(prdDir)) {
|
|
11121
11610
|
cleanOrphanedClaims(prdDir);
|
|
11122
11611
|
}
|
|
11123
|
-
broadcastSSE(
|
|
11612
|
+
broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
|
|
11124
11613
|
res.json({ cleared: true });
|
|
11125
11614
|
} catch (error2) {
|
|
11126
11615
|
res.status(500).json({
|
|
@@ -11130,110 +11619,28 @@ function createActionRoutes(deps) {
|
|
|
11130
11619
|
});
|
|
11131
11620
|
return router;
|
|
11132
11621
|
}
|
|
11622
|
+
function createActionRoutes(deps) {
|
|
11623
|
+
return createActionRouteHandlers({
|
|
11624
|
+
getConfig: () => deps.getConfig(),
|
|
11625
|
+
getProjectDir: () => deps.projectDir,
|
|
11626
|
+
getSseClients: () => deps.sseClients,
|
|
11627
|
+
pathPrefix: ""
|
|
11628
|
+
});
|
|
11629
|
+
}
|
|
11133
11630
|
function createProjectActionRoutes(deps) {
|
|
11134
11631
|
const { projectSseClients } = deps;
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
const clients = projectSseClients.get(projectDir);
|
|
11140
|
-
if (clients) {
|
|
11141
|
-
broadcastSSE(clients, "executor_started", { pid });
|
|
11142
|
-
}
|
|
11143
|
-
});
|
|
11144
|
-
});
|
|
11145
|
-
router.post("/actions/review", (req, res) => {
|
|
11146
|
-
spawnAction2(req.projectDir, ["review"], req, res);
|
|
11147
|
-
});
|
|
11148
|
-
router.post("/actions/install-cron", (req, res) => {
|
|
11149
|
-
spawnAction2(req.projectDir, ["install"], req, res);
|
|
11150
|
-
});
|
|
11151
|
-
router.post("/actions/uninstall-cron", (req, res) => {
|
|
11152
|
-
spawnAction2(req.projectDir, ["uninstall"], req, res);
|
|
11153
|
-
});
|
|
11154
|
-
router.post("/actions/cancel", async (req, res) => {
|
|
11155
|
-
try {
|
|
11632
|
+
return createActionRouteHandlers({
|
|
11633
|
+
getConfig: (req) => req.projectConfig,
|
|
11634
|
+
getProjectDir: (req) => req.projectDir,
|
|
11635
|
+
getSseClients: (req) => {
|
|
11156
11636
|
const projectDir = req.projectDir;
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
if (!validTypes.includes(type)) {
|
|
11160
|
-
res.status(400).json({
|
|
11161
|
-
error: `Invalid type. Must be one of: ${validTypes.join(", ")}`
|
|
11162
|
-
});
|
|
11163
|
-
return;
|
|
11637
|
+
if (!projectSseClients.has(projectDir)) {
|
|
11638
|
+
projectSseClients.set(projectDir, /* @__PURE__ */ new Set());
|
|
11164
11639
|
}
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
});
|
|
11169
|
-
const hasFailure = results.some((r) => !r.success);
|
|
11170
|
-
res.status(hasFailure ? 500 : 200).json({ results });
|
|
11171
|
-
} catch (error2) {
|
|
11172
|
-
res.status(500).json({
|
|
11173
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11174
|
-
});
|
|
11175
|
-
}
|
|
11176
|
-
});
|
|
11177
|
-
router.post("/actions/retry", (req, res) => {
|
|
11178
|
-
try {
|
|
11179
|
-
const projectDir = req.projectDir;
|
|
11180
|
-
const config = req.projectConfig;
|
|
11181
|
-
const { prdName } = req.body;
|
|
11182
|
-
if (!prdName || typeof prdName !== "string") {
|
|
11183
|
-
res.status(400).json({ error: "prdName is required" });
|
|
11184
|
-
return;
|
|
11185
|
-
}
|
|
11186
|
-
if (!validatePrdName(prdName)) {
|
|
11187
|
-
res.status(400).json({ error: "Invalid PRD name" });
|
|
11188
|
-
return;
|
|
11189
|
-
}
|
|
11190
|
-
const prdDir = path24.join(projectDir, config.prdDir);
|
|
11191
|
-
const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
|
|
11192
|
-
const pendingPath = path24.join(prdDir, normalized);
|
|
11193
|
-
const donePath = path24.join(prdDir, "done", normalized);
|
|
11194
|
-
if (fs24.existsSync(pendingPath)) {
|
|
11195
|
-
res.json({ message: `"${normalized}" is already pending` });
|
|
11196
|
-
return;
|
|
11197
|
-
}
|
|
11198
|
-
if (!fs24.existsSync(donePath)) {
|
|
11199
|
-
res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
|
|
11200
|
-
return;
|
|
11201
|
-
}
|
|
11202
|
-
fs24.renameSync(donePath, pendingPath);
|
|
11203
|
-
res.json({ message: `Moved "${normalized}" back to pending` });
|
|
11204
|
-
} catch (error2) {
|
|
11205
|
-
res.status(500).json({
|
|
11206
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11207
|
-
});
|
|
11208
|
-
}
|
|
11209
|
-
});
|
|
11210
|
-
router.post("/actions/clear-lock", (req, res) => {
|
|
11211
|
-
try {
|
|
11212
|
-
const projectDir = req.projectDir;
|
|
11213
|
-
const config = req.projectConfig;
|
|
11214
|
-
const lockPath = executorLockPath(projectDir);
|
|
11215
|
-
const lock = checkLockFile(lockPath);
|
|
11216
|
-
if (lock.running) {
|
|
11217
|
-
res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
|
|
11218
|
-
return;
|
|
11219
|
-
}
|
|
11220
|
-
if (fs24.existsSync(lockPath)) {
|
|
11221
|
-
fs24.unlinkSync(lockPath);
|
|
11222
|
-
}
|
|
11223
|
-
const prdDir = path24.join(projectDir, config.prdDir);
|
|
11224
|
-
if (fs24.existsSync(prdDir)) {
|
|
11225
|
-
cleanOrphanedClaims(prdDir);
|
|
11226
|
-
}
|
|
11227
|
-
const clients = projectSseClients.get(projectDir) ?? /* @__PURE__ */ new Set();
|
|
11228
|
-
broadcastSSE(clients, "status_changed", fetchStatusSnapshot(projectDir, config));
|
|
11229
|
-
res.json({ cleared: true });
|
|
11230
|
-
} catch (error2) {
|
|
11231
|
-
res.status(500).json({
|
|
11232
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11233
|
-
});
|
|
11234
|
-
}
|
|
11640
|
+
return projectSseClients.get(projectDir);
|
|
11641
|
+
},
|
|
11642
|
+
pathPrefix: "actions/"
|
|
11235
11643
|
});
|
|
11236
|
-
return router;
|
|
11237
11644
|
}
|
|
11238
11645
|
init_dist();
|
|
11239
11646
|
function createAgentRoutes() {
|
|
@@ -11336,12 +11743,13 @@ function createAgentRoutes() {
|
|
|
11336
11743
|
return router;
|
|
11337
11744
|
}
|
|
11338
11745
|
init_dist();
|
|
11339
|
-
function
|
|
11340
|
-
const {
|
|
11341
|
-
const
|
|
11342
|
-
router.get(
|
|
11746
|
+
function createBoardRouteHandlers(ctx) {
|
|
11747
|
+
const router = Router3({ mergeParams: true });
|
|
11748
|
+
const p = ctx.pathPrefix;
|
|
11749
|
+
router.get(`/${p}status`, async (req, res) => {
|
|
11343
11750
|
try {
|
|
11344
|
-
const config = getConfig();
|
|
11751
|
+
const config = ctx.getConfig(req);
|
|
11752
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11345
11753
|
const provider = getBoardProvider(config, projectDir);
|
|
11346
11754
|
if (!provider) {
|
|
11347
11755
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11371,9 +11779,10 @@ function createBoardRoutes(deps) {
|
|
|
11371
11779
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
11372
11780
|
}
|
|
11373
11781
|
});
|
|
11374
|
-
router.get(
|
|
11782
|
+
router.get(`/${p}issues`, async (req, res) => {
|
|
11375
11783
|
try {
|
|
11376
|
-
const config = getConfig();
|
|
11784
|
+
const config = ctx.getConfig(req);
|
|
11785
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11377
11786
|
const provider = getBoardProvider(config, projectDir);
|
|
11378
11787
|
if (!provider) {
|
|
11379
11788
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11385,9 +11794,10 @@ function createBoardRoutes(deps) {
|
|
|
11385
11794
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
11386
11795
|
}
|
|
11387
11796
|
});
|
|
11388
|
-
router.post(
|
|
11797
|
+
router.post(`/${p}issues`, async (req, res) => {
|
|
11389
11798
|
try {
|
|
11390
|
-
const config = getConfig();
|
|
11799
|
+
const config = ctx.getConfig(req);
|
|
11800
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11391
11801
|
const provider = getBoardProvider(config, projectDir);
|
|
11392
11802
|
if (!provider) {
|
|
11393
11803
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11415,9 +11825,10 @@ function createBoardRoutes(deps) {
|
|
|
11415
11825
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
11416
11826
|
}
|
|
11417
11827
|
});
|
|
11418
|
-
router.patch(
|
|
11828
|
+
router.patch(`/${p}issues/:number/move`, async (req, res) => {
|
|
11419
11829
|
try {
|
|
11420
|
-
const config = getConfig();
|
|
11830
|
+
const config = ctx.getConfig(req);
|
|
11831
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11421
11832
|
const provider = getBoardProvider(config, projectDir);
|
|
11422
11833
|
if (!provider) {
|
|
11423
11834
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11444,9 +11855,10 @@ function createBoardRoutes(deps) {
|
|
|
11444
11855
|
});
|
|
11445
11856
|
}
|
|
11446
11857
|
});
|
|
11447
|
-
router.post(
|
|
11858
|
+
router.post(`/${p}issues/:number/comment`, async (req, res) => {
|
|
11448
11859
|
try {
|
|
11449
|
-
const config = getConfig();
|
|
11860
|
+
const config = ctx.getConfig(req);
|
|
11861
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11450
11862
|
const provider = getBoardProvider(config, projectDir);
|
|
11451
11863
|
if (!provider) {
|
|
11452
11864
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11471,9 +11883,10 @@ function createBoardRoutes(deps) {
|
|
|
11471
11883
|
});
|
|
11472
11884
|
}
|
|
11473
11885
|
});
|
|
11474
|
-
router.delete(
|
|
11886
|
+
router.delete(`/${p}issues/:number`, async (req, res) => {
|
|
11475
11887
|
try {
|
|
11476
|
-
const config = getConfig();
|
|
11888
|
+
const config = ctx.getConfig(req);
|
|
11889
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11477
11890
|
const provider = getBoardProvider(config, projectDir);
|
|
11478
11891
|
if (!provider) {
|
|
11479
11892
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11495,175 +11908,19 @@ function createBoardRoutes(deps) {
|
|
|
11495
11908
|
});
|
|
11496
11909
|
return router;
|
|
11497
11910
|
}
|
|
11498
|
-
function
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
const projectDir = req.projectDir;
|
|
11504
|
-
const provider = getBoardProvider(config, projectDir);
|
|
11505
|
-
if (!provider) {
|
|
11506
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11507
|
-
return;
|
|
11508
|
-
}
|
|
11509
|
-
const cached = getCachedBoardData(projectDir);
|
|
11510
|
-
if (cached) {
|
|
11511
|
-
res.json(cached);
|
|
11512
|
-
return;
|
|
11513
|
-
}
|
|
11514
|
-
const issues = await provider.getAllIssues();
|
|
11515
|
-
const columns = {
|
|
11516
|
-
Draft: [],
|
|
11517
|
-
Ready: [],
|
|
11518
|
-
"In Progress": [],
|
|
11519
|
-
Review: [],
|
|
11520
|
-
Done: []
|
|
11521
|
-
};
|
|
11522
|
-
for (const issue of issues) {
|
|
11523
|
-
const col = issue.column ?? "Draft";
|
|
11524
|
-
columns[col].push(issue);
|
|
11525
|
-
}
|
|
11526
|
-
const result = { enabled: true, columns };
|
|
11527
|
-
setCachedBoardData(projectDir, result);
|
|
11528
|
-
res.json(result);
|
|
11529
|
-
} catch (error2) {
|
|
11530
|
-
res.status(500).json({
|
|
11531
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11532
|
-
});
|
|
11533
|
-
}
|
|
11534
|
-
});
|
|
11535
|
-
router.get("/board/issues", async (_req, res) => {
|
|
11536
|
-
try {
|
|
11537
|
-
const config = _req.projectConfig;
|
|
11538
|
-
const projectDir = _req.projectDir;
|
|
11539
|
-
const provider = getBoardProvider(config, projectDir);
|
|
11540
|
-
if (!provider) {
|
|
11541
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11542
|
-
return;
|
|
11543
|
-
}
|
|
11544
|
-
const issues = await provider.getAllIssues();
|
|
11545
|
-
res.json(issues);
|
|
11546
|
-
} catch (error2) {
|
|
11547
|
-
res.status(500).json({
|
|
11548
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11549
|
-
});
|
|
11550
|
-
}
|
|
11551
|
-
});
|
|
11552
|
-
router.post("/board/issues", async (req, res) => {
|
|
11553
|
-
try {
|
|
11554
|
-
const config = req.projectConfig;
|
|
11555
|
-
const projectDir = req.projectDir;
|
|
11556
|
-
const provider = getBoardProvider(config, projectDir);
|
|
11557
|
-
if (!provider) {
|
|
11558
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11559
|
-
return;
|
|
11560
|
-
}
|
|
11561
|
-
const { title, body, column } = req.body;
|
|
11562
|
-
if (!title || typeof title !== "string" || title.trim().length === 0) {
|
|
11563
|
-
res.status(400).json({ error: "title is required" });
|
|
11564
|
-
return;
|
|
11565
|
-
}
|
|
11566
|
-
if (column && !BOARD_COLUMNS.includes(column)) {
|
|
11567
|
-
res.status(400).json({
|
|
11568
|
-
error: `Invalid column. Must be one of: ${BOARD_COLUMNS.join(", ")}`
|
|
11569
|
-
});
|
|
11570
|
-
return;
|
|
11571
|
-
}
|
|
11572
|
-
const issue = await provider.createIssue({
|
|
11573
|
-
title: title.trim(),
|
|
11574
|
-
body: body ?? "",
|
|
11575
|
-
column
|
|
11576
|
-
});
|
|
11577
|
-
invalidateBoardCache(projectDir);
|
|
11578
|
-
res.status(201).json(issue);
|
|
11579
|
-
} catch (error2) {
|
|
11580
|
-
res.status(500).json({
|
|
11581
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11582
|
-
});
|
|
11583
|
-
}
|
|
11584
|
-
});
|
|
11585
|
-
router.patch("/board/issues/:number/move", async (req, res) => {
|
|
11586
|
-
try {
|
|
11587
|
-
const config = req.projectConfig;
|
|
11588
|
-
const projectDir = req.projectDir;
|
|
11589
|
-
const provider = getBoardProvider(config, projectDir);
|
|
11590
|
-
if (!provider) {
|
|
11591
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11592
|
-
return;
|
|
11593
|
-
}
|
|
11594
|
-
const issueNumber = parseInt(req.params.number, 10);
|
|
11595
|
-
if (isNaN(issueNumber)) {
|
|
11596
|
-
res.status(400).json({ error: "Invalid issue number" });
|
|
11597
|
-
return;
|
|
11598
|
-
}
|
|
11599
|
-
const { column } = req.body;
|
|
11600
|
-
if (!column || !BOARD_COLUMNS.includes(column)) {
|
|
11601
|
-
res.status(400).json({
|
|
11602
|
-
error: `Invalid column. Must be one of: ${BOARD_COLUMNS.join(", ")}`
|
|
11603
|
-
});
|
|
11604
|
-
return;
|
|
11605
|
-
}
|
|
11606
|
-
await provider.moveIssue(issueNumber, column);
|
|
11607
|
-
invalidateBoardCache(projectDir);
|
|
11608
|
-
res.json({ moved: true });
|
|
11609
|
-
} catch (error2) {
|
|
11610
|
-
res.status(500).json({
|
|
11611
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11612
|
-
});
|
|
11613
|
-
}
|
|
11614
|
-
});
|
|
11615
|
-
router.post("/board/issues/:number/comment", async (req, res) => {
|
|
11616
|
-
try {
|
|
11617
|
-
const config = req.projectConfig;
|
|
11618
|
-
const projectDir = req.projectDir;
|
|
11619
|
-
const provider = getBoardProvider(config, projectDir);
|
|
11620
|
-
if (!provider) {
|
|
11621
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11622
|
-
return;
|
|
11623
|
-
}
|
|
11624
|
-
const issueNumber = parseInt(req.params.number, 10);
|
|
11625
|
-
if (isNaN(issueNumber)) {
|
|
11626
|
-
res.status(400).json({ error: "Invalid issue number" });
|
|
11627
|
-
return;
|
|
11628
|
-
}
|
|
11629
|
-
const { body } = req.body;
|
|
11630
|
-
if (!body || typeof body !== "string" || body.trim().length === 0) {
|
|
11631
|
-
res.status(400).json({ error: "body is required" });
|
|
11632
|
-
return;
|
|
11633
|
-
}
|
|
11634
|
-
await provider.commentOnIssue(issueNumber, body);
|
|
11635
|
-
invalidateBoardCache(projectDir);
|
|
11636
|
-
res.json({ commented: true });
|
|
11637
|
-
} catch (error2) {
|
|
11638
|
-
res.status(500).json({
|
|
11639
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11640
|
-
});
|
|
11641
|
-
}
|
|
11911
|
+
function createBoardRoutes(deps) {
|
|
11912
|
+
return createBoardRouteHandlers({
|
|
11913
|
+
getConfig: () => deps.getConfig(),
|
|
11914
|
+
getProjectDir: () => deps.projectDir,
|
|
11915
|
+
pathPrefix: ""
|
|
11642
11916
|
});
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11650
|
-
return;
|
|
11651
|
-
}
|
|
11652
|
-
const issueNumber = parseInt(req.params.number, 10);
|
|
11653
|
-
if (isNaN(issueNumber)) {
|
|
11654
|
-
res.status(400).json({ error: "Invalid issue number" });
|
|
11655
|
-
return;
|
|
11656
|
-
}
|
|
11657
|
-
await provider.closeIssue(issueNumber);
|
|
11658
|
-
invalidateBoardCache(projectDir);
|
|
11659
|
-
res.json({ closed: true });
|
|
11660
|
-
} catch (error2) {
|
|
11661
|
-
res.status(500).json({
|
|
11662
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11663
|
-
});
|
|
11664
|
-
}
|
|
11917
|
+
}
|
|
11918
|
+
function createProjectBoardRoutes() {
|
|
11919
|
+
return createBoardRouteHandlers({
|
|
11920
|
+
getConfig: (req) => req.projectConfig,
|
|
11921
|
+
getProjectDir: (req) => req.projectDir,
|
|
11922
|
+
pathPrefix: "board/"
|
|
11665
11923
|
});
|
|
11666
|
-
return router;
|
|
11667
11924
|
}
|
|
11668
11925
|
init_dist();
|
|
11669
11926
|
function validateConfigChanges(changes) {
|
|
@@ -11679,6 +11936,9 @@ function validateConfigChanges(changes) {
|
|
|
11679
11936
|
if (changes.reviewerEnabled !== void 0 && typeof changes.reviewerEnabled !== "boolean") {
|
|
11680
11937
|
return "reviewerEnabled must be a boolean";
|
|
11681
11938
|
}
|
|
11939
|
+
if (changes.executorEnabled !== void 0 && typeof changes.executorEnabled !== "boolean") {
|
|
11940
|
+
return "executorEnabled must be a boolean";
|
|
11941
|
+
}
|
|
11682
11942
|
if (changes.maxRuntime !== void 0 && (typeof changes.maxRuntime !== "number" || changes.maxRuntime < 60)) {
|
|
11683
11943
|
return "maxRuntime must be a number >= 60";
|
|
11684
11944
|
}
|
|
@@ -11751,6 +12011,82 @@ function validateConfigChanges(changes) {
|
|
|
11751
12011
|
}
|
|
11752
12012
|
}
|
|
11753
12013
|
}
|
|
12014
|
+
if (changes.prdDir !== void 0 && (typeof changes.prdDir !== "string" || changes.prdDir.trim().length === 0)) {
|
|
12015
|
+
return "prdDir must be a non-empty string";
|
|
12016
|
+
}
|
|
12017
|
+
if (changes.cronScheduleOffset !== void 0 && (typeof changes.cronScheduleOffset !== "number" || changes.cronScheduleOffset < 0 || changes.cronScheduleOffset > 59)) {
|
|
12018
|
+
return "cronScheduleOffset must be a number between 0 and 59";
|
|
12019
|
+
}
|
|
12020
|
+
if (changes.fallbackOnRateLimit !== void 0 && typeof changes.fallbackOnRateLimit !== "boolean") {
|
|
12021
|
+
return "fallbackOnRateLimit must be a boolean";
|
|
12022
|
+
}
|
|
12023
|
+
if (changes.claudeModel !== void 0 && !VALID_CLAUDE_MODELS.includes(changes.claudeModel)) {
|
|
12024
|
+
return `Invalid claudeModel. Must be one of: ${VALID_CLAUDE_MODELS.join(", ")}`;
|
|
12025
|
+
}
|
|
12026
|
+
if (changes.qa !== void 0) {
|
|
12027
|
+
if (typeof changes.qa !== "object" || changes.qa === null) {
|
|
12028
|
+
return "qa must be an object";
|
|
12029
|
+
}
|
|
12030
|
+
const qa = changes.qa;
|
|
12031
|
+
if (qa.enabled !== void 0 && typeof qa.enabled !== "boolean") {
|
|
12032
|
+
return "qa.enabled must be a boolean";
|
|
12033
|
+
}
|
|
12034
|
+
if (qa.schedule !== void 0 && (typeof qa.schedule !== "string" || qa.schedule.trim().length === 0)) {
|
|
12035
|
+
return "qa.schedule must be a non-empty string";
|
|
12036
|
+
}
|
|
12037
|
+
if (qa.maxRuntime !== void 0 && (typeof qa.maxRuntime !== "number" || qa.maxRuntime < 60)) {
|
|
12038
|
+
return "qa.maxRuntime must be a number >= 60";
|
|
12039
|
+
}
|
|
12040
|
+
if (qa.branchPatterns !== void 0) {
|
|
12041
|
+
if (!Array.isArray(qa.branchPatterns) || !qa.branchPatterns.every((p) => typeof p === "string")) {
|
|
12042
|
+
return "qa.branchPatterns must be an array of strings";
|
|
12043
|
+
}
|
|
12044
|
+
}
|
|
12045
|
+
if (qa.artifacts !== void 0) {
|
|
12046
|
+
const validArtifacts = ["screenshot", "video", "both"];
|
|
12047
|
+
if (!validArtifacts.includes(qa.artifacts)) {
|
|
12048
|
+
return `Invalid qa.artifacts. Must be one of: ${validArtifacts.join(", ")}`;
|
|
12049
|
+
}
|
|
12050
|
+
}
|
|
12051
|
+
if (qa.skipLabel !== void 0 && typeof qa.skipLabel !== "string") {
|
|
12052
|
+
return "qa.skipLabel must be a string";
|
|
12053
|
+
}
|
|
12054
|
+
if (qa.autoInstallPlaywright !== void 0 && typeof qa.autoInstallPlaywright !== "boolean") {
|
|
12055
|
+
return "qa.autoInstallPlaywright must be a boolean";
|
|
12056
|
+
}
|
|
12057
|
+
}
|
|
12058
|
+
if (changes.audit !== void 0) {
|
|
12059
|
+
if (typeof changes.audit !== "object" || changes.audit === null) {
|
|
12060
|
+
return "audit must be an object";
|
|
12061
|
+
}
|
|
12062
|
+
const audit = changes.audit;
|
|
12063
|
+
if (audit.enabled !== void 0 && typeof audit.enabled !== "boolean") {
|
|
12064
|
+
return "audit.enabled must be a boolean";
|
|
12065
|
+
}
|
|
12066
|
+
if (audit.schedule !== void 0 && (typeof audit.schedule !== "string" || audit.schedule.trim().length === 0)) {
|
|
12067
|
+
return "audit.schedule must be a non-empty string";
|
|
12068
|
+
}
|
|
12069
|
+
if (audit.maxRuntime !== void 0 && (typeof audit.maxRuntime !== "number" || audit.maxRuntime < 60)) {
|
|
12070
|
+
return "audit.maxRuntime must be a number >= 60";
|
|
12071
|
+
}
|
|
12072
|
+
}
|
|
12073
|
+
if (changes.roadmapScanner !== void 0) {
|
|
12074
|
+
const rs = changes.roadmapScanner;
|
|
12075
|
+
if (rs.slicerSchedule !== void 0 && (typeof rs.slicerSchedule !== "string" || rs.slicerSchedule.trim().length === 0)) {
|
|
12076
|
+
return "roadmapScanner.slicerSchedule must be a non-empty string";
|
|
12077
|
+
}
|
|
12078
|
+
if (rs.slicerMaxRuntime !== void 0 && (typeof rs.slicerMaxRuntime !== "number" || rs.slicerMaxRuntime < 60)) {
|
|
12079
|
+
return "roadmapScanner.slicerMaxRuntime must be a number >= 60";
|
|
12080
|
+
}
|
|
12081
|
+
}
|
|
12082
|
+
if (changes.boardProvider !== void 0) {
|
|
12083
|
+
if (typeof changes.boardProvider !== "object" || changes.boardProvider === null) {
|
|
12084
|
+
return "boardProvider must be an object";
|
|
12085
|
+
}
|
|
12086
|
+
if (changes.boardProvider.enabled !== void 0 && typeof changes.boardProvider.enabled !== "boolean") {
|
|
12087
|
+
return "boardProvider.enabled must be a boolean";
|
|
12088
|
+
}
|
|
12089
|
+
}
|
|
11754
12090
|
return null;
|
|
11755
12091
|
}
|
|
11756
12092
|
function createConfigRoutes(deps) {
|
|
@@ -11818,7 +12154,7 @@ init_dist();
|
|
|
11818
12154
|
function runDoctorChecks(projectDir, config) {
|
|
11819
12155
|
const checks = [];
|
|
11820
12156
|
try {
|
|
11821
|
-
|
|
12157
|
+
execSync5("git rev-parse --is-inside-work-tree", {
|
|
11822
12158
|
cwd: projectDir,
|
|
11823
12159
|
stdio: "pipe"
|
|
11824
12160
|
});
|
|
@@ -11827,7 +12163,7 @@ function runDoctorChecks(projectDir, config) {
|
|
|
11827
12163
|
checks.push({ name: "git", status: "fail", detail: "Not a git repository" });
|
|
11828
12164
|
}
|
|
11829
12165
|
try {
|
|
11830
|
-
|
|
12166
|
+
execSync5(`which ${config.provider}`, { stdio: "pipe" });
|
|
11831
12167
|
checks.push({
|
|
11832
12168
|
name: "provider",
|
|
11833
12169
|
status: "pass",
|
|
@@ -11923,7 +12259,7 @@ function createLogRoutes(deps) {
|
|
|
11923
12259
|
router.get("/:name", (req, res) => {
|
|
11924
12260
|
try {
|
|
11925
12261
|
const { name } = req.params;
|
|
11926
|
-
const validNames = ["executor", "reviewer", "qa"];
|
|
12262
|
+
const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
|
|
11927
12263
|
if (!validNames.includes(name)) {
|
|
11928
12264
|
res.status(400).json({
|
|
11929
12265
|
error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
|
|
@@ -11949,7 +12285,7 @@ function createProjectLogRoutes() {
|
|
|
11949
12285
|
try {
|
|
11950
12286
|
const projectDir = req.projectDir;
|
|
11951
12287
|
const { name } = req.params;
|
|
11952
|
-
const validNames = ["executor", "reviewer", "qa"];
|
|
12288
|
+
const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
|
|
11953
12289
|
if (!validNames.includes(name)) {
|
|
11954
12290
|
res.status(400).json({
|
|
11955
12291
|
error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
|
|
@@ -11990,12 +12326,13 @@ function createProjectPrdRoutes() {
|
|
|
11990
12326
|
return router;
|
|
11991
12327
|
}
|
|
11992
12328
|
init_dist();
|
|
11993
|
-
function
|
|
11994
|
-
const {
|
|
11995
|
-
const
|
|
11996
|
-
router.get(
|
|
12329
|
+
function createRoadmapRouteHandlers(ctx) {
|
|
12330
|
+
const router = Router8({ mergeParams: true });
|
|
12331
|
+
const p = ctx.pathPrefix;
|
|
12332
|
+
router.get(`/${p}`, (req, res) => {
|
|
11997
12333
|
try {
|
|
11998
|
-
const config = getConfig();
|
|
12334
|
+
const config = ctx.getConfig(req);
|
|
12335
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11999
12336
|
const status = getRoadmapStatus(projectDir, config);
|
|
12000
12337
|
const prdDir = path27.join(projectDir, config.prdDir);
|
|
12001
12338
|
const state = loadRoadmapState(prdDir);
|
|
@@ -12008,9 +12345,10 @@ function createRoadmapRoutes(deps) {
|
|
|
12008
12345
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12009
12346
|
}
|
|
12010
12347
|
});
|
|
12011
|
-
router.post(
|
|
12348
|
+
router.post(`/${p}scan`, async (req, res) => {
|
|
12012
12349
|
try {
|
|
12013
|
-
const config = getConfig();
|
|
12350
|
+
const config = ctx.getConfig(req);
|
|
12351
|
+
const projectDir = ctx.getProjectDir(req);
|
|
12014
12352
|
if (!config.roadmapScanner.enabled) {
|
|
12015
12353
|
res.status(409).json({ error: "Roadmap scanner is disabled" });
|
|
12016
12354
|
return;
|
|
@@ -12023,14 +12361,15 @@ function createRoadmapRoutes(deps) {
|
|
|
12023
12361
|
});
|
|
12024
12362
|
}
|
|
12025
12363
|
});
|
|
12026
|
-
router.put(
|
|
12364
|
+
router.put(`/${p}toggle`, (req, res) => {
|
|
12027
12365
|
try {
|
|
12028
12366
|
const { enabled } = req.body;
|
|
12029
12367
|
if (typeof enabled !== "boolean") {
|
|
12030
12368
|
res.status(400).json({ error: "enabled must be a boolean" });
|
|
12031
12369
|
return;
|
|
12032
12370
|
}
|
|
12033
|
-
const
|
|
12371
|
+
const projectDir = ctx.getProjectDir(req);
|
|
12372
|
+
const currentConfig = ctx.getConfig(req);
|
|
12034
12373
|
const result = saveConfig(projectDir, {
|
|
12035
12374
|
roadmapScanner: {
|
|
12036
12375
|
...currentConfig.roadmapScanner,
|
|
@@ -12041,71 +12380,30 @@ function createRoadmapRoutes(deps) {
|
|
|
12041
12380
|
res.status(500).json({ error: result.error });
|
|
12042
12381
|
return;
|
|
12043
12382
|
}
|
|
12044
|
-
|
|
12045
|
-
res.json(
|
|
12383
|
+
ctx.afterToggle(req);
|
|
12384
|
+
res.json(loadConfig(projectDir));
|
|
12046
12385
|
} catch (error2) {
|
|
12047
12386
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12048
12387
|
}
|
|
12049
12388
|
});
|
|
12050
12389
|
return router;
|
|
12051
12390
|
}
|
|
12052
|
-
function
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
const status = getRoadmapStatus(projectDir, config);
|
|
12059
|
-
const prdDir = path27.join(projectDir, config.prdDir);
|
|
12060
|
-
const state = loadRoadmapState(prdDir);
|
|
12061
|
-
res.json({
|
|
12062
|
-
...status,
|
|
12063
|
-
lastScan: state.lastScan || null,
|
|
12064
|
-
autoScanInterval: config.roadmapScanner.autoScanInterval
|
|
12065
|
-
});
|
|
12066
|
-
} catch (error2) {
|
|
12067
|
-
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12068
|
-
}
|
|
12069
|
-
});
|
|
12070
|
-
router.post("/roadmap/scan", async (req, res) => {
|
|
12071
|
-
try {
|
|
12072
|
-
const config = req.projectConfig;
|
|
12073
|
-
const projectDir = req.projectDir;
|
|
12074
|
-
if (!config.roadmapScanner.enabled) {
|
|
12075
|
-
res.status(409).json({ error: "Roadmap scanner is disabled" });
|
|
12076
|
-
return;
|
|
12077
|
-
}
|
|
12078
|
-
const result = await scanRoadmap(projectDir, config);
|
|
12079
|
-
res.json(result);
|
|
12080
|
-
} catch (error2) {
|
|
12081
|
-
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12082
|
-
}
|
|
12391
|
+
function createRoadmapRoutes(deps) {
|
|
12392
|
+
return createRoadmapRouteHandlers({
|
|
12393
|
+
getConfig: () => deps.getConfig(),
|
|
12394
|
+
getProjectDir: () => deps.projectDir,
|
|
12395
|
+
afterToggle: () => deps.reloadConfig(),
|
|
12396
|
+
pathPrefix: ""
|
|
12083
12397
|
});
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12090
|
-
|
|
12091
|
-
|
|
12092
|
-
const currentConfig = req.projectConfig;
|
|
12093
|
-
const result = saveConfig(projectDir, {
|
|
12094
|
-
roadmapScanner: {
|
|
12095
|
-
...currentConfig.roadmapScanner,
|
|
12096
|
-
enabled
|
|
12097
|
-
}
|
|
12098
|
-
});
|
|
12099
|
-
if (!result.success) {
|
|
12100
|
-
res.status(500).json({ error: result.error });
|
|
12101
|
-
return;
|
|
12102
|
-
}
|
|
12103
|
-
res.json(loadConfig(projectDir));
|
|
12104
|
-
} catch (error2) {
|
|
12105
|
-
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12106
|
-
}
|
|
12398
|
+
}
|
|
12399
|
+
function createProjectRoadmapRoutes() {
|
|
12400
|
+
return createRoadmapRouteHandlers({
|
|
12401
|
+
getConfig: (req) => req.projectConfig,
|
|
12402
|
+
getProjectDir: (req) => req.projectDir,
|
|
12403
|
+
afterToggle: () => {
|
|
12404
|
+
},
|
|
12405
|
+
pathPrefix: "roadmap/"
|
|
12107
12406
|
});
|
|
12108
|
-
return router;
|
|
12109
12407
|
}
|
|
12110
12408
|
init_dist();
|
|
12111
12409
|
function createStatusRoutes(deps) {
|
|
@@ -12117,21 +12415,20 @@ function createStatusRoutes(deps) {
|
|
|
12117
12415
|
res.setHeader("Connection", "keep-alive");
|
|
12118
12416
|
res.flushHeaders();
|
|
12119
12417
|
sseClients.add(res);
|
|
12120
|
-
|
|
12121
|
-
const snapshot = fetchStatusSnapshot(projectDir, getConfig());
|
|
12418
|
+
fetchStatusSnapshot(projectDir, getConfig()).then((snapshot) => {
|
|
12122
12419
|
res.write(`event: status_changed
|
|
12123
12420
|
data: ${JSON.stringify(snapshot)}
|
|
12124
12421
|
|
|
12125
12422
|
`);
|
|
12126
|
-
}
|
|
12127
|
-
}
|
|
12423
|
+
}).catch(() => {
|
|
12424
|
+
});
|
|
12128
12425
|
req.on("close", () => {
|
|
12129
12426
|
sseClients.delete(res);
|
|
12130
12427
|
});
|
|
12131
12428
|
});
|
|
12132
|
-
router.get("/", (_req, res) => {
|
|
12429
|
+
router.get("/", async (_req, res) => {
|
|
12133
12430
|
try {
|
|
12134
|
-
const snapshot = fetchStatusSnapshot(projectDir, getConfig());
|
|
12431
|
+
const snapshot = await fetchStatusSnapshot(projectDir, getConfig());
|
|
12135
12432
|
res.json(snapshot);
|
|
12136
12433
|
} catch (error2) {
|
|
12137
12434
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
@@ -12147,30 +12444,49 @@ function computeNextRun(cronExpr) {
|
|
|
12147
12444
|
return null;
|
|
12148
12445
|
}
|
|
12149
12446
|
}
|
|
12447
|
+
function hasScheduledCommand(entries, command) {
|
|
12448
|
+
const commandPattern = new RegExp(`\\s${command}\\s+>>`);
|
|
12449
|
+
return entries.some((entry) => commandPattern.test(entry));
|
|
12450
|
+
}
|
|
12150
12451
|
function createScheduleInfoRoutes(deps) {
|
|
12151
12452
|
const { projectDir, getConfig } = deps;
|
|
12152
12453
|
const router = Router9();
|
|
12153
|
-
router.get("/", (_req, res) => {
|
|
12454
|
+
router.get("/", async (_req, res) => {
|
|
12154
12455
|
try {
|
|
12155
12456
|
const config = getConfig();
|
|
12156
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
12457
|
+
const snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
12157
12458
|
const installed = snapshot.crontab.installed;
|
|
12158
12459
|
const entries = snapshot.crontab.entries;
|
|
12460
|
+
const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
|
|
12461
|
+
const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
|
|
12462
|
+
const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
|
|
12463
|
+
const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
|
|
12464
|
+
const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
|
|
12159
12465
|
res.json({
|
|
12160
12466
|
executor: {
|
|
12161
12467
|
schedule: config.cronSchedule,
|
|
12162
|
-
installed,
|
|
12163
|
-
nextRun:
|
|
12468
|
+
installed: executorInstalled,
|
|
12469
|
+
nextRun: executorInstalled ? computeNextRun(config.cronSchedule) : null
|
|
12164
12470
|
},
|
|
12165
12471
|
reviewer: {
|
|
12166
12472
|
schedule: config.reviewerSchedule,
|
|
12167
|
-
installed:
|
|
12168
|
-
nextRun:
|
|
12473
|
+
installed: reviewerInstalled,
|
|
12474
|
+
nextRun: reviewerInstalled ? computeNextRun(config.reviewerSchedule) : null
|
|
12169
12475
|
},
|
|
12170
12476
|
qa: {
|
|
12171
12477
|
schedule: config.qa.schedule,
|
|
12172
|
-
installed:
|
|
12173
|
-
nextRun:
|
|
12478
|
+
installed: qaInstalled,
|
|
12479
|
+
nextRun: qaInstalled ? computeNextRun(config.qa.schedule) : null
|
|
12480
|
+
},
|
|
12481
|
+
audit: {
|
|
12482
|
+
schedule: config.audit.schedule,
|
|
12483
|
+
installed: auditInstalled,
|
|
12484
|
+
nextRun: auditInstalled ? computeNextRun(config.audit.schedule) : null
|
|
12485
|
+
},
|
|
12486
|
+
planner: {
|
|
12487
|
+
schedule: config.roadmapScanner.slicerSchedule,
|
|
12488
|
+
installed: plannerInstalled,
|
|
12489
|
+
nextRun: plannerInstalled ? computeNextRun(config.roadmapScanner.slicerSchedule) : null
|
|
12174
12490
|
},
|
|
12175
12491
|
paused: !installed,
|
|
12176
12492
|
entries
|
|
@@ -12186,13 +12502,12 @@ function createProjectSseRoutes(deps) {
|
|
|
12186
12502
|
const router = Router9({ mergeParams: true });
|
|
12187
12503
|
router.get("/status/events", (req, res) => {
|
|
12188
12504
|
const projectDir = req.projectDir;
|
|
12189
|
-
const config = req.projectConfig;
|
|
12190
12505
|
if (!projectSseClients.has(projectDir)) {
|
|
12191
12506
|
projectSseClients.set(projectDir, /* @__PURE__ */ new Set());
|
|
12192
12507
|
}
|
|
12193
12508
|
const clients = projectSseClients.get(projectDir);
|
|
12194
12509
|
if (!projectSseWatchers.has(projectDir)) {
|
|
12195
|
-
const watcher = startSseStatusWatcher(clients, projectDir, () =>
|
|
12510
|
+
const watcher = startSseStatusWatcher(clients, projectDir, () => loadConfig(projectDir));
|
|
12196
12511
|
projectSseWatchers.set(projectDir, watcher);
|
|
12197
12512
|
}
|
|
12198
12513
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -12200,48 +12515,69 @@ function createProjectSseRoutes(deps) {
|
|
|
12200
12515
|
res.setHeader("Connection", "keep-alive");
|
|
12201
12516
|
res.flushHeaders();
|
|
12202
12517
|
clients.add(res);
|
|
12203
|
-
|
|
12204
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
12518
|
+
fetchStatusSnapshot(projectDir, loadConfig(projectDir)).then((snapshot) => {
|
|
12205
12519
|
res.write(`event: status_changed
|
|
12206
12520
|
data: ${JSON.stringify(snapshot)}
|
|
12207
12521
|
|
|
12208
12522
|
`);
|
|
12209
|
-
}
|
|
12210
|
-
}
|
|
12523
|
+
}).catch(() => {
|
|
12524
|
+
});
|
|
12211
12525
|
req.on("close", () => {
|
|
12212
12526
|
clients.delete(res);
|
|
12527
|
+
if (clients.size === 0) {
|
|
12528
|
+
const watcher = projectSseWatchers.get(projectDir);
|
|
12529
|
+
if (watcher !== void 0) {
|
|
12530
|
+
clearInterval(watcher);
|
|
12531
|
+
projectSseWatchers.delete(projectDir);
|
|
12532
|
+
}
|
|
12533
|
+
}
|
|
12213
12534
|
});
|
|
12214
12535
|
});
|
|
12215
|
-
router.get("/status", (req, res) => {
|
|
12536
|
+
router.get("/status", async (req, res) => {
|
|
12216
12537
|
try {
|
|
12217
|
-
const snapshot = fetchStatusSnapshot(req.projectDir, req.projectConfig);
|
|
12538
|
+
const snapshot = await fetchStatusSnapshot(req.projectDir, req.projectConfig);
|
|
12218
12539
|
res.json(snapshot);
|
|
12219
12540
|
} catch (error2) {
|
|
12220
12541
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12221
12542
|
}
|
|
12222
12543
|
});
|
|
12223
|
-
router.get("/schedule-info", (req, res) => {
|
|
12544
|
+
router.get("/schedule-info", async (req, res) => {
|
|
12224
12545
|
try {
|
|
12225
12546
|
const config = req.projectConfig;
|
|
12226
12547
|
const projectDir = req.projectDir;
|
|
12227
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
12548
|
+
const snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
12228
12549
|
const installed = snapshot.crontab.installed;
|
|
12229
12550
|
const entries = snapshot.crontab.entries;
|
|
12551
|
+
const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
|
|
12552
|
+
const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
|
|
12553
|
+
const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
|
|
12554
|
+
const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
|
|
12555
|
+
const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
|
|
12230
12556
|
res.json({
|
|
12231
12557
|
executor: {
|
|
12232
12558
|
schedule: config.cronSchedule,
|
|
12233
|
-
installed,
|
|
12234
|
-
nextRun:
|
|
12559
|
+
installed: executorInstalled,
|
|
12560
|
+
nextRun: executorInstalled ? computeNextRun(config.cronSchedule) : null
|
|
12235
12561
|
},
|
|
12236
12562
|
reviewer: {
|
|
12237
12563
|
schedule: config.reviewerSchedule,
|
|
12238
|
-
installed:
|
|
12239
|
-
nextRun:
|
|
12564
|
+
installed: reviewerInstalled,
|
|
12565
|
+
nextRun: reviewerInstalled ? computeNextRun(config.reviewerSchedule) : null
|
|
12240
12566
|
},
|
|
12241
12567
|
qa: {
|
|
12242
12568
|
schedule: config.qa.schedule,
|
|
12243
|
-
installed:
|
|
12244
|
-
nextRun:
|
|
12569
|
+
installed: qaInstalled,
|
|
12570
|
+
nextRun: qaInstalled ? computeNextRun(config.qa.schedule) : null
|
|
12571
|
+
},
|
|
12572
|
+
audit: {
|
|
12573
|
+
schedule: config.audit.schedule,
|
|
12574
|
+
installed: auditInstalled,
|
|
12575
|
+
nextRun: auditInstalled ? computeNextRun(config.audit.schedule) : null
|
|
12576
|
+
},
|
|
12577
|
+
planner: {
|
|
12578
|
+
schedule: config.roadmapScanner.slicerSchedule,
|
|
12579
|
+
installed: plannerInstalled,
|
|
12580
|
+
nextRun: plannerInstalled ? computeNextRun(config.roadmapScanner.slicerSchedule) : null
|
|
12245
12581
|
},
|
|
12246
12582
|
paused: !installed,
|
|
12247
12583
|
entries
|
|
@@ -12316,9 +12652,9 @@ function createApp(projectDir) {
|
|
|
12316
12652
|
app.use("/api/roadmap", createRoadmapRoutes({ projectDir, getConfig: () => config, reloadConfig }));
|
|
12317
12653
|
app.use("/api/logs", createLogRoutes({ projectDir }));
|
|
12318
12654
|
app.use("/api/doctor", createDoctorRoutes({ projectDir, getConfig: () => config }));
|
|
12319
|
-
app.get("/api/prs", (_req, res) => {
|
|
12655
|
+
app.get("/api/prs", async (_req, res) => {
|
|
12320
12656
|
try {
|
|
12321
|
-
res.json(collectPrInfo(projectDir, config.branchPatterns));
|
|
12657
|
+
res.json(await collectPrInfo(projectDir, config.branchPatterns));
|
|
12322
12658
|
} catch (error2) {
|
|
12323
12659
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12324
12660
|
}
|
|
@@ -12362,9 +12698,9 @@ function createProjectRouter() {
|
|
|
12362
12698
|
router.use("/agents", createAgentRoutes());
|
|
12363
12699
|
router.use(createProjectActionRoutes({ projectSseClients }));
|
|
12364
12700
|
router.use(createProjectRoadmapRoutes());
|
|
12365
|
-
router.get("/prs", (req, res) => {
|
|
12701
|
+
router.get("/prs", async (req, res) => {
|
|
12366
12702
|
try {
|
|
12367
|
-
res.json(collectPrInfo(req.projectDir, req.projectConfig.branchPatterns));
|
|
12703
|
+
res.json(await collectPrInfo(req.projectDir, req.projectConfig.branchPatterns));
|
|
12368
12704
|
} catch (error2) {
|
|
12369
12705
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12370
12706
|
}
|
|
@@ -12754,7 +13090,7 @@ function prsCommand(program2) {
|
|
|
12754
13090
|
}
|
|
12755
13091
|
const projectDir = process.cwd();
|
|
12756
13092
|
const config = loadConfig(projectDir);
|
|
12757
|
-
const prs = collectPrInfo(projectDir, config.branchPatterns);
|
|
13093
|
+
const prs = await collectPrInfo(projectDir, config.branchPatterns);
|
|
12758
13094
|
if (options.json) {
|
|
12759
13095
|
const output = {
|
|
12760
13096
|
prs,
|
|
@@ -12801,7 +13137,7 @@ function prsCommand(program2) {
|
|
|
12801
13137
|
init_dist();
|
|
12802
13138
|
function getOpenPrBranches(projectDir) {
|
|
12803
13139
|
try {
|
|
12804
|
-
|
|
13140
|
+
execSync6("git rev-parse --git-dir", {
|
|
12805
13141
|
cwd: projectDir,
|
|
12806
13142
|
encoding: "utf-8",
|
|
12807
13143
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -12810,12 +13146,12 @@ function getOpenPrBranches(projectDir) {
|
|
|
12810
13146
|
return /* @__PURE__ */ new Set();
|
|
12811
13147
|
}
|
|
12812
13148
|
try {
|
|
12813
|
-
|
|
13149
|
+
execSync6("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
12814
13150
|
} catch {
|
|
12815
13151
|
return /* @__PURE__ */ new Set();
|
|
12816
13152
|
}
|
|
12817
13153
|
try {
|
|
12818
|
-
const output =
|
|
13154
|
+
const output = execSync6("gh pr list --state open --json headRefName", {
|
|
12819
13155
|
cwd: projectDir,
|
|
12820
13156
|
encoding: "utf-8",
|
|
12821
13157
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -13116,6 +13452,36 @@ function cancelCommand(program2) {
|
|
|
13116
13452
|
});
|
|
13117
13453
|
}
|
|
13118
13454
|
init_dist();
|
|
13455
|
+
function getTelegramStatusWebhooks3(config) {
|
|
13456
|
+
return (config.notifications?.webhooks ?? []).filter((wh) => wh.type === "telegram" && typeof wh.botToken === "string" && wh.botToken.trim().length > 0 && typeof wh.chatId === "string" && wh.chatId.trim().length > 0).map((wh) => ({ botToken: wh.botToken, chatId: wh.chatId }));
|
|
13457
|
+
}
|
|
13458
|
+
function plannerLockPath2(projectDir) {
|
|
13459
|
+
return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
|
|
13460
|
+
}
|
|
13461
|
+
function acquirePlannerLock(projectDir) {
|
|
13462
|
+
const lockFile = plannerLockPath2(projectDir);
|
|
13463
|
+
if (fs31.existsSync(lockFile)) {
|
|
13464
|
+
const pidRaw = fs31.readFileSync(lockFile, "utf-8").trim();
|
|
13465
|
+
const pid = parseInt(pidRaw, 10);
|
|
13466
|
+
if (!Number.isNaN(pid) && isProcessRunning(pid)) {
|
|
13467
|
+
return { acquired: false, lockFile, pid };
|
|
13468
|
+
}
|
|
13469
|
+
try {
|
|
13470
|
+
fs31.unlinkSync(lockFile);
|
|
13471
|
+
} catch {
|
|
13472
|
+
}
|
|
13473
|
+
}
|
|
13474
|
+
fs31.writeFileSync(lockFile, String(process.pid));
|
|
13475
|
+
return { acquired: true, lockFile };
|
|
13476
|
+
}
|
|
13477
|
+
function releasePlannerLock(lockFile) {
|
|
13478
|
+
try {
|
|
13479
|
+
if (fs31.existsSync(lockFile)) {
|
|
13480
|
+
fs31.unlinkSync(lockFile);
|
|
13481
|
+
}
|
|
13482
|
+
} catch {
|
|
13483
|
+
}
|
|
13484
|
+
}
|
|
13119
13485
|
function buildEnvVars5(config, options) {
|
|
13120
13486
|
const env = {};
|
|
13121
13487
|
const slicerProvider = resolveJobProvider(config, "slicer");
|
|
@@ -13126,6 +13492,12 @@ function buildEnvVars5(config, options) {
|
|
|
13126
13492
|
if (config.providerEnv) {
|
|
13127
13493
|
Object.assign(env, config.providerEnv);
|
|
13128
13494
|
}
|
|
13495
|
+
const telegramWebhooks = getTelegramStatusWebhooks3(config);
|
|
13496
|
+
if (telegramWebhooks.length > 0) {
|
|
13497
|
+
env.NW_TELEGRAM_STATUS_WEBHOOKS = JSON.stringify(telegramWebhooks);
|
|
13498
|
+
env.NW_TELEGRAM_BOT_TOKEN = telegramWebhooks[0].botToken;
|
|
13499
|
+
env.NW_TELEGRAM_CHAT_ID = telegramWebhooks[0].chatId;
|
|
13500
|
+
}
|
|
13129
13501
|
if (options.dryRun) {
|
|
13130
13502
|
env.NW_DRY_RUN = "1";
|
|
13131
13503
|
}
|
|
@@ -13149,13 +13521,20 @@ function applyCliOverrides4(config, options) {
|
|
|
13149
13521
|
return overridden;
|
|
13150
13522
|
}
|
|
13151
13523
|
function sliceCommand(program2) {
|
|
13152
|
-
program2.command("slice").description("Run roadmap slicer to create PRD from next roadmap item").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds for slicer").option("--provider <string>", "AI provider to use (claude or codex)").action(async (options) => {
|
|
13524
|
+
program2.command("slice").alias("planner").description("Run Planner (roadmap slicer) to create a PRD from the next roadmap item").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds for slicer").option("--provider <string>", "AI provider to use (claude or codex)").action(async (options) => {
|
|
13153
13525
|
const projectDir = process.cwd();
|
|
13526
|
+
const lockResult = acquirePlannerLock(projectDir);
|
|
13527
|
+
if (!lockResult.acquired) {
|
|
13528
|
+
info(`Planner is already running${lockResult.pid ? ` (PID ${lockResult.pid})` : ""}`);
|
|
13529
|
+
process.exit(0);
|
|
13530
|
+
}
|
|
13531
|
+
const cleanupLock = () => releasePlannerLock(lockResult.lockFile);
|
|
13532
|
+
process.on("exit", cleanupLock);
|
|
13154
13533
|
let config = loadConfig(projectDir);
|
|
13155
13534
|
config = applyCliOverrides4(config, options);
|
|
13156
13535
|
const envVars = buildEnvVars5(config, options);
|
|
13157
13536
|
if (options.dryRun) {
|
|
13158
|
-
header("Dry Run:
|
|
13537
|
+
header("Dry Run: Planner");
|
|
13159
13538
|
const slicerProvider = resolveJobProvider(config, "slicer");
|
|
13160
13539
|
header("Configuration");
|
|
13161
13540
|
const configTable = createTable({ head: ["Setting", "Value"] });
|
|
@@ -13164,10 +13543,10 @@ function sliceCommand(program2) {
|
|
|
13164
13543
|
configTable.push(["PRD Directory", config.prdDir]);
|
|
13165
13544
|
configTable.push(["Roadmap Path", config.roadmapScanner.roadmapPath]);
|
|
13166
13545
|
configTable.push([
|
|
13167
|
-
"
|
|
13546
|
+
"Planner Max Runtime",
|
|
13168
13547
|
`${config.roadmapScanner.slicerMaxRuntime}s (${Math.floor(config.roadmapScanner.slicerMaxRuntime / 60)}min)`
|
|
13169
13548
|
]);
|
|
13170
|
-
configTable.push(["
|
|
13549
|
+
configTable.push(["Planner Schedule", config.roadmapScanner.slicerSchedule]);
|
|
13171
13550
|
configTable.push(["Scanner Enabled", config.roadmapScanner.enabled ? "Yes" : "No"]);
|
|
13172
13551
|
console.log(configTable.toString());
|
|
13173
13552
|
header("Roadmap Status");
|
|
@@ -13211,38 +13590,51 @@ function sliceCommand(program2) {
|
|
|
13211
13590
|
process.exit(0);
|
|
13212
13591
|
}
|
|
13213
13592
|
if (!config.roadmapScanner.enabled) {
|
|
13214
|
-
|
|
13215
|
-
process.exit(
|
|
13593
|
+
info("Planner is disabled in config; skipping run.");
|
|
13594
|
+
process.exit(0);
|
|
13216
13595
|
}
|
|
13217
|
-
const spinner = createSpinner("Running
|
|
13596
|
+
const spinner = createSpinner("Running Planner...");
|
|
13218
13597
|
spinner.start();
|
|
13219
13598
|
try {
|
|
13599
|
+
if (!options.dryRun) {
|
|
13600
|
+
await sendNotifications(config, {
|
|
13601
|
+
event: "run_started",
|
|
13602
|
+
projectName: path31.basename(projectDir),
|
|
13603
|
+
exitCode: 0,
|
|
13604
|
+
provider: config.provider
|
|
13605
|
+
});
|
|
13606
|
+
}
|
|
13220
13607
|
const result = await sliceNextItem(projectDir, config);
|
|
13221
13608
|
if (result.sliced) {
|
|
13222
|
-
spinner.succeed(`
|
|
13609
|
+
spinner.succeed(`Planner completed successfully: Created ${result.file}`);
|
|
13223
13610
|
} else if (result.error) {
|
|
13224
13611
|
if (result.error === "No pending items to process") {
|
|
13225
13612
|
spinner.succeed("No pending items to process");
|
|
13226
13613
|
} else {
|
|
13227
|
-
spinner.fail(`
|
|
13614
|
+
spinner.fail(`Planner failed: ${result.error}`);
|
|
13228
13615
|
}
|
|
13229
13616
|
}
|
|
13230
13617
|
const nothingPending = result.error === "No pending items to process";
|
|
13231
13618
|
const exitCode = result.sliced || nothingPending ? 0 : 1;
|
|
13232
13619
|
if (!options.dryRun && result.sliced) {
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
event,
|
|
13620
|
+
await sendNotifications(config, {
|
|
13621
|
+
event: "run_succeeded",
|
|
13236
13622
|
projectName: path31.basename(projectDir),
|
|
13237
13623
|
exitCode,
|
|
13238
13624
|
provider: config.provider,
|
|
13239
13625
|
prTitle: result.item?.title
|
|
13240
|
-
};
|
|
13241
|
-
|
|
13626
|
+
});
|
|
13627
|
+
} else if (!options.dryRun && !nothingPending) {
|
|
13628
|
+
await sendNotifications(config, {
|
|
13629
|
+
event: "run_failed",
|
|
13630
|
+
projectName: path31.basename(projectDir),
|
|
13631
|
+
exitCode,
|
|
13632
|
+
provider: config.provider
|
|
13633
|
+
});
|
|
13242
13634
|
}
|
|
13243
13635
|
process.exit(exitCode);
|
|
13244
13636
|
} catch (err) {
|
|
13245
|
-
spinner.fail("Failed to execute
|
|
13637
|
+
spinner.fail("Failed to execute planner command");
|
|
13246
13638
|
error(`${err instanceof Error ? err.message : String(err)}`);
|
|
13247
13639
|
process.exit(1);
|
|
13248
13640
|
}
|
|
@@ -13347,7 +13739,7 @@ async function confirmPrompt(question) {
|
|
|
13347
13739
|
}
|
|
13348
13740
|
async function createGitHubLabel(label2, cwd) {
|
|
13349
13741
|
try {
|
|
13350
|
-
|
|
13742
|
+
execFileSync3("gh", ["label", "create", label2.name, "--description", label2.description, "--color", label2.color], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
13351
13743
|
return { created: true, skipped: false };
|
|
13352
13744
|
} catch (err) {
|
|
13353
13745
|
const output = err instanceof Error ? err.message : String(err);
|
|
@@ -13487,11 +13879,11 @@ function boardCommand(program2) {
|
|
|
13487
13879
|
let body = options.body ?? "";
|
|
13488
13880
|
if (options.bodyFile) {
|
|
13489
13881
|
const filePath = options.bodyFile;
|
|
13490
|
-
if (!
|
|
13882
|
+
if (!fs32.existsSync(filePath)) {
|
|
13491
13883
|
console.error(`File not found: ${filePath}`);
|
|
13492
13884
|
process.exit(1);
|
|
13493
13885
|
}
|
|
13494
|
-
body =
|
|
13886
|
+
body = fs32.readFileSync(filePath, "utf-8");
|
|
13495
13887
|
}
|
|
13496
13888
|
const labels = [];
|
|
13497
13889
|
if (options.label) {
|
|
@@ -13700,11 +14092,11 @@ function boardCommand(program2) {
|
|
|
13700
14092
|
const provider = getProvider(config, cwd);
|
|
13701
14093
|
await ensureBoardConfigured(config, cwd, provider);
|
|
13702
14094
|
const roadmapPath = options.roadmap ?? path33.join(cwd, "ROADMAP.md");
|
|
13703
|
-
if (!
|
|
14095
|
+
if (!fs32.existsSync(roadmapPath)) {
|
|
13704
14096
|
console.error(`Roadmap file not found: ${roadmapPath}`);
|
|
13705
14097
|
process.exit(1);
|
|
13706
14098
|
}
|
|
13707
|
-
const roadmapContent =
|
|
14099
|
+
const roadmapContent = fs32.readFileSync(roadmapPath, "utf-8");
|
|
13708
14100
|
const items = parseRoadmap(roadmapContent);
|
|
13709
14101
|
const uncheckedItems = getUncheckedItems(items);
|
|
13710
14102
|
if (uncheckedItems.length === 0) {
|
|
@@ -13798,7 +14190,7 @@ function boardCommand(program2) {
|
|
|
13798
14190
|
try {
|
|
13799
14191
|
const labelsToAdd = [category, horizon].filter((l) => !issue.labels.includes(l));
|
|
13800
14192
|
if (labelsToAdd.length > 0) {
|
|
13801
|
-
|
|
14193
|
+
execFileSync3("gh", ["issue", "edit", String(issue.number), "--add-label", labelsToAdd.join(",")], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
13802
14194
|
}
|
|
13803
14195
|
updated++;
|
|
13804
14196
|
success(`Updated labels on #${issue.number}: ${item.title}`);
|
|
@@ -13822,14 +14214,14 @@ var __dirname4 = dirname8(__filename3);
|
|
|
13822
14214
|
function findPackageRoot(dir) {
|
|
13823
14215
|
let d = dir;
|
|
13824
14216
|
for (let i = 0; i < 5; i++) {
|
|
13825
|
-
if (
|
|
14217
|
+
if (existsSync26(join30(d, "package.json")))
|
|
13826
14218
|
return d;
|
|
13827
14219
|
d = dirname8(d);
|
|
13828
14220
|
}
|
|
13829
14221
|
return dir;
|
|
13830
14222
|
}
|
|
13831
14223
|
var packageRoot = findPackageRoot(__dirname4);
|
|
13832
|
-
var packageJson = JSON.parse(
|
|
14224
|
+
var packageJson = JSON.parse(readFileSync16(join30(packageRoot, "package.json"), "utf-8"));
|
|
13833
14225
|
var program = new Command2();
|
|
13834
14226
|
program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
|
|
13835
14227
|
initCommand(program);
|