@jonit-dev/night-watch-cli 1.7.47 → 1.7.49
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 +1098 -708
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -36
- package/dist/commands/init.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/review.d.ts +10 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +34 -0
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +5 -1
- package/dist/commands/run.js.map +1 -1
- package/dist/scripts/night-watch-cron.sh +96 -0
- package/dist/scripts/night-watch-helpers.sh +1 -1
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +168 -35
- 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
|
@@ -7,15 +7,6 @@ import "reflect-metadata";
|
|
|
7
7
|
import "reflect-metadata";
|
|
8
8
|
import "reflect-metadata";
|
|
9
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
|
-
import "reflect-metadata";
|
|
16
|
-
import "reflect-metadata";
|
|
17
|
-
import "reflect-metadata";
|
|
18
|
-
import "reflect-metadata";
|
|
19
10
|
import * as fs from "fs";
|
|
20
11
|
import * as path from "path";
|
|
21
12
|
import { fileURLToPath } from "url";
|
|
@@ -38,8 +29,10 @@ import * as path2 from "path";
|
|
|
38
29
|
import Database7 from "better-sqlite3";
|
|
39
30
|
import "reflect-metadata";
|
|
40
31
|
import { container } from "tsyringe";
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
32
|
+
import { execFile } from "child_process";
|
|
33
|
+
import { promisify } from "util";
|
|
34
|
+
import { execFile as execFile2 } from "child_process";
|
|
35
|
+
import { promisify as promisify2 } from "util";
|
|
43
36
|
import * as fs3 from "fs";
|
|
44
37
|
import * as path3 from "path";
|
|
45
38
|
import { execSync } from "child_process";
|
|
@@ -47,18 +40,19 @@ import * as fs4 from "fs";
|
|
|
47
40
|
import * as os2 from "os";
|
|
48
41
|
import * as path4 from "path";
|
|
49
42
|
import { createHash } from "crypto";
|
|
50
|
-
import {
|
|
43
|
+
import { exec } from "child_process";
|
|
44
|
+
import { promisify as promisify3 } from "util";
|
|
51
45
|
import * as fs5 from "fs";
|
|
52
46
|
import * as path5 from "path";
|
|
53
47
|
import * as fs6 from "fs";
|
|
54
48
|
import * as fs7 from "fs";
|
|
55
49
|
import * as path6 from "path";
|
|
56
|
-
import { execSync as
|
|
50
|
+
import { execSync as execSync2 } from "child_process";
|
|
57
51
|
import * as fs8 from "fs";
|
|
58
52
|
import * as path7 from "path";
|
|
59
53
|
import * as os3 from "os";
|
|
60
54
|
import * as path8 from "path";
|
|
61
|
-
import { execFileSync
|
|
55
|
+
import { execFileSync } from "child_process";
|
|
62
56
|
import chalk from "chalk";
|
|
63
57
|
import ora from "ora";
|
|
64
58
|
import Table from "cli-table3";
|
|
@@ -74,26 +68,27 @@ import * as path11 from "path";
|
|
|
74
68
|
import * as fs13 from "fs";
|
|
75
69
|
import * as path12 from "path";
|
|
76
70
|
import { spawn } from "child_process";
|
|
71
|
+
import { createHash as createHash3 } from "crypto";
|
|
77
72
|
import { spawn as spawn2 } from "child_process";
|
|
78
73
|
import "reflect-metadata";
|
|
79
74
|
import { Command as Command2 } from "commander";
|
|
80
|
-
import { existsSync as
|
|
75
|
+
import { existsSync as existsSync26, readFileSync as readFileSync16 } from "fs";
|
|
81
76
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
82
77
|
import { dirname as dirname8, join as join30 } from "path";
|
|
83
78
|
import fs14 from "fs";
|
|
84
79
|
import path13 from "path";
|
|
85
|
-
import { execSync as
|
|
80
|
+
import { execSync as execSync3 } from "child_process";
|
|
86
81
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
87
82
|
import { dirname as dirname4, join as join13 } from "path";
|
|
88
83
|
import * as readline from "readline";
|
|
89
84
|
import * as fs15 from "fs";
|
|
90
85
|
import * as path14 from "path";
|
|
91
|
-
import { execFileSync as
|
|
86
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
92
87
|
import * as path15 from "path";
|
|
93
88
|
import * as path16 from "path";
|
|
94
89
|
import * as fs16 from "fs";
|
|
95
90
|
import * as path17 from "path";
|
|
96
|
-
import { execSync as
|
|
91
|
+
import { execSync as execSync4 } from "child_process";
|
|
97
92
|
import * as path18 from "path";
|
|
98
93
|
import * as fs17 from "fs";
|
|
99
94
|
import * as path19 from "path";
|
|
@@ -134,7 +129,7 @@ import { Router as Router3 } from "express";
|
|
|
134
129
|
import { Router as Router4 } from "express";
|
|
135
130
|
import * as fs25 from "fs";
|
|
136
131
|
import * as path25 from "path";
|
|
137
|
-
import { execSync as
|
|
132
|
+
import { execSync as execSync5 } from "child_process";
|
|
138
133
|
import { Router as Router5 } from "express";
|
|
139
134
|
import * as path26 from "path";
|
|
140
135
|
import { Router as Router6 } from "express";
|
|
@@ -150,16 +145,17 @@ import * as fs29 from "fs";
|
|
|
150
145
|
import * as path30 from "path";
|
|
151
146
|
import chalk3 from "chalk";
|
|
152
147
|
import chalk4 from "chalk";
|
|
153
|
-
import { execSync as
|
|
148
|
+
import { execSync as execSync6 } from "child_process";
|
|
154
149
|
import * as fs30 from "fs";
|
|
155
150
|
import * as readline3 from "readline";
|
|
151
|
+
import * as fs31 from "fs";
|
|
156
152
|
import * as path31 from "path";
|
|
157
153
|
import * as os5 from "os";
|
|
158
154
|
import * as path32 from "path";
|
|
159
155
|
import chalk5 from "chalk";
|
|
160
156
|
import { Command } from "commander";
|
|
161
|
-
import { execFileSync as
|
|
162
|
-
import * as
|
|
157
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
158
|
+
import * as fs32 from "fs";
|
|
163
159
|
import * as path33 from "path";
|
|
164
160
|
import * as readline4 from "readline";
|
|
165
161
|
import chalk6 from "chalk";
|
|
@@ -185,11 +181,14 @@ var DEFAULT_CRON_SCHEDULE;
|
|
|
185
181
|
var DEFAULT_REVIEWER_SCHEDULE;
|
|
186
182
|
var DEFAULT_CRON_SCHEDULE_OFFSET;
|
|
187
183
|
var DEFAULT_MAX_RETRIES;
|
|
184
|
+
var DEFAULT_REVIEWER_MAX_RETRIES;
|
|
185
|
+
var DEFAULT_REVIEWER_RETRY_DELAY;
|
|
188
186
|
var DEFAULT_BRANCH_PREFIX;
|
|
189
187
|
var DEFAULT_BRANCH_PATTERNS;
|
|
190
188
|
var DEFAULT_MIN_REVIEW_SCORE;
|
|
191
189
|
var DEFAULT_MAX_LOG_SIZE;
|
|
192
190
|
var DEFAULT_PROVIDER;
|
|
191
|
+
var DEFAULT_EXECUTOR_ENABLED;
|
|
193
192
|
var DEFAULT_REVIEWER_ENABLED;
|
|
194
193
|
var DEFAULT_PROVIDER_ENV;
|
|
195
194
|
var DEFAULT_FALLBACK_ON_RATE_LIMIT;
|
|
@@ -220,6 +219,7 @@ var DEFAULT_AUDIT_SCHEDULE;
|
|
|
220
219
|
var DEFAULT_AUDIT_MAX_RUNTIME;
|
|
221
220
|
var DEFAULT_AUDIT;
|
|
222
221
|
var AUDIT_LOG_NAME;
|
|
222
|
+
var PLANNER_LOG_NAME;
|
|
223
223
|
var VALID_PROVIDERS;
|
|
224
224
|
var VALID_JOB_TYPES;
|
|
225
225
|
var DEFAULT_JOB_PROVIDERS;
|
|
@@ -243,18 +243,21 @@ var init_constants = __esm({
|
|
|
243
243
|
"../core/dist/constants.js"() {
|
|
244
244
|
"use strict";
|
|
245
245
|
DEFAULT_DEFAULT_BRANCH = "";
|
|
246
|
-
DEFAULT_PRD_DIR = "docs/
|
|
246
|
+
DEFAULT_PRD_DIR = "docs/prds";
|
|
247
247
|
DEFAULT_MAX_RUNTIME = 7200;
|
|
248
248
|
DEFAULT_REVIEWER_MAX_RUNTIME = 3600;
|
|
249
249
|
DEFAULT_CRON_SCHEDULE = "0 0-21 * * *";
|
|
250
250
|
DEFAULT_REVIEWER_SCHEDULE = "0 0,3,6,9,12,15,18,21 * * *";
|
|
251
251
|
DEFAULT_CRON_SCHEDULE_OFFSET = 0;
|
|
252
252
|
DEFAULT_MAX_RETRIES = 3;
|
|
253
|
+
DEFAULT_REVIEWER_MAX_RETRIES = 2;
|
|
254
|
+
DEFAULT_REVIEWER_RETRY_DELAY = 30;
|
|
253
255
|
DEFAULT_BRANCH_PREFIX = "night-watch";
|
|
254
256
|
DEFAULT_BRANCH_PATTERNS = ["feat/", "night-watch/"];
|
|
255
257
|
DEFAULT_MIN_REVIEW_SCORE = 80;
|
|
256
258
|
DEFAULT_MAX_LOG_SIZE = 524288;
|
|
257
259
|
DEFAULT_PROVIDER = "claude";
|
|
260
|
+
DEFAULT_EXECUTOR_ENABLED = true;
|
|
258
261
|
DEFAULT_REVIEWER_ENABLED = true;
|
|
259
262
|
DEFAULT_PROVIDER_ENV = {};
|
|
260
263
|
DEFAULT_FALLBACK_ON_RATE_LIMIT = false;
|
|
@@ -269,7 +272,7 @@ var init_constants = __esm({
|
|
|
269
272
|
DEFAULT_SLICER_SCHEDULE = "0 */6 * * *";
|
|
270
273
|
DEFAULT_SLICER_MAX_RUNTIME = 600;
|
|
271
274
|
DEFAULT_ROADMAP_SCANNER = {
|
|
272
|
-
enabled:
|
|
275
|
+
enabled: true,
|
|
273
276
|
roadmapPath: "ROADMAP.md",
|
|
274
277
|
autoScanInterval: 300,
|
|
275
278
|
slicerSchedule: DEFAULT_SLICER_SCHEDULE,
|
|
@@ -309,6 +312,7 @@ var init_constants = __esm({
|
|
|
309
312
|
maxRuntime: DEFAULT_AUDIT_MAX_RUNTIME
|
|
310
313
|
};
|
|
311
314
|
AUDIT_LOG_NAME = "audit";
|
|
315
|
+
PLANNER_LOG_NAME = "slicer";
|
|
312
316
|
VALID_PROVIDERS = ["claude", "codex"];
|
|
313
317
|
VALID_JOB_TYPES = ["executor", "reviewer", "qa", "audit", "slicer"];
|
|
314
318
|
DEFAULT_JOB_PROVIDERS = {};
|
|
@@ -327,7 +331,9 @@ var init_constants = __esm({
|
|
|
327
331
|
LOG_FILE_NAMES = {
|
|
328
332
|
executor: EXECUTOR_LOG_NAME,
|
|
329
333
|
reviewer: REVIEWER_LOG_NAME,
|
|
330
|
-
qa: QA_LOG_NAME
|
|
334
|
+
qa: QA_LOG_NAME,
|
|
335
|
+
audit: AUDIT_LOG_NAME,
|
|
336
|
+
planner: PLANNER_LOG_NAME
|
|
331
337
|
};
|
|
332
338
|
GLOBAL_CONFIG_DIR = ".night-watch";
|
|
333
339
|
REGISTRY_FILE_NAME = "projects.json";
|
|
@@ -353,8 +359,12 @@ function getDefaultConfig() {
|
|
|
353
359
|
reviewerSchedule: DEFAULT_REVIEWER_SCHEDULE,
|
|
354
360
|
cronScheduleOffset: DEFAULT_CRON_SCHEDULE_OFFSET,
|
|
355
361
|
maxRetries: DEFAULT_MAX_RETRIES,
|
|
362
|
+
// Reviewer retry configuration
|
|
363
|
+
reviewerMaxRetries: DEFAULT_REVIEWER_MAX_RETRIES,
|
|
364
|
+
reviewerRetryDelay: DEFAULT_REVIEWER_RETRY_DELAY,
|
|
356
365
|
// Provider configuration
|
|
357
366
|
provider: DEFAULT_PROVIDER,
|
|
367
|
+
executorEnabled: DEFAULT_EXECUTOR_ENABLED,
|
|
358
368
|
reviewerEnabled: DEFAULT_REVIEWER_ENABLED,
|
|
359
369
|
providerEnv: { ...DEFAULT_PROVIDER_ENV },
|
|
360
370
|
// Notification configuration
|
|
@@ -416,7 +426,10 @@ function normalizeConfig(rawConfig) {
|
|
|
416
426
|
normalized.reviewerSchedule = readString(rawConfig.reviewerSchedule) ?? readString(cron?.reviewerSchedule);
|
|
417
427
|
normalized.cronScheduleOffset = readNumber(rawConfig.cronScheduleOffset);
|
|
418
428
|
normalized.maxRetries = readNumber(rawConfig.maxRetries);
|
|
429
|
+
normalized.reviewerMaxRetries = readNumber(rawConfig.reviewerMaxRetries);
|
|
430
|
+
normalized.reviewerRetryDelay = readNumber(rawConfig.reviewerRetryDelay);
|
|
419
431
|
normalized.provider = validateProvider(String(rawConfig.provider ?? "")) ?? void 0;
|
|
432
|
+
normalized.executorEnabled = readBoolean(rawConfig.executorEnabled);
|
|
420
433
|
normalized.reviewerEnabled = readBoolean(rawConfig.reviewerEnabled);
|
|
421
434
|
const rawProviderEnv = readObject(rawConfig.providerEnv);
|
|
422
435
|
if (rawProviderEnv) {
|
|
@@ -556,6 +569,28 @@ function sanitizeMaxRetries(value, fallback) {
|
|
|
556
569
|
const normalized = Math.floor(value);
|
|
557
570
|
return normalized >= 1 ? normalized : fallback;
|
|
558
571
|
}
|
|
572
|
+
function sanitizeReviewerMaxRetries(value, fallback) {
|
|
573
|
+
if (!Number.isFinite(value)) {
|
|
574
|
+
return fallback;
|
|
575
|
+
}
|
|
576
|
+
const normalized = Math.floor(value);
|
|
577
|
+
if (normalized < 0)
|
|
578
|
+
return 0;
|
|
579
|
+
if (normalized > 10)
|
|
580
|
+
return 10;
|
|
581
|
+
return normalized;
|
|
582
|
+
}
|
|
583
|
+
function sanitizeReviewerRetryDelay(value, fallback) {
|
|
584
|
+
if (!Number.isFinite(value)) {
|
|
585
|
+
return fallback;
|
|
586
|
+
}
|
|
587
|
+
const normalized = Math.floor(value);
|
|
588
|
+
if (normalized < 0)
|
|
589
|
+
return 0;
|
|
590
|
+
if (normalized > 300)
|
|
591
|
+
return 300;
|
|
592
|
+
return normalized;
|
|
593
|
+
}
|
|
559
594
|
function mergeConfigs(base, fileConfig, envConfig) {
|
|
560
595
|
const merged = { ...base };
|
|
561
596
|
if (fileConfig) {
|
|
@@ -583,8 +618,14 @@ function mergeConfigs(base, fileConfig, envConfig) {
|
|
|
583
618
|
merged.cronScheduleOffset = fileConfig.cronScheduleOffset;
|
|
584
619
|
if (fileConfig.maxRetries !== void 0)
|
|
585
620
|
merged.maxRetries = fileConfig.maxRetries;
|
|
621
|
+
if (fileConfig.reviewerMaxRetries !== void 0)
|
|
622
|
+
merged.reviewerMaxRetries = fileConfig.reviewerMaxRetries;
|
|
623
|
+
if (fileConfig.reviewerRetryDelay !== void 0)
|
|
624
|
+
merged.reviewerRetryDelay = fileConfig.reviewerRetryDelay;
|
|
586
625
|
if (fileConfig.provider !== void 0)
|
|
587
626
|
merged.provider = fileConfig.provider;
|
|
627
|
+
if (fileConfig.executorEnabled !== void 0)
|
|
628
|
+
merged.executorEnabled = fileConfig.executorEnabled;
|
|
588
629
|
if (fileConfig.reviewerEnabled !== void 0)
|
|
589
630
|
merged.reviewerEnabled = fileConfig.reviewerEnabled;
|
|
590
631
|
if (fileConfig.providerEnv !== void 0)
|
|
@@ -609,6 +650,8 @@ function mergeConfigs(base, fileConfig, envConfig) {
|
|
|
609
650
|
merged.claudeModel = fileConfig.claudeModel;
|
|
610
651
|
if (fileConfig.qa !== void 0)
|
|
611
652
|
merged.qa = { ...merged.qa, ...fileConfig.qa };
|
|
653
|
+
if (fileConfig.audit !== void 0)
|
|
654
|
+
merged.audit = { ...merged.audit, ...fileConfig.audit };
|
|
612
655
|
if (fileConfig.jobProviders !== void 0)
|
|
613
656
|
merged.jobProviders = { ...fileConfig.jobProviders };
|
|
614
657
|
}
|
|
@@ -636,8 +679,14 @@ function mergeConfigs(base, fileConfig, envConfig) {
|
|
|
636
679
|
merged.cronScheduleOffset = envConfig.cronScheduleOffset;
|
|
637
680
|
if (envConfig.maxRetries !== void 0)
|
|
638
681
|
merged.maxRetries = envConfig.maxRetries;
|
|
682
|
+
if (envConfig.reviewerMaxRetries !== void 0)
|
|
683
|
+
merged.reviewerMaxRetries = envConfig.reviewerMaxRetries;
|
|
684
|
+
if (envConfig.reviewerRetryDelay !== void 0)
|
|
685
|
+
merged.reviewerRetryDelay = envConfig.reviewerRetryDelay;
|
|
639
686
|
if (envConfig.provider !== void 0)
|
|
640
687
|
merged.provider = envConfig.provider;
|
|
688
|
+
if (envConfig.executorEnabled !== void 0)
|
|
689
|
+
merged.executorEnabled = envConfig.executorEnabled;
|
|
641
690
|
if (envConfig.reviewerEnabled !== void 0)
|
|
642
691
|
merged.reviewerEnabled = envConfig.reviewerEnabled;
|
|
643
692
|
if (envConfig.providerEnv !== void 0)
|
|
@@ -662,9 +711,13 @@ function mergeConfigs(base, fileConfig, envConfig) {
|
|
|
662
711
|
merged.claudeModel = envConfig.claudeModel;
|
|
663
712
|
if (envConfig.qa !== void 0)
|
|
664
713
|
merged.qa = { ...merged.qa, ...envConfig.qa };
|
|
714
|
+
if (envConfig.audit !== void 0)
|
|
715
|
+
merged.audit = { ...merged.audit, ...envConfig.audit };
|
|
665
716
|
if (envConfig.jobProviders !== void 0)
|
|
666
717
|
merged.jobProviders = { ...envConfig.jobProviders };
|
|
667
718
|
merged.maxRetries = sanitizeMaxRetries(merged.maxRetries, DEFAULT_MAX_RETRIES);
|
|
719
|
+
merged.reviewerMaxRetries = sanitizeReviewerMaxRetries(merged.reviewerMaxRetries, DEFAULT_REVIEWER_MAX_RETRIES);
|
|
720
|
+
merged.reviewerRetryDelay = sanitizeReviewerRetryDelay(merged.reviewerRetryDelay, DEFAULT_REVIEWER_RETRY_DELAY);
|
|
668
721
|
return merged;
|
|
669
722
|
}
|
|
670
723
|
function loadConfig(projectDir) {
|
|
@@ -730,6 +783,18 @@ function loadConfig(projectDir) {
|
|
|
730
783
|
envConfig.maxRetries = retries;
|
|
731
784
|
}
|
|
732
785
|
}
|
|
786
|
+
if (process.env.NW_REVIEWER_MAX_RETRIES !== void 0) {
|
|
787
|
+
const reviewerMaxRetries = parseInt(process.env.NW_REVIEWER_MAX_RETRIES, 10);
|
|
788
|
+
if (!isNaN(reviewerMaxRetries) && reviewerMaxRetries >= 0) {
|
|
789
|
+
envConfig.reviewerMaxRetries = reviewerMaxRetries;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (process.env.NW_REVIEWER_RETRY_DELAY !== void 0) {
|
|
793
|
+
const reviewerRetryDelay = parseInt(process.env.NW_REVIEWER_RETRY_DELAY, 10);
|
|
794
|
+
if (!isNaN(reviewerRetryDelay) && reviewerRetryDelay >= 0) {
|
|
795
|
+
envConfig.reviewerRetryDelay = reviewerRetryDelay;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
733
798
|
if (process.env.NW_PROVIDER) {
|
|
734
799
|
const provider = validateProvider(process.env.NW_PROVIDER);
|
|
735
800
|
if (provider !== null) {
|
|
@@ -742,6 +807,12 @@ function loadConfig(projectDir) {
|
|
|
742
807
|
envConfig.reviewerEnabled = reviewerEnabled;
|
|
743
808
|
}
|
|
744
809
|
}
|
|
810
|
+
if (process.env.NW_EXECUTOR_ENABLED) {
|
|
811
|
+
const executorEnabled = parseBoolean(process.env.NW_EXECUTOR_ENABLED);
|
|
812
|
+
if (executorEnabled !== null) {
|
|
813
|
+
envConfig.executorEnabled = executorEnabled;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
745
816
|
if (process.env.NW_NOTIFICATIONS) {
|
|
746
817
|
try {
|
|
747
818
|
const parsed = JSON.parse(process.env.NW_NOTIFICATIONS);
|
|
@@ -2365,7 +2436,7 @@ var init_container = __esm({
|
|
|
2365
2436
|
DATABASE_TOKEN = "Database";
|
|
2366
2437
|
}
|
|
2367
2438
|
});
|
|
2368
|
-
function graphql(query, variables, cwd) {
|
|
2439
|
+
async function graphql(query, variables, cwd) {
|
|
2369
2440
|
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
2370
2441
|
for (const [key, value] of Object.entries(variables)) {
|
|
2371
2442
|
if (typeof value === "number") {
|
|
@@ -2374,10 +2445,9 @@ function graphql(query, variables, cwd) {
|
|
|
2374
2445
|
args.push("-f", `${key}=${String(value)}`);
|
|
2375
2446
|
}
|
|
2376
2447
|
}
|
|
2377
|
-
const output =
|
|
2448
|
+
const { stdout: output } = await execFileAsync("gh", args, {
|
|
2378
2449
|
cwd,
|
|
2379
|
-
encoding: "utf-8"
|
|
2380
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2450
|
+
encoding: "utf-8"
|
|
2381
2451
|
});
|
|
2382
2452
|
const parsed = JSON.parse(output);
|
|
2383
2453
|
if (parsed.errors?.length) {
|
|
@@ -2385,25 +2455,29 @@ function graphql(query, variables, cwd) {
|
|
|
2385
2455
|
}
|
|
2386
2456
|
return parsed.data;
|
|
2387
2457
|
}
|
|
2388
|
-
function getRepoNwo(cwd) {
|
|
2389
|
-
const output =
|
|
2458
|
+
async function getRepoNwo(cwd) {
|
|
2459
|
+
const { stdout: output } = await execFileAsync("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], { cwd, encoding: "utf-8" });
|
|
2390
2460
|
return output.trim();
|
|
2391
2461
|
}
|
|
2392
|
-
function getViewerLogin(cwd) {
|
|
2393
|
-
const result = graphql(`query { viewer { login } }`, {}, cwd);
|
|
2462
|
+
async function getViewerLogin(cwd) {
|
|
2463
|
+
const result = await graphql(`query { viewer { login } }`, {}, cwd);
|
|
2394
2464
|
return result.viewer.login;
|
|
2395
2465
|
}
|
|
2466
|
+
var execFileAsync;
|
|
2396
2467
|
var init_github_graphql = __esm({
|
|
2397
2468
|
"../core/dist/board/providers/github-graphql.js"() {
|
|
2398
2469
|
"use strict";
|
|
2470
|
+
execFileAsync = promisify(execFile);
|
|
2399
2471
|
}
|
|
2400
2472
|
});
|
|
2473
|
+
var execFileAsync2;
|
|
2401
2474
|
var GitHubProjectsProvider;
|
|
2402
2475
|
var init_github_projects = __esm({
|
|
2403
2476
|
"../core/dist/board/providers/github-projects.js"() {
|
|
2404
2477
|
"use strict";
|
|
2405
2478
|
init_types2();
|
|
2406
2479
|
init_github_graphql();
|
|
2480
|
+
execFileAsync2 = promisify2(execFile2);
|
|
2407
2481
|
GitHubProjectsProvider = class {
|
|
2408
2482
|
config;
|
|
2409
2483
|
cwd;
|
|
@@ -2419,26 +2493,26 @@ var init_github_projects = __esm({
|
|
|
2419
2493
|
// -------------------------------------------------------------------------
|
|
2420
2494
|
// Helpers
|
|
2421
2495
|
// -------------------------------------------------------------------------
|
|
2422
|
-
getRepo() {
|
|
2496
|
+
async getRepo() {
|
|
2423
2497
|
return this.config.repo ?? getRepoNwo(this.cwd);
|
|
2424
2498
|
}
|
|
2425
|
-
getRepoParts() {
|
|
2426
|
-
const repo = this.getRepo();
|
|
2499
|
+
async getRepoParts() {
|
|
2500
|
+
const repo = await this.getRepo();
|
|
2427
2501
|
const [owner, name] = repo.split("/");
|
|
2428
2502
|
if (!owner || !name) {
|
|
2429
2503
|
throw new Error(`Invalid repository slug: "${repo}". Expected "owner/repo".`);
|
|
2430
2504
|
}
|
|
2431
2505
|
return { owner, name };
|
|
2432
2506
|
}
|
|
2433
|
-
getRepoOwnerLogin() {
|
|
2434
|
-
return this.getRepoParts().owner;
|
|
2507
|
+
async getRepoOwnerLogin() {
|
|
2508
|
+
return (await this.getRepoParts()).owner;
|
|
2435
2509
|
}
|
|
2436
|
-
getRepoOwner() {
|
|
2510
|
+
async getRepoOwner() {
|
|
2437
2511
|
if (this.cachedOwner && this.cachedRepositoryId) {
|
|
2438
2512
|
return this.cachedOwner;
|
|
2439
2513
|
}
|
|
2440
|
-
const { owner, name } = this.getRepoParts();
|
|
2441
|
-
const data = graphql(`query ResolveRepoOwner($owner: String!, $name: String!) {
|
|
2514
|
+
const { owner, name } = await this.getRepoParts();
|
|
2515
|
+
const data = await graphql(`query ResolveRepoOwner($owner: String!, $name: String!) {
|
|
2442
2516
|
repository(owner: $owner, name: $name) {
|
|
2443
2517
|
id
|
|
2444
2518
|
owner {
|
|
@@ -2463,20 +2537,20 @@ var init_github_projects = __esm({
|
|
|
2463
2537
|
};
|
|
2464
2538
|
return this.cachedOwner;
|
|
2465
2539
|
}
|
|
2466
|
-
getRepositoryNodeId() {
|
|
2540
|
+
async getRepositoryNodeId() {
|
|
2467
2541
|
if (this.cachedRepositoryId) {
|
|
2468
2542
|
return this.cachedRepositoryId;
|
|
2469
2543
|
}
|
|
2470
|
-
this.getRepoOwner();
|
|
2544
|
+
await this.getRepoOwner();
|
|
2471
2545
|
if (!this.cachedRepositoryId) {
|
|
2472
|
-
throw new Error(`Failed to resolve repository ID for ${this.getRepo()}.`);
|
|
2546
|
+
throw new Error(`Failed to resolve repository ID for ${await this.getRepo()}.`);
|
|
2473
2547
|
}
|
|
2474
2548
|
return this.cachedRepositoryId;
|
|
2475
2549
|
}
|
|
2476
|
-
linkProjectToRepository(projectId) {
|
|
2477
|
-
const repositoryId = this.getRepositoryNodeId();
|
|
2550
|
+
async linkProjectToRepository(projectId) {
|
|
2551
|
+
const repositoryId = await this.getRepositoryNodeId();
|
|
2478
2552
|
try {
|
|
2479
|
-
graphql(`mutation LinkProjectToRepository($projectId: ID!, $repositoryId: ID!) {
|
|
2553
|
+
await graphql(`mutation LinkProjectToRepository($projectId: ID!, $repositoryId: ID!) {
|
|
2480
2554
|
linkProjectV2ToRepository(input: { projectId: $projectId, repositoryId: $repositoryId }) {
|
|
2481
2555
|
repository {
|
|
2482
2556
|
id
|
|
@@ -2492,8 +2566,8 @@ var init_github_projects = __esm({
|
|
|
2492
2566
|
throw err;
|
|
2493
2567
|
}
|
|
2494
2568
|
}
|
|
2495
|
-
fetchStatusField(projectId) {
|
|
2496
|
-
const fieldData = graphql(`query GetStatusField($projectId: ID!) {
|
|
2569
|
+
async fetchStatusField(projectId) {
|
|
2570
|
+
const fieldData = await graphql(`query GetStatusField($projectId: ID!) {
|
|
2497
2571
|
node(id: $projectId) {
|
|
2498
2572
|
... on ProjectV2 {
|
|
2499
2573
|
field(name: "Status") {
|
|
@@ -2530,7 +2604,7 @@ var init_github_projects = __esm({
|
|
|
2530
2604
|
};
|
|
2531
2605
|
}
|
|
2532
2606
|
if (this.cachedProjectId !== null) {
|
|
2533
|
-
const statusField2 = this.fetchStatusField(this.cachedProjectId);
|
|
2607
|
+
const statusField2 = await this.fetchStatusField(this.cachedProjectId);
|
|
2534
2608
|
this.cachedFieldId = statusField2.fieldId;
|
|
2535
2609
|
this.cachedOptionIds = statusField2.optionIds;
|
|
2536
2610
|
return {
|
|
@@ -2543,23 +2617,23 @@ var init_github_projects = __esm({
|
|
|
2543
2617
|
if (!projectNumber) {
|
|
2544
2618
|
throw new Error("No projectNumber configured. Run `night-watch board setup` first.");
|
|
2545
2619
|
}
|
|
2546
|
-
const ownerLogins = /* @__PURE__ */ new Set([this.getRepoOwnerLogin()]);
|
|
2620
|
+
const ownerLogins = /* @__PURE__ */ new Set([await this.getRepoOwnerLogin()]);
|
|
2547
2621
|
try {
|
|
2548
|
-
ownerLogins.add(getViewerLogin(this.cwd));
|
|
2622
|
+
ownerLogins.add(await getViewerLogin(this.cwd));
|
|
2549
2623
|
} catch {
|
|
2550
2624
|
}
|
|
2551
2625
|
let projectNode = null;
|
|
2552
2626
|
for (const login of ownerLogins) {
|
|
2553
|
-
projectNode = this.fetchProjectNode(login, projectNumber);
|
|
2627
|
+
projectNode = await this.fetchProjectNode(login, projectNumber);
|
|
2554
2628
|
if (projectNode) {
|
|
2555
2629
|
break;
|
|
2556
2630
|
}
|
|
2557
2631
|
}
|
|
2558
2632
|
if (!projectNode) {
|
|
2559
|
-
throw new Error(`GitHub Project #${projectNumber} not found for repository owner "${this.getRepoOwnerLogin()}".`);
|
|
2633
|
+
throw new Error(`GitHub Project #${projectNumber} not found for repository owner "${await this.getRepoOwnerLogin()}".`);
|
|
2560
2634
|
}
|
|
2561
2635
|
this.cachedProjectId = projectNode.id;
|
|
2562
|
-
const statusField = this.fetchStatusField(projectNode.id);
|
|
2636
|
+
const statusField = await this.fetchStatusField(projectNode.id);
|
|
2563
2637
|
this.cachedFieldId = statusField.fieldId;
|
|
2564
2638
|
this.cachedOptionIds = statusField.optionIds;
|
|
2565
2639
|
return {
|
|
@@ -2569,9 +2643,9 @@ var init_github_projects = __esm({
|
|
|
2569
2643
|
};
|
|
2570
2644
|
}
|
|
2571
2645
|
/** Try user query first, fall back to org query. */
|
|
2572
|
-
fetchProjectNode(login, projectNumber) {
|
|
2646
|
+
async fetchProjectNode(login, projectNumber) {
|
|
2573
2647
|
try {
|
|
2574
|
-
const userData = graphql(`query GetProject($login: String!, $number: Int!) {
|
|
2648
|
+
const userData = await graphql(`query GetProject($login: String!, $number: Int!) {
|
|
2575
2649
|
user(login: $login) {
|
|
2576
2650
|
projectV2(number: $number) {
|
|
2577
2651
|
id
|
|
@@ -2587,7 +2661,7 @@ var init_github_projects = __esm({
|
|
|
2587
2661
|
} catch {
|
|
2588
2662
|
}
|
|
2589
2663
|
try {
|
|
2590
|
-
const orgData = graphql(`query GetOrgProject($login: String!, $number: Int!) {
|
|
2664
|
+
const orgData = await graphql(`query GetOrgProject($login: String!, $number: Int!) {
|
|
2591
2665
|
organization(login: $login) {
|
|
2592
2666
|
projectV2(number: $number) {
|
|
2593
2667
|
id
|
|
@@ -2633,6 +2707,117 @@ var init_github_projects = __esm({
|
|
|
2633
2707
|
assignees: content.assignees?.nodes.map((a) => a.login) ?? []
|
|
2634
2708
|
};
|
|
2635
2709
|
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Fetch ALL items from a GitHub ProjectV2 using cursor-based pagination.
|
|
2712
|
+
*
|
|
2713
|
+
* The API caps each page at 100 items. We loop until `hasNextPage` is false,
|
|
2714
|
+
* accumulating every item node so callers never see a truncated board.
|
|
2715
|
+
*/
|
|
2716
|
+
async fetchAllProjectItems(projectId) {
|
|
2717
|
+
const allNodes = [];
|
|
2718
|
+
let cursor = null;
|
|
2719
|
+
const query = `query GetProjectItems($projectId: ID!, $cursor: String) {
|
|
2720
|
+
node(id: $projectId) {
|
|
2721
|
+
... on ProjectV2 {
|
|
2722
|
+
items(first: 100, after: $cursor) {
|
|
2723
|
+
pageInfo {
|
|
2724
|
+
hasNextPage
|
|
2725
|
+
endCursor
|
|
2726
|
+
}
|
|
2727
|
+
nodes {
|
|
2728
|
+
id
|
|
2729
|
+
content {
|
|
2730
|
+
... on Issue {
|
|
2731
|
+
number
|
|
2732
|
+
title
|
|
2733
|
+
body
|
|
2734
|
+
url
|
|
2735
|
+
id
|
|
2736
|
+
labels(first: 10) { nodes { name } }
|
|
2737
|
+
assignees(first: 10) { nodes { login } }
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
fieldValues(first: 10) {
|
|
2741
|
+
nodes {
|
|
2742
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
2743
|
+
name
|
|
2744
|
+
field {
|
|
2745
|
+
... on ProjectV2SingleSelectField {
|
|
2746
|
+
name
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
}`;
|
|
2757
|
+
do {
|
|
2758
|
+
const variables = { projectId };
|
|
2759
|
+
if (cursor !== null) {
|
|
2760
|
+
variables.cursor = cursor;
|
|
2761
|
+
}
|
|
2762
|
+
const data = await graphql(query, variables, this.cwd);
|
|
2763
|
+
const page = data.node.items;
|
|
2764
|
+
allNodes.push(...page.nodes);
|
|
2765
|
+
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
|
|
2766
|
+
} while (cursor !== null);
|
|
2767
|
+
return allNodes;
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Fetch project items for moveIssue — only needs id, content.number, and
|
|
2771
|
+
* fieldValues. Uses the same paginated approach to ensure items beyond
|
|
2772
|
+
* position 100 are reachable.
|
|
2773
|
+
*/
|
|
2774
|
+
async fetchAllProjectItemsForMove(projectId) {
|
|
2775
|
+
const allNodes = [];
|
|
2776
|
+
let cursor = null;
|
|
2777
|
+
const query = `query GetProjectItemsForMove($projectId: ID!, $cursor: String) {
|
|
2778
|
+
node(id: $projectId) {
|
|
2779
|
+
... on ProjectV2 {
|
|
2780
|
+
items(first: 100, after: $cursor) {
|
|
2781
|
+
pageInfo {
|
|
2782
|
+
hasNextPage
|
|
2783
|
+
endCursor
|
|
2784
|
+
}
|
|
2785
|
+
nodes {
|
|
2786
|
+
id
|
|
2787
|
+
content {
|
|
2788
|
+
... on Issue {
|
|
2789
|
+
number
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
fieldValues(first: 10) {
|
|
2793
|
+
nodes {
|
|
2794
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
2795
|
+
name
|
|
2796
|
+
field {
|
|
2797
|
+
... on ProjectV2SingleSelectField {
|
|
2798
|
+
name
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
}`;
|
|
2809
|
+
do {
|
|
2810
|
+
const variables = { projectId };
|
|
2811
|
+
if (cursor !== null) {
|
|
2812
|
+
variables.cursor = cursor;
|
|
2813
|
+
}
|
|
2814
|
+
const data = await graphql(query, variables, this.cwd);
|
|
2815
|
+
const page = data.node.items;
|
|
2816
|
+
allNodes.push(...page.nodes);
|
|
2817
|
+
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
|
|
2818
|
+
} while (cursor !== null);
|
|
2819
|
+
return allNodes;
|
|
2820
|
+
}
|
|
2636
2821
|
// -------------------------------------------------------------------------
|
|
2637
2822
|
// IBoardProvider implementation
|
|
2638
2823
|
// -------------------------------------------------------------------------
|
|
@@ -2640,10 +2825,10 @@ var init_github_projects = __esm({
|
|
|
2640
2825
|
* Find an existing project by title among the repository owner's first 50 projects.
|
|
2641
2826
|
* Returns null if not found.
|
|
2642
2827
|
*/
|
|
2643
|
-
findExistingProject(owner, title) {
|
|
2828
|
+
async findExistingProject(owner, title) {
|
|
2644
2829
|
try {
|
|
2645
2830
|
if (owner.type === "User") {
|
|
2646
|
-
const data2 = graphql(`query ListUserProjects($login: String!) {
|
|
2831
|
+
const data2 = await graphql(`query ListUserProjects($login: String!) {
|
|
2647
2832
|
user(login: $login) {
|
|
2648
2833
|
projectsV2(first: 50) {
|
|
2649
2834
|
nodes { id number title url }
|
|
@@ -2652,7 +2837,7 @@ var init_github_projects = __esm({
|
|
|
2652
2837
|
}`, { login: owner.login }, this.cwd);
|
|
2653
2838
|
return data2.user?.projectsV2.nodes.find((p) => p.title === title) ?? null;
|
|
2654
2839
|
}
|
|
2655
|
-
const data = graphql(`query ListOrgProjects($login: String!) {
|
|
2840
|
+
const data = await graphql(`query ListOrgProjects($login: String!) {
|
|
2656
2841
|
organization(login: $login) {
|
|
2657
2842
|
projectsV2(first: 50) {
|
|
2658
2843
|
nodes { id number title url }
|
|
@@ -2668,8 +2853,8 @@ var init_github_projects = __esm({
|
|
|
2668
2853
|
* Ensure the Status field on an existing project has all five Night Watch
|
|
2669
2854
|
* lifecycle columns, updating it via GraphQL if any are missing.
|
|
2670
2855
|
*/
|
|
2671
|
-
ensureStatusColumns(projectId) {
|
|
2672
|
-
const fieldData = graphql(`query GetStatusField($projectId: ID!) {
|
|
2856
|
+
async ensureStatusColumns(projectId) {
|
|
2857
|
+
const fieldData = await graphql(`query GetStatusField($projectId: ID!) {
|
|
2673
2858
|
node(id: $projectId) {
|
|
2674
2859
|
... on ProjectV2 {
|
|
2675
2860
|
field(name: "Status") {
|
|
@@ -2701,7 +2886,7 @@ var init_github_projects = __esm({
|
|
|
2701
2886
|
color: colorMap[name],
|
|
2702
2887
|
description: ""
|
|
2703
2888
|
}));
|
|
2704
|
-
graphql(`mutation UpdateField($fieldId: ID!) {
|
|
2889
|
+
await graphql(`mutation UpdateField($fieldId: ID!) {
|
|
2705
2890
|
updateProjectV2Field(input: {
|
|
2706
2891
|
fieldId: $fieldId,
|
|
2707
2892
|
singleSelectOptions: [
|
|
@@ -2722,15 +2907,15 @@ var init_github_projects = __esm({
|
|
|
2722
2907
|
}`, { fieldId: field.id, allOptions }, this.cwd);
|
|
2723
2908
|
}
|
|
2724
2909
|
async setupBoard(title) {
|
|
2725
|
-
const owner = this.getRepoOwner();
|
|
2726
|
-
const existing = this.findExistingProject(owner, title);
|
|
2910
|
+
const owner = await this.getRepoOwner();
|
|
2911
|
+
const existing = await this.findExistingProject(owner, title);
|
|
2727
2912
|
if (existing) {
|
|
2728
2913
|
this.cachedProjectId = existing.id;
|
|
2729
|
-
this.linkProjectToRepository(existing.id);
|
|
2730
|
-
this.ensureStatusColumns(existing.id);
|
|
2914
|
+
await this.linkProjectToRepository(existing.id);
|
|
2915
|
+
await this.ensureStatusColumns(existing.id);
|
|
2731
2916
|
return { id: existing.id, number: existing.number, title: existing.title, url: existing.url };
|
|
2732
2917
|
}
|
|
2733
|
-
const createData = graphql(`mutation CreateProject($ownerId: ID!, $title: String!) {
|
|
2918
|
+
const createData = await graphql(`mutation CreateProject($ownerId: ID!, $title: String!) {
|
|
2734
2919
|
createProjectV2(input: { ownerId: $ownerId, title: $title }) {
|
|
2735
2920
|
projectV2 {
|
|
2736
2921
|
id
|
|
@@ -2742,13 +2927,13 @@ var init_github_projects = __esm({
|
|
|
2742
2927
|
}`, { ownerId: owner.id, title }, this.cwd);
|
|
2743
2928
|
const project = createData.createProjectV2.projectV2;
|
|
2744
2929
|
this.cachedProjectId = project.id;
|
|
2745
|
-
this.linkProjectToRepository(project.id);
|
|
2930
|
+
await this.linkProjectToRepository(project.id);
|
|
2746
2931
|
try {
|
|
2747
|
-
const statusField = this.fetchStatusField(project.id);
|
|
2932
|
+
const statusField = await this.fetchStatusField(project.id);
|
|
2748
2933
|
this.cachedFieldId = statusField.fieldId;
|
|
2749
2934
|
this.cachedOptionIds = statusField.optionIds;
|
|
2750
|
-
this.ensureStatusColumns(project.id);
|
|
2751
|
-
const refreshed = this.fetchStatusField(project.id);
|
|
2935
|
+
await this.ensureStatusColumns(project.id);
|
|
2936
|
+
const refreshed = await this.fetchStatusField(project.id);
|
|
2752
2937
|
this.cachedFieldId = refreshed.fieldId;
|
|
2753
2938
|
this.cachedOptionIds = refreshed.optionIds;
|
|
2754
2939
|
} catch (err) {
|
|
@@ -2756,7 +2941,7 @@ var init_github_projects = __esm({
|
|
|
2756
2941
|
if (!message.includes("Status field not found")) {
|
|
2757
2942
|
throw err;
|
|
2758
2943
|
}
|
|
2759
|
-
const createFieldData = graphql(`mutation CreateStatusField($projectId: ID!) {
|
|
2944
|
+
const createFieldData = await graphql(`mutation CreateStatusField($projectId: ID!) {
|
|
2760
2945
|
createProjectV2Field(input: {
|
|
2761
2946
|
projectId: $projectId,
|
|
2762
2947
|
dataType: SINGLE_SELECT,
|
|
@@ -2789,14 +2974,14 @@ var init_github_projects = __esm({
|
|
|
2789
2974
|
return null;
|
|
2790
2975
|
}
|
|
2791
2976
|
try {
|
|
2792
|
-
const ownerLogins = /* @__PURE__ */ new Set([this.getRepoOwnerLogin()]);
|
|
2977
|
+
const ownerLogins = /* @__PURE__ */ new Set([await this.getRepoOwnerLogin()]);
|
|
2793
2978
|
try {
|
|
2794
|
-
ownerLogins.add(getViewerLogin(this.cwd));
|
|
2979
|
+
ownerLogins.add(await getViewerLogin(this.cwd));
|
|
2795
2980
|
} catch {
|
|
2796
2981
|
}
|
|
2797
2982
|
let node = null;
|
|
2798
2983
|
for (const login of ownerLogins) {
|
|
2799
|
-
node = this.fetchProjectNode(login, projectNumber);
|
|
2984
|
+
node = await this.fetchProjectNode(login, projectNumber);
|
|
2800
2985
|
if (node) {
|
|
2801
2986
|
break;
|
|
2802
2987
|
}
|
|
@@ -2817,7 +3002,7 @@ var init_github_projects = __esm({
|
|
|
2817
3002
|
}));
|
|
2818
3003
|
}
|
|
2819
3004
|
async createIssue(input) {
|
|
2820
|
-
const repo = this.getRepo();
|
|
3005
|
+
const repo = await this.getRepo();
|
|
2821
3006
|
const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
|
|
2822
3007
|
const issueArgs = [
|
|
2823
3008
|
"issue",
|
|
@@ -2832,19 +3017,20 @@ var init_github_projects = __esm({
|
|
|
2832
3017
|
if (input.labels && input.labels.length > 0) {
|
|
2833
3018
|
issueArgs.push("--label", input.labels.join(","));
|
|
2834
3019
|
}
|
|
2835
|
-
const
|
|
3020
|
+
const { stdout: issueUrlRaw } = await execFileAsync2("gh", issueArgs, {
|
|
2836
3021
|
cwd: this.cwd,
|
|
2837
|
-
encoding: "utf-8"
|
|
2838
|
-
|
|
2839
|
-
|
|
3022
|
+
encoding: "utf-8"
|
|
3023
|
+
});
|
|
3024
|
+
const issueUrl = issueUrlRaw.trim();
|
|
2840
3025
|
const issueNumber = parseInt(issueUrl.split("/").pop() ?? "", 10);
|
|
2841
3026
|
if (!issueNumber) {
|
|
2842
3027
|
throw new Error(`Failed to parse issue number from URL: ${issueUrl}`);
|
|
2843
3028
|
}
|
|
2844
3029
|
const [owner, repoName] = repo.split("/");
|
|
2845
|
-
const
|
|
3030
|
+
const { stdout: nodeIdRaw } = await execFileAsync2("gh", ["api", `repos/${owner}/${repoName}/issues/${issueNumber}`, "--jq", ".node_id"], { cwd: this.cwd, encoding: "utf-8" });
|
|
3031
|
+
const nodeIdOutput = nodeIdRaw.trim();
|
|
2846
3032
|
const issueJson = { number: issueNumber, id: nodeIdOutput, url: issueUrl };
|
|
2847
|
-
const addData = graphql(`mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
|
|
3033
|
+
const addData = await graphql(`mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
|
|
2848
3034
|
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
|
|
2849
3035
|
item {
|
|
2850
3036
|
id
|
|
@@ -2855,7 +3041,7 @@ var init_github_projects = __esm({
|
|
|
2855
3041
|
const targetColumn = input.column ?? "Draft";
|
|
2856
3042
|
const optionId = optionIds.get(targetColumn);
|
|
2857
3043
|
if (optionId) {
|
|
2858
|
-
graphql(`mutation UpdateItemField(
|
|
3044
|
+
await graphql(`mutation UpdateItemField(
|
|
2859
3045
|
$projectId: ID!,
|
|
2860
3046
|
$itemId: ID!,
|
|
2861
3047
|
$fieldId: ID!,
|
|
@@ -2889,10 +3075,10 @@ var init_github_projects = __esm({
|
|
|
2889
3075
|
};
|
|
2890
3076
|
}
|
|
2891
3077
|
async getIssue(issueNumber) {
|
|
2892
|
-
const repo = this.getRepo();
|
|
3078
|
+
const repo = await this.getRepo();
|
|
2893
3079
|
let rawIssue;
|
|
2894
3080
|
try {
|
|
2895
|
-
const output =
|
|
3081
|
+
const { stdout: output } = await execFileAsync2("gh", [
|
|
2896
3082
|
"issue",
|
|
2897
3083
|
"view",
|
|
2898
3084
|
String(issueNumber),
|
|
@@ -2900,7 +3086,7 @@ var init_github_projects = __esm({
|
|
|
2900
3086
|
repo,
|
|
2901
3087
|
"--json",
|
|
2902
3088
|
"number,title,body,url,id,labels,assignees"
|
|
2903
|
-
], { cwd: this.cwd, encoding: "utf-8"
|
|
3089
|
+
], { cwd: this.cwd, encoding: "utf-8" });
|
|
2904
3090
|
rawIssue = JSON.parse(output);
|
|
2905
3091
|
} catch {
|
|
2906
3092
|
return null;
|
|
@@ -2931,42 +3117,9 @@ var init_github_projects = __esm({
|
|
|
2931
3117
|
}
|
|
2932
3118
|
async getAllIssues() {
|
|
2933
3119
|
const { projectId } = await this.ensureProjectCache();
|
|
2934
|
-
const
|
|
2935
|
-
node(id: $projectId) {
|
|
2936
|
-
... on ProjectV2 {
|
|
2937
|
-
items(first: 100) {
|
|
2938
|
-
nodes {
|
|
2939
|
-
id
|
|
2940
|
-
content {
|
|
2941
|
-
... on Issue {
|
|
2942
|
-
number
|
|
2943
|
-
title
|
|
2944
|
-
body
|
|
2945
|
-
url
|
|
2946
|
-
id
|
|
2947
|
-
labels(first: 10) { nodes { name } }
|
|
2948
|
-
assignees(first: 10) { nodes { login } }
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
fieldValues(first: 10) {
|
|
2952
|
-
nodes {
|
|
2953
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
2954
|
-
name
|
|
2955
|
-
field {
|
|
2956
|
-
... on ProjectV2SingleSelectField {
|
|
2957
|
-
name
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
}
|
|
2961
|
-
}
|
|
2962
|
-
}
|
|
2963
|
-
}
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
}`, { projectId }, this.cwd);
|
|
3120
|
+
const allNodes = await this.fetchAllProjectItems(projectId);
|
|
2968
3121
|
const results = [];
|
|
2969
|
-
for (const item of
|
|
3122
|
+
for (const item of allNodes) {
|
|
2970
3123
|
const parsed = this.parseItem(item);
|
|
2971
3124
|
if (parsed) {
|
|
2972
3125
|
results.push(parsed);
|
|
@@ -2976,35 +3129,8 @@ var init_github_projects = __esm({
|
|
|
2976
3129
|
}
|
|
2977
3130
|
async moveIssue(issueNumber, targetColumn) {
|
|
2978
3131
|
const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
|
|
2979
|
-
const
|
|
2980
|
-
|
|
2981
|
-
... on ProjectV2 {
|
|
2982
|
-
items(first: 100) {
|
|
2983
|
-
nodes {
|
|
2984
|
-
id
|
|
2985
|
-
content {
|
|
2986
|
-
... on Issue {
|
|
2987
|
-
number
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2990
|
-
fieldValues(first: 10) {
|
|
2991
|
-
nodes {
|
|
2992
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
2993
|
-
name
|
|
2994
|
-
field {
|
|
2995
|
-
... on ProjectV2SingleSelectField {
|
|
2996
|
-
name
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
}
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
}`, { projectId }, this.cwd);
|
|
3007
|
-
const itemNode = data.node.items.nodes.find((n) => n.content?.number === issueNumber);
|
|
3132
|
+
const allNodes = await this.fetchAllProjectItemsForMove(projectId);
|
|
3133
|
+
const itemNode = allNodes.find((n) => n.content?.number === issueNumber);
|
|
3008
3134
|
if (!itemNode) {
|
|
3009
3135
|
throw new Error(`Issue #${issueNumber} not found on the project board.`);
|
|
3010
3136
|
}
|
|
@@ -3012,7 +3138,7 @@ var init_github_projects = __esm({
|
|
|
3012
3138
|
if (!optionId) {
|
|
3013
3139
|
throw new Error(`Column "${targetColumn}" not found on the project board.`);
|
|
3014
3140
|
}
|
|
3015
|
-
graphql(`mutation UpdateItemField(
|
|
3141
|
+
await graphql(`mutation UpdateItemField(
|
|
3016
3142
|
$projectId: ID!,
|
|
3017
3143
|
$itemId: ID!,
|
|
3018
3144
|
$fieldId: ID!,
|
|
@@ -3031,12 +3157,12 @@ var init_github_projects = __esm({
|
|
|
3031
3157
|
}`, { projectId, itemId: itemNode.id, fieldId, optionId }, this.cwd);
|
|
3032
3158
|
}
|
|
3033
3159
|
async closeIssue(issueNumber) {
|
|
3034
|
-
const repo = this.getRepo();
|
|
3035
|
-
|
|
3160
|
+
const repo = await this.getRepo();
|
|
3161
|
+
await execFileAsync2("gh", ["issue", "close", String(issueNumber), "--repo", repo], { cwd: this.cwd, encoding: "utf-8" });
|
|
3036
3162
|
}
|
|
3037
3163
|
async commentOnIssue(issueNumber, body) {
|
|
3038
|
-
const repo = this.getRepo();
|
|
3039
|
-
|
|
3164
|
+
const repo = await this.getRepo();
|
|
3165
|
+
await execFileAsync2("gh", ["issue", "comment", String(issueNumber), "--repo", repo, "--body", body], { cwd: this.cwd, encoding: "utf-8" });
|
|
3040
3166
|
}
|
|
3041
3167
|
};
|
|
3042
3168
|
}
|
|
@@ -3497,7 +3623,7 @@ function collectPrdDirs(projectPaths) {
|
|
|
3497
3623
|
const prdDirs = [];
|
|
3498
3624
|
for (const projectPath of projectPaths) {
|
|
3499
3625
|
const configPath = path3.join(projectPath, CONFIG_FILE_NAME);
|
|
3500
|
-
let prdDir = "docs/
|
|
3626
|
+
let prdDir = "docs/prds";
|
|
3501
3627
|
if (fs3.existsSync(configPath)) {
|
|
3502
3628
|
try {
|
|
3503
3629
|
const config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
@@ -4127,9 +4253,15 @@ function executorLockPath(projectDir) {
|
|
|
4127
4253
|
function reviewerLockPath(projectDir) {
|
|
4128
4254
|
return `${LOCK_FILE_PREFIX}pr-reviewer-${projectRuntimeKey(projectDir)}.lock`;
|
|
4129
4255
|
}
|
|
4256
|
+
function qaLockPath(projectDir) {
|
|
4257
|
+
return `${LOCK_FILE_PREFIX}qa-${projectRuntimeKey(projectDir)}.lock`;
|
|
4258
|
+
}
|
|
4130
4259
|
function auditLockPath(projectDir) {
|
|
4131
4260
|
return `${LOCK_FILE_PREFIX}audit-${projectRuntimeKey(projectDir)}.lock`;
|
|
4132
4261
|
}
|
|
4262
|
+
function plannerLockPath(projectDir) {
|
|
4263
|
+
return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
|
|
4264
|
+
}
|
|
4133
4265
|
function isProcessRunning(pid) {
|
|
4134
4266
|
try {
|
|
4135
4267
|
process.kill(pid, 0);
|
|
@@ -4306,24 +4438,26 @@ function collectPrdInfo(projectDir, prdDir, maxRuntime) {
|
|
|
4306
4438
|
}
|
|
4307
4439
|
return prds;
|
|
4308
4440
|
}
|
|
4309
|
-
function countOpenPRs(projectDir, branchPatterns) {
|
|
4441
|
+
async function countOpenPRs(projectDir, branchPatterns) {
|
|
4310
4442
|
try {
|
|
4311
|
-
|
|
4443
|
+
await execAsync("git rev-parse --git-dir", {
|
|
4312
4444
|
cwd: projectDir,
|
|
4313
|
-
encoding: "utf-8"
|
|
4314
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4445
|
+
encoding: "utf-8"
|
|
4315
4446
|
});
|
|
4316
4447
|
try {
|
|
4317
|
-
|
|
4448
|
+
await execAsync("which gh", { encoding: "utf-8" });
|
|
4318
4449
|
} catch {
|
|
4319
4450
|
return 0;
|
|
4320
4451
|
}
|
|
4321
|
-
const output =
|
|
4452
|
+
const { stdout: output } = await execAsync("gh pr list --state open --json headRefName,number", {
|
|
4322
4453
|
cwd: projectDir,
|
|
4323
|
-
encoding: "utf-8"
|
|
4324
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4454
|
+
encoding: "utf-8"
|
|
4325
4455
|
});
|
|
4326
|
-
const
|
|
4456
|
+
const trimmed = output.trim();
|
|
4457
|
+
if (!trimmed || trimmed === "[]") {
|
|
4458
|
+
return 0;
|
|
4459
|
+
}
|
|
4460
|
+
const prs = JSON.parse(trimmed);
|
|
4327
4461
|
const matchingPRs = prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern)));
|
|
4328
4462
|
return matchingPRs.length;
|
|
4329
4463
|
} catch {
|
|
@@ -4389,27 +4523,29 @@ function deriveReviewScore(reviewDecision) {
|
|
|
4389
4523
|
return null;
|
|
4390
4524
|
}
|
|
4391
4525
|
}
|
|
4392
|
-
function collectPrInfo(projectDir, branchPatterns) {
|
|
4526
|
+
async function collectPrInfo(projectDir, branchPatterns) {
|
|
4393
4527
|
try {
|
|
4394
|
-
|
|
4528
|
+
await execAsync("git rev-parse --git-dir", {
|
|
4395
4529
|
cwd: projectDir,
|
|
4396
|
-
encoding: "utf-8"
|
|
4397
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4530
|
+
encoding: "utf-8"
|
|
4398
4531
|
});
|
|
4399
4532
|
try {
|
|
4400
|
-
|
|
4533
|
+
await execAsync("which gh", { encoding: "utf-8" });
|
|
4401
4534
|
} catch {
|
|
4402
4535
|
return [];
|
|
4403
4536
|
}
|
|
4404
|
-
const output =
|
|
4537
|
+
const { stdout: output } = await execAsync("gh pr list --state open --json headRefName,number,title,url,statusCheckRollup,reviewDecision", {
|
|
4405
4538
|
cwd: projectDir,
|
|
4406
|
-
encoding: "utf-8"
|
|
4407
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4539
|
+
encoding: "utf-8"
|
|
4408
4540
|
});
|
|
4409
4541
|
if (process.env.DEBUG_PR_DATA === "1") {
|
|
4410
4542
|
console.error("[DEBUG] Raw gh pr list output:", output);
|
|
4411
4543
|
}
|
|
4412
|
-
const
|
|
4544
|
+
const trimmed = output.trim();
|
|
4545
|
+
if (!trimmed || trimmed === "[]") {
|
|
4546
|
+
return [];
|
|
4547
|
+
}
|
|
4548
|
+
const prs = JSON.parse(trimmed);
|
|
4413
4549
|
return prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern))).map((pr) => {
|
|
4414
4550
|
if (process.env.DEBUG_PR_DATA === "1") {
|
|
4415
4551
|
console.error(`[DEBUG] PR #${pr.number}:`);
|
|
@@ -4454,7 +4590,9 @@ function collectLogInfo(projectDir) {
|
|
|
4454
4590
|
const logEntries = [
|
|
4455
4591
|
{ name: "executor", fileName: "executor.log" },
|
|
4456
4592
|
{ name: "reviewer", fileName: "reviewer.log" },
|
|
4457
|
-
{ name: "qa", fileName: `${QA_LOG_NAME}.log` }
|
|
4593
|
+
{ name: "qa", fileName: `${QA_LOG_NAME}.log` },
|
|
4594
|
+
{ name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
|
|
4595
|
+
{ name: "planner", fileName: `${PLANNER_LOG_NAME}.log` }
|
|
4458
4596
|
];
|
|
4459
4597
|
return logEntries.map(({ name, fileName }) => {
|
|
4460
4598
|
const logPath = path5.join(projectDir, LOG_DIR, fileName);
|
|
@@ -4476,16 +4614,22 @@ function getCrontabInfo(projectName, projectDir) {
|
|
|
4476
4614
|
entries: crontabEntries
|
|
4477
4615
|
};
|
|
4478
4616
|
}
|
|
4479
|
-
function fetchStatusSnapshot(projectDir, config) {
|
|
4617
|
+
async function fetchStatusSnapshot(projectDir, config) {
|
|
4480
4618
|
const projectName = getProjectName(projectDir);
|
|
4481
4619
|
const executorLock = checkLockFile(executorLockPath(projectDir));
|
|
4482
4620
|
const reviewerLock = checkLockFile(reviewerLockPath(projectDir));
|
|
4621
|
+
const qaLock = checkLockFile(qaLockPath(projectDir));
|
|
4622
|
+
const auditLock = checkLockFile(auditLockPath(projectDir));
|
|
4623
|
+
const plannerLock = checkLockFile(plannerLockPath(projectDir));
|
|
4483
4624
|
const processes = [
|
|
4484
4625
|
{ name: "executor", running: executorLock.running, pid: executorLock.pid },
|
|
4485
|
-
{ name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid }
|
|
4626
|
+
{ name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
|
|
4627
|
+
{ name: "qa", running: qaLock.running, pid: qaLock.pid },
|
|
4628
|
+
{ name: "audit", running: auditLock.running, pid: auditLock.pid },
|
|
4629
|
+
{ name: "planner", running: plannerLock.running, pid: plannerLock.pid }
|
|
4486
4630
|
];
|
|
4487
4631
|
const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
|
|
4488
|
-
const prs = collectPrInfo(projectDir, config.branchPatterns);
|
|
4632
|
+
const prs = await collectPrInfo(projectDir, config.branchPatterns);
|
|
4489
4633
|
const logs = collectLogInfo(projectDir);
|
|
4490
4634
|
const crontab = getCrontabInfo(projectName, projectDir);
|
|
4491
4635
|
const activePrd = prds.find((p) => p.status === "in-progress")?.name ?? null;
|
|
@@ -4502,12 +4646,14 @@ function fetchStatusSnapshot(projectDir, config) {
|
|
|
4502
4646
|
timestamp: /* @__PURE__ */ new Date()
|
|
4503
4647
|
};
|
|
4504
4648
|
}
|
|
4649
|
+
var execAsync;
|
|
4505
4650
|
var init_status_data = __esm({
|
|
4506
4651
|
"../core/dist/utils/status-data.js"() {
|
|
4507
4652
|
"use strict";
|
|
4508
4653
|
init_constants();
|
|
4509
4654
|
init_prd_states();
|
|
4510
4655
|
init_crontab();
|
|
4656
|
+
execAsync = promisify3(exec);
|
|
4511
4657
|
}
|
|
4512
4658
|
});
|
|
4513
4659
|
function getLockFilePaths(projectDir) {
|
|
@@ -4626,7 +4772,7 @@ function checkGitRepo(cwd) {
|
|
|
4626
4772
|
}
|
|
4627
4773
|
function checkGhCli() {
|
|
4628
4774
|
try {
|
|
4629
|
-
|
|
4775
|
+
execSync2("gh auth status", {
|
|
4630
4776
|
encoding: "utf-8",
|
|
4631
4777
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4632
4778
|
});
|
|
@@ -4645,7 +4791,7 @@ function checkGhCli() {
|
|
|
4645
4791
|
}
|
|
4646
4792
|
function checkProviderCli(provider) {
|
|
4647
4793
|
try {
|
|
4648
|
-
|
|
4794
|
+
execSync2(`which ${provider}`, {
|
|
4649
4795
|
encoding: "utf-8",
|
|
4650
4796
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4651
4797
|
});
|
|
@@ -4666,7 +4812,7 @@ function detectProviders() {
|
|
|
4666
4812
|
const providers = [];
|
|
4667
4813
|
for (const provider of VALID_PROVIDERS) {
|
|
4668
4814
|
try {
|
|
4669
|
-
|
|
4815
|
+
execSync2(`which ${provider}`, {
|
|
4670
4816
|
encoding: "utf-8",
|
|
4671
4817
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4672
4818
|
});
|
|
@@ -4760,7 +4906,7 @@ function checkLogsDirectory(projectDir) {
|
|
|
4760
4906
|
}
|
|
4761
4907
|
function checkCrontabAccess() {
|
|
4762
4908
|
try {
|
|
4763
|
-
|
|
4909
|
+
execSync2("crontab -l", {
|
|
4764
4910
|
encoding: "utf-8",
|
|
4765
4911
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4766
4912
|
});
|
|
@@ -4910,7 +5056,7 @@ function parsePrDetails(raw) {
|
|
|
4910
5056
|
}
|
|
4911
5057
|
function fetchPrBySelector(selector, cwd) {
|
|
4912
5058
|
try {
|
|
4913
|
-
const output =
|
|
5059
|
+
const output = execFileSync("gh", ["pr", "view", selector, "--json", "number,title,url,body,additions,deletions,changedFiles"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
4914
5060
|
return parsePrDetails(output);
|
|
4915
5061
|
} catch {
|
|
4916
5062
|
return null;
|
|
@@ -4924,7 +5070,7 @@ function fetchPrDetailsByNumber(prNumber, cwd) {
|
|
|
4924
5070
|
}
|
|
4925
5071
|
function fetchPrDetails(branchPrefix, cwd) {
|
|
4926
5072
|
try {
|
|
4927
|
-
const listOutput =
|
|
5073
|
+
const listOutput = execFileSync("gh", ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "20"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
4928
5074
|
const prs = JSON.parse(listOutput);
|
|
4929
5075
|
const matching = prs.filter((pr) => pr.headRefName.startsWith(branchPrefix + "/"));
|
|
4930
5076
|
if (matching.length === 0) {
|
|
@@ -4938,7 +5084,7 @@ function fetchPrDetails(branchPrefix, cwd) {
|
|
|
4938
5084
|
}
|
|
4939
5085
|
function fetchReviewedPrDetails(branchPatterns, cwd) {
|
|
4940
5086
|
try {
|
|
4941
|
-
const listOutput =
|
|
5087
|
+
const listOutput = execFileSync("gh", ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "20"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
4942
5088
|
const prs = JSON.parse(listOutput);
|
|
4943
5089
|
const matching = prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern)));
|
|
4944
5090
|
if (matching.length === 0) {
|
|
@@ -5138,6 +5284,14 @@ function buildDescription(ctx) {
|
|
|
5138
5284
|
if (ctx.duration !== void 0) {
|
|
5139
5285
|
lines.push(`Duration: ${ctx.duration}s`);
|
|
5140
5286
|
}
|
|
5287
|
+
if (ctx.event === "review_completed" && ctx.attempts !== void 0 && ctx.attempts > 1) {
|
|
5288
|
+
const retryInfo = `Attempts: ${ctx.attempts}`;
|
|
5289
|
+
if (ctx.finalScore !== void 0) {
|
|
5290
|
+
lines.push(`${retryInfo} (final score: ${ctx.finalScore}/100)`);
|
|
5291
|
+
} else {
|
|
5292
|
+
lines.push(retryInfo);
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5141
5295
|
return lines.join("\n");
|
|
5142
5296
|
}
|
|
5143
5297
|
function escapeMarkdownV2(text) {
|
|
@@ -5192,7 +5346,7 @@ function formatDiscordPayload(ctx) {
|
|
|
5192
5346
|
}
|
|
5193
5347
|
function formatTelegramPayload(ctx) {
|
|
5194
5348
|
const emoji = getEventEmoji(ctx.event);
|
|
5195
|
-
const title = getEventTitle(ctx.event);
|
|
5349
|
+
const title = ctx.event === "run_succeeded" ? "PR Opened" : getEventTitle(ctx.event);
|
|
5196
5350
|
if (ctx.prUrl && ctx.prTitle) {
|
|
5197
5351
|
const lines = [];
|
|
5198
5352
|
lines.push(`*${escapeMarkdownV2(emoji + " " + title)}*`);
|
|
@@ -5219,6 +5373,14 @@ function formatTelegramPayload(ctx) {
|
|
|
5219
5373
|
}
|
|
5220
5374
|
lines.push(escapeMarkdownV2(stats.join(" | ")));
|
|
5221
5375
|
}
|
|
5376
|
+
if (ctx.event === "review_completed" && ctx.attempts !== void 0 && ctx.attempts > 1) {
|
|
5377
|
+
lines.push("");
|
|
5378
|
+
if (ctx.finalScore !== void 0) {
|
|
5379
|
+
lines.push(escapeMarkdownV2(`\u{1F501} Attempts: ${ctx.attempts} (final score: ${ctx.finalScore}/100)`));
|
|
5380
|
+
} else {
|
|
5381
|
+
lines.push(escapeMarkdownV2(`\u{1F501} Attempts: ${ctx.attempts}`));
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5222
5384
|
lines.push("");
|
|
5223
5385
|
lines.push(escapeMarkdownV2(`\u2699\uFE0F Project: ${ctx.projectName} | Provider: ${ctx.provider}`));
|
|
5224
5386
|
return {
|
|
@@ -5696,6 +5858,8 @@ The PRD directory is: \`{{PRD_DIR}}\`
|
|
|
5696
5858
|
|
|
5697
5859
|
## Your Task
|
|
5698
5860
|
|
|
5861
|
+
0. **Load Planner Skill** - Read and apply \`.claude/skills/prd-creator/SKILL.md\` before writing the PRD. If unavailable, continue with this template.
|
|
5862
|
+
|
|
5699
5863
|
1. **Explore the Codebase** - Read relevant existing files to understand the project structure, patterns, and conventions.
|
|
5700
5864
|
|
|
5701
5865
|
2. **Assess Complexity** - Score the complexity using the rubric and determine whether this is LOW, MEDIUM, or HIGH complexity.
|
|
@@ -5755,6 +5919,116 @@ DO NOT forget to write the file.
|
|
|
5755
5919
|
cachedTemplate = null;
|
|
5756
5920
|
}
|
|
5757
5921
|
});
|
|
5922
|
+
function normalizeAuditSeverity(raw) {
|
|
5923
|
+
const normalized = raw.trim().toLowerCase();
|
|
5924
|
+
if (normalized === "critical")
|
|
5925
|
+
return "critical";
|
|
5926
|
+
if (normalized === "high")
|
|
5927
|
+
return "high";
|
|
5928
|
+
if (normalized === "low")
|
|
5929
|
+
return "low";
|
|
5930
|
+
return "medium";
|
|
5931
|
+
}
|
|
5932
|
+
function extractAuditField(block, field) {
|
|
5933
|
+
const pattern = new RegExp(`- \\*\\*${field}\\*\\*:\\s*([\\s\\S]*?)(?=\\n- \\*\\*|\\n###\\s+Finding\\s+\\d+|$)`, "i");
|
|
5934
|
+
const match = block.match(pattern);
|
|
5935
|
+
if (!match)
|
|
5936
|
+
return "";
|
|
5937
|
+
return match[1].replace(/`/g, "").replace(/\r/g, "").trim();
|
|
5938
|
+
}
|
|
5939
|
+
function parseAuditFindings(reportContent) {
|
|
5940
|
+
const headingRegex = /^###\s+Finding\s+(\d+)\s*$/gm;
|
|
5941
|
+
const headings = [];
|
|
5942
|
+
let match;
|
|
5943
|
+
while ((match = headingRegex.exec(reportContent)) !== null) {
|
|
5944
|
+
const number = parseInt(match[1], 10);
|
|
5945
|
+
if (!Number.isNaN(number)) {
|
|
5946
|
+
headings.push({
|
|
5947
|
+
number,
|
|
5948
|
+
bodyStart: headingRegex.lastIndex,
|
|
5949
|
+
headingStart: match.index
|
|
5950
|
+
});
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
if (headings.length === 0) {
|
|
5954
|
+
return [];
|
|
5955
|
+
}
|
|
5956
|
+
const findings = [];
|
|
5957
|
+
for (let i = 0; i < headings.length; i++) {
|
|
5958
|
+
const current = headings[i];
|
|
5959
|
+
const next = headings[i + 1];
|
|
5960
|
+
const block = reportContent.slice(current.bodyStart, next?.headingStart ?? reportContent.length);
|
|
5961
|
+
const severityRaw = extractAuditField(block, "Severity");
|
|
5962
|
+
const category = extractAuditField(block, "Category") || "uncategorized";
|
|
5963
|
+
const location = extractAuditField(block, "Location") || "unknown location";
|
|
5964
|
+
const description = extractAuditField(block, "Description") || "No description provided";
|
|
5965
|
+
const suggestedFix = extractAuditField(block, "Suggested Fix") || "No suggested fix provided";
|
|
5966
|
+
findings.push({
|
|
5967
|
+
number: current.number,
|
|
5968
|
+
severity: normalizeAuditSeverity(severityRaw),
|
|
5969
|
+
category,
|
|
5970
|
+
location,
|
|
5971
|
+
description,
|
|
5972
|
+
suggestedFix
|
|
5973
|
+
});
|
|
5974
|
+
}
|
|
5975
|
+
return findings;
|
|
5976
|
+
}
|
|
5977
|
+
function auditSeverityRank(severity) {
|
|
5978
|
+
switch (severity) {
|
|
5979
|
+
case "critical":
|
|
5980
|
+
return 0;
|
|
5981
|
+
case "high":
|
|
5982
|
+
return 1;
|
|
5983
|
+
case "medium":
|
|
5984
|
+
return 2;
|
|
5985
|
+
case "low":
|
|
5986
|
+
return 3;
|
|
5987
|
+
default:
|
|
5988
|
+
return 4;
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
function auditFindingToRoadmapItem(finding) {
|
|
5992
|
+
const title = `Audit Finding ${finding.number}: ${finding.category} (${finding.severity}) at ${finding.location}`;
|
|
5993
|
+
const hashSource = [
|
|
5994
|
+
finding.severity,
|
|
5995
|
+
finding.category,
|
|
5996
|
+
finding.location,
|
|
5997
|
+
finding.description,
|
|
5998
|
+
finding.suggestedFix
|
|
5999
|
+
].join("|");
|
|
6000
|
+
const hash = createHash3("sha256").update(hashSource).digest("hex").slice(0, 8);
|
|
6001
|
+
const description = [
|
|
6002
|
+
"Source: logs/audit-report.md",
|
|
6003
|
+
`Severity: ${finding.severity}`,
|
|
6004
|
+
`Category: ${finding.category}`,
|
|
6005
|
+
`Location: ${finding.location}`,
|
|
6006
|
+
"",
|
|
6007
|
+
finding.description,
|
|
6008
|
+
"",
|
|
6009
|
+
`Suggested fix: ${finding.suggestedFix}`
|
|
6010
|
+
].join("\n");
|
|
6011
|
+
return {
|
|
6012
|
+
hash,
|
|
6013
|
+
title,
|
|
6014
|
+
description,
|
|
6015
|
+
checked: false,
|
|
6016
|
+
section: "Audit Findings"
|
|
6017
|
+
};
|
|
6018
|
+
}
|
|
6019
|
+
function collectAuditPlannerItems(projectDir) {
|
|
6020
|
+
const reportPath = path12.join(projectDir, "logs", "audit-report.md");
|
|
6021
|
+
if (!fs13.existsSync(reportPath)) {
|
|
6022
|
+
return [];
|
|
6023
|
+
}
|
|
6024
|
+
const reportContent = fs13.readFileSync(reportPath, "utf-8");
|
|
6025
|
+
if (!reportContent.trim() || /\bNO_ISSUES_FOUND\b/.test(reportContent)) {
|
|
6026
|
+
return [];
|
|
6027
|
+
}
|
|
6028
|
+
const findings = parseAuditFindings(reportContent);
|
|
6029
|
+
findings.sort((a, b) => auditSeverityRank(a.severity) - auditSeverityRank(b.severity) || a.number - b.number);
|
|
6030
|
+
return findings.map(auditFindingToRoadmapItem);
|
|
6031
|
+
}
|
|
5758
6032
|
function getRoadmapStatus(projectDir, config) {
|
|
5759
6033
|
const roadmapPath = path12.join(projectDir, config.roadmapScanner.roadmapPath);
|
|
5760
6034
|
const scannerEnabled = config.roadmapScanner.enabled;
|
|
@@ -5920,15 +6194,16 @@ async function sliceNextItem(projectDir, config) {
|
|
|
5920
6194
|
};
|
|
5921
6195
|
}
|
|
5922
6196
|
const roadmapPath = path12.join(projectDir, config.roadmapScanner.roadmapPath);
|
|
5923
|
-
|
|
6197
|
+
const auditItems = collectAuditPlannerItems(projectDir);
|
|
6198
|
+
const roadmapExists = fs13.existsSync(roadmapPath);
|
|
6199
|
+
if (!roadmapExists && auditItems.length === 0) {
|
|
5924
6200
|
return {
|
|
5925
6201
|
sliced: false,
|
|
5926
6202
|
error: "ROADMAP.md not found"
|
|
5927
6203
|
};
|
|
5928
6204
|
}
|
|
5929
|
-
const
|
|
5930
|
-
|
|
5931
|
-
if (items.length === 0) {
|
|
6205
|
+
const roadmapItems = roadmapExists ? parseRoadmap(fs13.readFileSync(roadmapPath, "utf-8")) : [];
|
|
6206
|
+
if (roadmapExists && roadmapItems.length === 0 && auditItems.length === 0) {
|
|
5932
6207
|
return {
|
|
5933
6208
|
sliced: false,
|
|
5934
6209
|
error: "No items in roadmap"
|
|
@@ -5937,21 +6212,23 @@ async function sliceNextItem(projectDir, config) {
|
|
|
5937
6212
|
const prdDir = path12.join(projectDir, config.prdDir);
|
|
5938
6213
|
const state = loadRoadmapState(prdDir);
|
|
5939
6214
|
const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
6215
|
+
const pickEligibleItem = (items) => {
|
|
6216
|
+
for (const item of items) {
|
|
6217
|
+
if (item.checked) {
|
|
6218
|
+
continue;
|
|
6219
|
+
}
|
|
6220
|
+
if (isItemProcessed(state, item.hash)) {
|
|
6221
|
+
continue;
|
|
6222
|
+
}
|
|
6223
|
+
const itemSlug = slugify(item.title);
|
|
6224
|
+
if (existingPrdSlugs.has(itemSlug)) {
|
|
6225
|
+
continue;
|
|
6226
|
+
}
|
|
6227
|
+
return item;
|
|
5951
6228
|
}
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
6229
|
+
return void 0;
|
|
6230
|
+
};
|
|
6231
|
+
const targetItem = pickEligibleItem(auditItems) ?? pickEligibleItem(roadmapItems);
|
|
5955
6232
|
if (!targetItem) {
|
|
5956
6233
|
return {
|
|
5957
6234
|
sliced: false,
|
|
@@ -6050,11 +6327,11 @@ var init_script_result = __esm({
|
|
|
6050
6327
|
RESULT_PREFIX = "NIGHT_WATCH_RESULT:";
|
|
6051
6328
|
}
|
|
6052
6329
|
});
|
|
6053
|
-
async function executeScript(scriptPath, args = [], env = {}) {
|
|
6054
|
-
const result = await executeScriptWithOutput(scriptPath, args, env);
|
|
6330
|
+
async function executeScript(scriptPath, args = [], env = {}, options = {}) {
|
|
6331
|
+
const result = await executeScriptWithOutput(scriptPath, args, env, options);
|
|
6055
6332
|
return result.exitCode;
|
|
6056
6333
|
}
|
|
6057
|
-
async function executeScriptWithOutput(scriptPath, args = [], env = {}) {
|
|
6334
|
+
async function executeScriptWithOutput(scriptPath, args = [], env = {}, options = {}) {
|
|
6058
6335
|
return new Promise((resolve9, reject) => {
|
|
6059
6336
|
const childEnv = {
|
|
6060
6337
|
...process.env,
|
|
@@ -6064,6 +6341,7 @@ async function executeScriptWithOutput(scriptPath, args = [], env = {}) {
|
|
|
6064
6341
|
const stderrChunks = [];
|
|
6065
6342
|
const child = spawn2("bash", [scriptPath, ...args], {
|
|
6066
6343
|
env: childEnv,
|
|
6344
|
+
cwd: options.cwd,
|
|
6067
6345
|
stdio: ["inherit", "pipe", "pipe"]
|
|
6068
6346
|
});
|
|
6069
6347
|
child.stdout?.on("data", (data) => {
|
|
@@ -6342,6 +6620,7 @@ __export(dist_exports, {
|
|
|
6342
6620
|
DEFAULT_CRON_SCHEDULE: () => DEFAULT_CRON_SCHEDULE,
|
|
6343
6621
|
DEFAULT_CRON_SCHEDULE_OFFSET: () => DEFAULT_CRON_SCHEDULE_OFFSET,
|
|
6344
6622
|
DEFAULT_DEFAULT_BRANCH: () => DEFAULT_DEFAULT_BRANCH,
|
|
6623
|
+
DEFAULT_EXECUTOR_ENABLED: () => DEFAULT_EXECUTOR_ENABLED,
|
|
6345
6624
|
DEFAULT_FALLBACK_ON_RATE_LIMIT: () => DEFAULT_FALLBACK_ON_RATE_LIMIT,
|
|
6346
6625
|
DEFAULT_JOB_PROVIDERS: () => DEFAULT_JOB_PROVIDERS,
|
|
6347
6626
|
DEFAULT_LOCAL_BOARD_INFO: () => DEFAULT_LOCAL_BOARD_INFO,
|
|
@@ -6362,7 +6641,9 @@ __export(dist_exports, {
|
|
|
6362
6641
|
DEFAULT_QA_SCHEDULE: () => DEFAULT_QA_SCHEDULE,
|
|
6363
6642
|
DEFAULT_QA_SKIP_LABEL: () => DEFAULT_QA_SKIP_LABEL,
|
|
6364
6643
|
DEFAULT_REVIEWER_ENABLED: () => DEFAULT_REVIEWER_ENABLED,
|
|
6644
|
+
DEFAULT_REVIEWER_MAX_RETRIES: () => DEFAULT_REVIEWER_MAX_RETRIES,
|
|
6365
6645
|
DEFAULT_REVIEWER_MAX_RUNTIME: () => DEFAULT_REVIEWER_MAX_RUNTIME,
|
|
6646
|
+
DEFAULT_REVIEWER_RETRY_DELAY: () => DEFAULT_REVIEWER_RETRY_DELAY,
|
|
6366
6647
|
DEFAULT_REVIEWER_SCHEDULE: () => DEFAULT_REVIEWER_SCHEDULE,
|
|
6367
6648
|
DEFAULT_ROADMAP_SCANNER: () => DEFAULT_ROADMAP_SCANNER,
|
|
6368
6649
|
DEFAULT_SLICER_MAX_RUNTIME: () => DEFAULT_SLICER_MAX_RUNTIME,
|
|
@@ -6381,6 +6662,7 @@ __export(dist_exports, {
|
|
|
6381
6662
|
Logger: () => Logger,
|
|
6382
6663
|
MAX_HISTORY_RECORDS_PER_PRD: () => MAX_HISTORY_RECORDS_PER_PRD,
|
|
6383
6664
|
NIGHT_WATCH_LABELS: () => NIGHT_WATCH_LABELS,
|
|
6665
|
+
PLANNER_LOG_NAME: () => PLANNER_LOG_NAME,
|
|
6384
6666
|
PRD_STATES_FILE_NAME: () => PRD_STATES_FILE_NAME,
|
|
6385
6667
|
PRD_TEMPLATE: () => PRD_TEMPLATE,
|
|
6386
6668
|
PRIORITY_LABELS: () => PRIORITY_LABELS,
|
|
@@ -6509,7 +6791,9 @@ __export(dist_exports, {
|
|
|
6509
6791
|
parseRoadmap: () => parseRoadmap,
|
|
6510
6792
|
parseScriptResult: () => parseScriptResult,
|
|
6511
6793
|
performCancel: () => performCancel,
|
|
6794
|
+
plannerLockPath: () => plannerLockPath,
|
|
6512
6795
|
projectRuntimeKey: () => projectRuntimeKey,
|
|
6796
|
+
qaLockPath: () => qaLockPath,
|
|
6513
6797
|
readCrontab: () => readCrontab,
|
|
6514
6798
|
readPrdStates: () => readPrdStates,
|
|
6515
6799
|
recordExecution: () => recordExecution,
|
|
@@ -6624,7 +6908,7 @@ function detectPlaywright(cwd) {
|
|
|
6624
6908
|
return true;
|
|
6625
6909
|
}
|
|
6626
6910
|
try {
|
|
6627
|
-
|
|
6911
|
+
execSync3("playwright --version", {
|
|
6628
6912
|
cwd,
|
|
6629
6913
|
encoding: "utf-8",
|
|
6630
6914
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -6668,12 +6952,12 @@ function promptYesNo(question, defaultNo = true) {
|
|
|
6668
6952
|
function installPlaywrightForQa(cwd) {
|
|
6669
6953
|
try {
|
|
6670
6954
|
const installCmd = resolvePlaywrightInstallCommand(cwd);
|
|
6671
|
-
|
|
6955
|
+
execSync3(installCmd, {
|
|
6672
6956
|
cwd,
|
|
6673
6957
|
encoding: "utf-8",
|
|
6674
6958
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6675
6959
|
});
|
|
6676
|
-
|
|
6960
|
+
execSync3("npx playwright install chromium", {
|
|
6677
6961
|
cwd,
|
|
6678
6962
|
encoding: "utf-8",
|
|
6679
6963
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6686,7 +6970,7 @@ function installPlaywrightForQa(cwd) {
|
|
|
6686
6970
|
function getDefaultBranch(cwd) {
|
|
6687
6971
|
const getRefTimestamp = (ref) => {
|
|
6688
6972
|
try {
|
|
6689
|
-
const timestamp =
|
|
6973
|
+
const timestamp = execSync3(`git log -1 --format=%ct ${ref}`, {
|
|
6690
6974
|
encoding: "utf-8",
|
|
6691
6975
|
cwd,
|
|
6692
6976
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6720,7 +7004,7 @@ function getDefaultBranch(cwd) {
|
|
|
6720
7004
|
if (masterTimestamp !== null) {
|
|
6721
7005
|
return "master";
|
|
6722
7006
|
}
|
|
6723
|
-
const remoteRef =
|
|
7007
|
+
const remoteRef = execSync3('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo ""', {
|
|
6724
7008
|
encoding: "utf-8",
|
|
6725
7009
|
cwd,
|
|
6726
7010
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6976,7 +7260,7 @@ function initCommand(program2) {
|
|
|
6976
7260
|
} else {
|
|
6977
7261
|
let hasGitHubRemote = false;
|
|
6978
7262
|
try {
|
|
6979
|
-
const remoteUrl =
|
|
7263
|
+
const remoteUrl = execSync3("git remote get-url origin", {
|
|
6980
7264
|
cwd,
|
|
6981
7265
|
encoding: "utf-8",
|
|
6982
7266
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7038,7 +7322,7 @@ function initCommand(program2) {
|
|
|
7038
7322
|
label("Reviewer", reviewerEnabled ? "Enabled" : "Disabled");
|
|
7039
7323
|
console.log();
|
|
7040
7324
|
header("Next Steps");
|
|
7041
|
-
info(
|
|
7325
|
+
info(`1. Add your PRD files to ${prdDir}/`);
|
|
7042
7326
|
info("2. Run `night-watch install` to set up cron jobs");
|
|
7043
7327
|
info("3. Or run `night-watch run` to execute PRDs manually");
|
|
7044
7328
|
console.log();
|
|
@@ -7058,10 +7342,10 @@ function resolveRunNotificationEvent(exitCode, scriptStatus) {
|
|
|
7058
7342
|
return null;
|
|
7059
7343
|
}
|
|
7060
7344
|
function shouldAttemptCrossProjectFallback(options, scriptStatus) {
|
|
7061
|
-
if (options.
|
|
7345
|
+
if (options.crossProjectFallback !== true) {
|
|
7062
7346
|
return false;
|
|
7063
7347
|
}
|
|
7064
|
-
if (options.
|
|
7348
|
+
if (options.dryRun) {
|
|
7065
7349
|
return false;
|
|
7066
7350
|
}
|
|
7067
7351
|
if (process.env.NW_CROSS_PROJECT_FALLBACK_ACTIVE === "1") {
|
|
@@ -7136,7 +7420,7 @@ async function runCrossProjectFallback(currentProjectDir, options) {
|
|
|
7136
7420
|
const envVars = buildEnvVars(candidateConfig, options);
|
|
7137
7421
|
envVars.NW_CROSS_PROJECT_FALLBACK_ACTIVE = "1";
|
|
7138
7422
|
try {
|
|
7139
|
-
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [candidate.path], envVars);
|
|
7423
|
+
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [candidate.path], envVars, { cwd: candidate.path });
|
|
7140
7424
|
const scriptResult = parseScriptResult(`${stdout}
|
|
7141
7425
|
${stderr}`);
|
|
7142
7426
|
if (!options.dryRun) {
|
|
@@ -7255,10 +7539,14 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
|
|
|
7255
7539
|
return { pending, completed };
|
|
7256
7540
|
}
|
|
7257
7541
|
function runCommand(program2) {
|
|
7258
|
-
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("--
|
|
7542
|
+
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) => {
|
|
7259
7543
|
const projectDir = process.cwd();
|
|
7260
7544
|
let config = loadConfig(projectDir);
|
|
7261
7545
|
config = applyCliOverrides(config, options);
|
|
7546
|
+
if (config.executorEnabled === false && !options.dryRun) {
|
|
7547
|
+
info("Executor is disabled in config; skipping run.");
|
|
7548
|
+
process.exit(0);
|
|
7549
|
+
}
|
|
7262
7550
|
const envVars = buildEnvVars(config, options);
|
|
7263
7551
|
const scriptPath = getScriptPath("night-watch-cron.sh");
|
|
7264
7552
|
if (options.dryRun) {
|
|
@@ -7348,7 +7636,7 @@ function runCommand(program2) {
|
|
|
7348
7636
|
const spinner = createSpinner("Running PRD executor...");
|
|
7349
7637
|
spinner.start();
|
|
7350
7638
|
try {
|
|
7351
|
-
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
|
|
7639
|
+
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars, { cwd: projectDir });
|
|
7352
7640
|
const scriptResult = parseScriptResult(`${stdout}
|
|
7353
7641
|
${stderr}`);
|
|
7354
7642
|
if (exitCode === 0) {
|
|
@@ -7392,6 +7680,23 @@ function parseAutoMergedPrNumbers(raw) {
|
|
|
7392
7680
|
}
|
|
7393
7681
|
return raw.split(",").map((token) => parseInt(token.trim().replace(/^#/, ""), 10)).filter((value) => !Number.isNaN(value));
|
|
7394
7682
|
}
|
|
7683
|
+
function parseRetryAttempts(raw) {
|
|
7684
|
+
if (!raw) {
|
|
7685
|
+
return 1;
|
|
7686
|
+
}
|
|
7687
|
+
const parsed = parseInt(raw, 10);
|
|
7688
|
+
return Number.isNaN(parsed) || parsed < 1 ? 1 : parsed;
|
|
7689
|
+
}
|
|
7690
|
+
function parseFinalReviewScore(raw) {
|
|
7691
|
+
if (!raw) {
|
|
7692
|
+
return void 0;
|
|
7693
|
+
}
|
|
7694
|
+
const parsed = parseInt(raw, 10);
|
|
7695
|
+
if (Number.isNaN(parsed)) {
|
|
7696
|
+
return void 0;
|
|
7697
|
+
}
|
|
7698
|
+
return parsed;
|
|
7699
|
+
}
|
|
7395
7700
|
function buildEnvVars2(config, options) {
|
|
7396
7701
|
const env = {};
|
|
7397
7702
|
const reviewerProvider = resolveJobProvider(config, "reviewer");
|
|
@@ -7400,6 +7705,8 @@ function buildEnvVars2(config, options) {
|
|
|
7400
7705
|
env.NW_DEFAULT_BRANCH = config.defaultBranch;
|
|
7401
7706
|
}
|
|
7402
7707
|
env.NW_REVIEWER_MAX_RUNTIME = String(config.reviewerMaxRuntime);
|
|
7708
|
+
env.NW_REVIEWER_MAX_RETRIES = String(config.reviewerMaxRetries);
|
|
7709
|
+
env.NW_REVIEWER_RETRY_DELAY = String(config.reviewerRetryDelay);
|
|
7403
7710
|
env.NW_MIN_REVIEW_SCORE = String(config.minReviewScore);
|
|
7404
7711
|
env.NW_BRANCH_PATTERNS = config.branchPatterns.join(",");
|
|
7405
7712
|
if (config.providerEnv) {
|
|
@@ -7412,10 +7719,6 @@ function buildEnvVars2(config, options) {
|
|
|
7412
7719
|
if (options.dryRun) {
|
|
7413
7720
|
env.NW_DRY_RUN = "1";
|
|
7414
7721
|
}
|
|
7415
|
-
if (config.autoMerge) {
|
|
7416
|
-
env.NW_AUTO_MERGE = "1";
|
|
7417
|
-
}
|
|
7418
|
-
env.NW_AUTO_MERGE_METHOD = config.autoMergeMethod;
|
|
7419
7722
|
env.NW_EXECUTION_CONTEXT = "agent";
|
|
7420
7723
|
return env;
|
|
7421
7724
|
}
|
|
@@ -7441,7 +7744,7 @@ function getOpenPrsNeedingWork(branchPatterns) {
|
|
|
7441
7744
|
for (const pattern of branchPatterns) {
|
|
7442
7745
|
args.push("--head", pattern);
|
|
7443
7746
|
}
|
|
7444
|
-
const result =
|
|
7747
|
+
const result = execFileSync2("gh", args, {
|
|
7445
7748
|
encoding: "utf-8",
|
|
7446
7749
|
stdio: ["pipe", "pipe", "pipe"]
|
|
7447
7750
|
});
|
|
@@ -7460,6 +7763,10 @@ function reviewCommand(program2) {
|
|
|
7460
7763
|
const projectDir = process.cwd();
|
|
7461
7764
|
let config = loadConfig(projectDir);
|
|
7462
7765
|
config = applyCliOverrides2(config, options);
|
|
7766
|
+
if (!config.reviewerEnabled && !options.dryRun) {
|
|
7767
|
+
info("Reviewer is disabled in config; skipping review.");
|
|
7768
|
+
process.exit(0);
|
|
7769
|
+
}
|
|
7463
7770
|
const envVars = buildEnvVars2(config, options);
|
|
7464
7771
|
const scriptPath = getScriptPath("night-watch-pr-reviewer-cron.sh");
|
|
7465
7772
|
if (options.dryRun) {
|
|
@@ -7479,6 +7786,8 @@ function reviewCommand(program2) {
|
|
|
7479
7786
|
"Auto-merge",
|
|
7480
7787
|
config.autoMerge ? `Enabled (${config.autoMergeMethod})` : "Disabled"
|
|
7481
7788
|
]);
|
|
7789
|
+
configTable.push(["Max Retry Attempts", String(config.reviewerMaxRetries)]);
|
|
7790
|
+
configTable.push(["Retry Delay", `${config.reviewerRetryDelay}s`]);
|
|
7482
7791
|
console.log(configTable.toString());
|
|
7483
7792
|
header("Open PRs Needing Work");
|
|
7484
7793
|
const openPrs = getOpenPrsNeedingWork(config.branchPatterns);
|
|
@@ -7538,6 +7847,8 @@ ${stderr}`);
|
|
|
7538
7847
|
}
|
|
7539
7848
|
}
|
|
7540
7849
|
if (!skipNotification) {
|
|
7850
|
+
const attempts = parseRetryAttempts(scriptResult?.data.attempts);
|
|
7851
|
+
const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
|
|
7541
7852
|
const _reviewCtx = {
|
|
7542
7853
|
event: "review_completed",
|
|
7543
7854
|
projectName: path15.basename(projectDir),
|
|
@@ -7549,7 +7860,9 @@ ${stderr}`);
|
|
|
7549
7860
|
prNumber: prDetails?.number,
|
|
7550
7861
|
filesChanged: prDetails?.changedFiles,
|
|
7551
7862
|
additions: prDetails?.additions,
|
|
7552
|
-
deletions: prDetails?.deletions
|
|
7863
|
+
deletions: prDetails?.deletions,
|
|
7864
|
+
attempts,
|
|
7865
|
+
finalScore
|
|
7553
7866
|
};
|
|
7554
7867
|
await sendNotifications(config, _reviewCtx);
|
|
7555
7868
|
}
|
|
@@ -7603,6 +7916,9 @@ function parseQaPrNumbers(prsRaw) {
|
|
|
7603
7916
|
}
|
|
7604
7917
|
return numbers;
|
|
7605
7918
|
}
|
|
7919
|
+
function getTelegramStatusWebhooks(config) {
|
|
7920
|
+
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 }));
|
|
7921
|
+
}
|
|
7606
7922
|
function buildEnvVars3(config, options) {
|
|
7607
7923
|
const env = {};
|
|
7608
7924
|
const qaProvider = resolveJobProvider(config, "qa");
|
|
@@ -7619,6 +7935,12 @@ function buildEnvVars3(config, options) {
|
|
|
7619
7935
|
if (config.providerEnv) {
|
|
7620
7936
|
Object.assign(env, config.providerEnv);
|
|
7621
7937
|
}
|
|
7938
|
+
const telegramWebhooks = getTelegramStatusWebhooks(config);
|
|
7939
|
+
if (telegramWebhooks.length > 0) {
|
|
7940
|
+
env.NW_TELEGRAM_STATUS_WEBHOOKS = JSON.stringify(telegramWebhooks);
|
|
7941
|
+
env.NW_TELEGRAM_BOT_TOKEN = telegramWebhooks[0].botToken;
|
|
7942
|
+
env.NW_TELEGRAM_CHAT_ID = telegramWebhooks[0].chatId;
|
|
7943
|
+
}
|
|
7622
7944
|
if (options.dryRun) {
|
|
7623
7945
|
env.NW_DRY_RUN = "1";
|
|
7624
7946
|
}
|
|
@@ -7643,6 +7965,10 @@ function qaCommand(program2) {
|
|
|
7643
7965
|
const projectDir = process.cwd();
|
|
7644
7966
|
let config = loadConfig(projectDir);
|
|
7645
7967
|
config = applyCliOverrides3(config, options);
|
|
7968
|
+
if (!config.qa.enabled && !options.dryRun) {
|
|
7969
|
+
info("QA is disabled in config; skipping run.");
|
|
7970
|
+
process.exit(0);
|
|
7971
|
+
}
|
|
7646
7972
|
const envVars = buildEnvVars3(config, options);
|
|
7647
7973
|
const scriptPath = getScriptPath("night-watch-qa-cron.sh");
|
|
7648
7974
|
if (options.dryRun) {
|
|
@@ -7725,6 +8051,9 @@ ${stderr}`);
|
|
|
7725
8051
|
});
|
|
7726
8052
|
}
|
|
7727
8053
|
init_dist();
|
|
8054
|
+
function getTelegramStatusWebhooks2(config) {
|
|
8055
|
+
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 }));
|
|
8056
|
+
}
|
|
7728
8057
|
function buildEnvVars4(config, options) {
|
|
7729
8058
|
const env = {};
|
|
7730
8059
|
const auditProvider = resolveJobProvider(config, "audit");
|
|
@@ -7736,6 +8065,12 @@ function buildEnvVars4(config, options) {
|
|
|
7736
8065
|
if (config.providerEnv) {
|
|
7737
8066
|
Object.assign(env, config.providerEnv);
|
|
7738
8067
|
}
|
|
8068
|
+
const telegramWebhooks = getTelegramStatusWebhooks2(config);
|
|
8069
|
+
if (telegramWebhooks.length > 0) {
|
|
8070
|
+
env.NW_TELEGRAM_STATUS_WEBHOOKS = JSON.stringify(telegramWebhooks);
|
|
8071
|
+
env.NW_TELEGRAM_BOT_TOKEN = telegramWebhooks[0].botToken;
|
|
8072
|
+
env.NW_TELEGRAM_CHAT_ID = telegramWebhooks[0].chatId;
|
|
8073
|
+
}
|
|
7739
8074
|
if (options.dryRun) {
|
|
7740
8075
|
env.NW_DRY_RUN = "1";
|
|
7741
8076
|
}
|
|
@@ -7758,6 +8093,10 @@ function auditCommand(program2) {
|
|
|
7758
8093
|
_cliProviderOverride: options.provider
|
|
7759
8094
|
};
|
|
7760
8095
|
}
|
|
8096
|
+
if (!config.audit.enabled && !options.dryRun) {
|
|
8097
|
+
info("Audit is disabled in config; skipping run.");
|
|
8098
|
+
process.exit(0);
|
|
8099
|
+
}
|
|
7761
8100
|
const envVars = buildEnvVars4(config, options);
|
|
7762
8101
|
const scriptPath = getScriptPath("night-watch-audit-cron.sh");
|
|
7763
8102
|
if (options.dryRun) {
|
|
@@ -7827,7 +8166,7 @@ function shellQuote(value) {
|
|
|
7827
8166
|
}
|
|
7828
8167
|
function getNightWatchBinPath() {
|
|
7829
8168
|
try {
|
|
7830
|
-
const npmBin =
|
|
8169
|
+
const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
|
|
7831
8170
|
const binPath = path18.join(npmBin, "night-watch");
|
|
7832
8171
|
if (fs17.existsSync(binPath)) {
|
|
7833
8172
|
return binPath;
|
|
@@ -7835,14 +8174,14 @@ function getNightWatchBinPath() {
|
|
|
7835
8174
|
} catch {
|
|
7836
8175
|
}
|
|
7837
8176
|
try {
|
|
7838
|
-
return
|
|
8177
|
+
return execSync4("which night-watch", { encoding: "utf-8" }).trim();
|
|
7839
8178
|
} catch {
|
|
7840
8179
|
return "night-watch";
|
|
7841
8180
|
}
|
|
7842
8181
|
}
|
|
7843
8182
|
function getNodeBinDir() {
|
|
7844
8183
|
try {
|
|
7845
|
-
const nodePath =
|
|
8184
|
+
const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
|
|
7846
8185
|
return path18.dirname(nodePath);
|
|
7847
8186
|
} catch {
|
|
7848
8187
|
return "";
|
|
@@ -7901,8 +8240,11 @@ function performInstall(projectDir, config, options) {
|
|
|
7901
8240
|
const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
|
|
7902
8241
|
providerEnvPrefix = exports + " && ";
|
|
7903
8242
|
}
|
|
7904
|
-
const
|
|
7905
|
-
|
|
8243
|
+
const installExecutor = config.executorEnabled !== false;
|
|
8244
|
+
if (installExecutor) {
|
|
8245
|
+
const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
|
|
8246
|
+
entries.push(executorEntry);
|
|
8247
|
+
}
|
|
7906
8248
|
const installReviewer = options?.noReviewer === true ? false : config.reviewerEnabled;
|
|
7907
8249
|
if (installReviewer) {
|
|
7908
8250
|
const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
|
|
@@ -7912,7 +8254,7 @@ function performInstall(projectDir, config, options) {
|
|
|
7912
8254
|
if (installSlicer) {
|
|
7913
8255
|
const slicerSchedule = applyScheduleOffset(config.roadmapScanner.slicerSchedule, offset);
|
|
7914
8256
|
const slicerLog = path18.join(logDir, "slicer.log");
|
|
7915
|
-
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)}
|
|
8257
|
+
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
7916
8258
|
entries.push(slicerEntry);
|
|
7917
8259
|
}
|
|
7918
8260
|
const disableQa = options?.noQa === true || options?.qa === false;
|
|
@@ -7979,8 +8321,11 @@ function installCommand(program2) {
|
|
|
7979
8321
|
const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
|
|
7980
8322
|
providerEnvPrefix = exports + " && ";
|
|
7981
8323
|
}
|
|
7982
|
-
const
|
|
7983
|
-
|
|
8324
|
+
const installExecutor = config.executorEnabled !== false;
|
|
8325
|
+
if (installExecutor) {
|
|
8326
|
+
const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
|
|
8327
|
+
entries.push(executorEntry);
|
|
8328
|
+
}
|
|
7984
8329
|
const installReviewer = options.noReviewer === true ? false : config.reviewerEnabled;
|
|
7985
8330
|
if (installReviewer) {
|
|
7986
8331
|
const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
|
|
@@ -7991,7 +8336,7 @@ function installCommand(program2) {
|
|
|
7991
8336
|
if (installSlicer) {
|
|
7992
8337
|
slicerLog = path18.join(logDir, "slicer.log");
|
|
7993
8338
|
const slicerSchedule = applyScheduleOffset(config.roadmapScanner.slicerSchedule, offset);
|
|
7994
|
-
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)}
|
|
8339
|
+
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
7995
8340
|
entries.push(slicerEntry);
|
|
7996
8341
|
}
|
|
7997
8342
|
const disableQa = options.noQa === true || options.qa === false;
|
|
@@ -8021,12 +8366,14 @@ function installCommand(program2) {
|
|
|
8021
8366
|
entries.forEach((entry) => dim(` ${entry}`));
|
|
8022
8367
|
console.log();
|
|
8023
8368
|
header("Log Files");
|
|
8024
|
-
|
|
8369
|
+
if (installExecutor) {
|
|
8370
|
+
dim(` Executor: ${executorLog}`);
|
|
8371
|
+
}
|
|
8025
8372
|
if (installReviewer) {
|
|
8026
8373
|
dim(` Reviewer: ${reviewerLog}`);
|
|
8027
8374
|
}
|
|
8028
8375
|
if (installSlicer && slicerLog) {
|
|
8029
|
-
dim(`
|
|
8376
|
+
dim(` Planner: ${slicerLog}`);
|
|
8030
8377
|
}
|
|
8031
8378
|
if (installQa && qaLog) {
|
|
8032
8379
|
dim(` QA: ${qaLog}`);
|
|
@@ -8145,11 +8492,17 @@ function statusCommand(program2) {
|
|
|
8145
8492
|
try {
|
|
8146
8493
|
const projectDir = process.cwd();
|
|
8147
8494
|
const config = loadConfig(projectDir);
|
|
8148
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
8495
|
+
const snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
8149
8496
|
const executorProc = snapshot.processes.find((p) => p.name === "executor");
|
|
8150
8497
|
const reviewerProc = snapshot.processes.find((p) => p.name === "reviewer");
|
|
8498
|
+
const qaProc = snapshot.processes.find((p) => p.name === "qa");
|
|
8499
|
+
const auditProc = snapshot.processes.find((p) => p.name === "audit");
|
|
8500
|
+
const plannerProc = snapshot.processes.find((p) => p.name === "planner");
|
|
8151
8501
|
const executorLog = snapshot.logs.find((l) => l.name === "executor");
|
|
8152
8502
|
const reviewerLog = snapshot.logs.find((l) => l.name === "reviewer");
|
|
8503
|
+
const qaLog = snapshot.logs.find((l) => l.name === "qa");
|
|
8504
|
+
const auditLog = snapshot.logs.find((l) => l.name === "audit");
|
|
8505
|
+
const plannerLog = snapshot.logs.find((l) => l.name === "planner");
|
|
8153
8506
|
const pendingPrds = snapshot.prds.filter((p) => p.status === "ready" || p.status === "blocked").length;
|
|
8154
8507
|
const claimedPrds = snapshot.prds.filter((p) => p.status === "in-progress").length;
|
|
8155
8508
|
const donePrds = snapshot.prds.filter((p) => p.status === "done").length;
|
|
@@ -8162,6 +8515,9 @@ function statusCommand(program2) {
|
|
|
8162
8515
|
autoMergeMethod: config.autoMergeMethod,
|
|
8163
8516
|
executor: { running: executorProc?.running ?? false, pid: executorProc?.pid ?? null },
|
|
8164
8517
|
reviewer: { running: reviewerProc?.running ?? false, pid: reviewerProc?.pid ?? null },
|
|
8518
|
+
qa: { running: qaProc?.running ?? false, pid: qaProc?.pid ?? null },
|
|
8519
|
+
audit: { running: auditProc?.running ?? false, pid: auditProc?.pid ?? null },
|
|
8520
|
+
planner: { running: plannerProc?.running ?? false, pid: plannerProc?.pid ?? null },
|
|
8165
8521
|
prds: { pending: pendingPrds, claimed: claimedPrds, done: donePrds },
|
|
8166
8522
|
prs: { open: snapshot.prs.length },
|
|
8167
8523
|
crontab: snapshot.crontab,
|
|
@@ -8177,6 +8533,24 @@ function statusCommand(program2) {
|
|
|
8177
8533
|
lastLines: reviewerLog.lastLines,
|
|
8178
8534
|
exists: reviewerLog.exists,
|
|
8179
8535
|
size: reviewerLog.size
|
|
8536
|
+
} : void 0,
|
|
8537
|
+
qa: qaLog ? {
|
|
8538
|
+
path: qaLog.path,
|
|
8539
|
+
lastLines: qaLog.lastLines,
|
|
8540
|
+
exists: qaLog.exists,
|
|
8541
|
+
size: qaLog.size
|
|
8542
|
+
} : void 0,
|
|
8543
|
+
audit: auditLog ? {
|
|
8544
|
+
path: auditLog.path,
|
|
8545
|
+
lastLines: auditLog.lastLines,
|
|
8546
|
+
exists: auditLog.exists,
|
|
8547
|
+
size: auditLog.size
|
|
8548
|
+
} : void 0,
|
|
8549
|
+
planner: plannerLog ? {
|
|
8550
|
+
path: plannerLog.path,
|
|
8551
|
+
lastLines: plannerLog.lastLines,
|
|
8552
|
+
exists: plannerLog.exists,
|
|
8553
|
+
size: plannerLog.size
|
|
8180
8554
|
} : void 0
|
|
8181
8555
|
}
|
|
8182
8556
|
};
|
|
@@ -8208,6 +8582,12 @@ function statusCommand(program2) {
|
|
|
8208
8582
|
"Reviewer",
|
|
8209
8583
|
formatRunningStatus(status.reviewer.running, status.reviewer.pid)
|
|
8210
8584
|
]);
|
|
8585
|
+
processTable.push(["QA", formatRunningStatus(status.qa.running, status.qa.pid)]);
|
|
8586
|
+
processTable.push(["Audit", formatRunningStatus(status.audit.running, status.audit.pid)]);
|
|
8587
|
+
processTable.push([
|
|
8588
|
+
"Planner",
|
|
8589
|
+
formatRunningStatus(status.planner.running, status.planner.pid)
|
|
8590
|
+
]);
|
|
8211
8591
|
console.log(processTable.toString());
|
|
8212
8592
|
header("PRD Status");
|
|
8213
8593
|
const prdTable = createTable({ head: ["Status", "Count"] });
|
|
@@ -8243,6 +8623,27 @@ function statusCommand(program2) {
|
|
|
8243
8623
|
status.logs.reviewer.exists ? "Exists" : "Not found"
|
|
8244
8624
|
]);
|
|
8245
8625
|
}
|
|
8626
|
+
if (status.logs.qa) {
|
|
8627
|
+
logTable.push([
|
|
8628
|
+
"QA",
|
|
8629
|
+
status.logs.qa.exists ? formatBytes(status.logs.qa.size) : "-",
|
|
8630
|
+
status.logs.qa.exists ? "Exists" : "Not found"
|
|
8631
|
+
]);
|
|
8632
|
+
}
|
|
8633
|
+
if (status.logs.audit) {
|
|
8634
|
+
logTable.push([
|
|
8635
|
+
"Audit",
|
|
8636
|
+
status.logs.audit.exists ? formatBytes(status.logs.audit.size) : "-",
|
|
8637
|
+
status.logs.audit.exists ? "Exists" : "Not found"
|
|
8638
|
+
]);
|
|
8639
|
+
}
|
|
8640
|
+
if (status.logs.planner) {
|
|
8641
|
+
logTable.push([
|
|
8642
|
+
"Planner",
|
|
8643
|
+
status.logs.planner.exists ? formatBytes(status.logs.planner.size) : "-",
|
|
8644
|
+
status.logs.planner.exists ? "Exists" : "Not found"
|
|
8645
|
+
]);
|
|
8646
|
+
}
|
|
8246
8647
|
console.log(logTable.toString());
|
|
8247
8648
|
if (options.verbose) {
|
|
8248
8649
|
if (status.logs.executor?.exists && status.logs.executor.lastLines.length > 0) {
|
|
@@ -8253,12 +8654,27 @@ function statusCommand(program2) {
|
|
|
8253
8654
|
dim(" Reviewer last 5 lines:");
|
|
8254
8655
|
status.logs.reviewer.lastLines.forEach((line) => dim(` ${line}`));
|
|
8255
8656
|
}
|
|
8657
|
+
if (status.logs.qa?.exists && status.logs.qa.lastLines.length > 0) {
|
|
8658
|
+
dim(" QA last 5 lines:");
|
|
8659
|
+
status.logs.qa.lastLines.forEach((line) => dim(` ${line}`));
|
|
8660
|
+
}
|
|
8661
|
+
if (status.logs.audit?.exists && status.logs.audit.lastLines.length > 0) {
|
|
8662
|
+
dim(" Audit last 5 lines:");
|
|
8663
|
+
status.logs.audit.lastLines.forEach((line) => dim(` ${line}`));
|
|
8664
|
+
}
|
|
8665
|
+
if (status.logs.planner?.exists && status.logs.planner.lastLines.length > 0) {
|
|
8666
|
+
dim(" Planner last 5 lines:");
|
|
8667
|
+
status.logs.planner.lastLines.forEach((line) => dim(` ${line}`));
|
|
8668
|
+
}
|
|
8256
8669
|
}
|
|
8257
8670
|
header("Commands");
|
|
8258
8671
|
dim(" night-watch install - Install crontab entries");
|
|
8259
8672
|
dim(" night-watch logs - View logs");
|
|
8260
8673
|
dim(" night-watch run - Run executor now");
|
|
8261
8674
|
dim(" night-watch review - Run reviewer now");
|
|
8675
|
+
dim(" night-watch qa - Run QA now");
|
|
8676
|
+
dim(" night-watch audit - Run audit now");
|
|
8677
|
+
dim(" night-watch planner - Run planner now");
|
|
8262
8678
|
console.log();
|
|
8263
8679
|
} catch (error2) {
|
|
8264
8680
|
console.error(`Error getting status: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
@@ -8297,22 +8713,36 @@ function followLog(filePath) {
|
|
|
8297
8713
|
});
|
|
8298
8714
|
}
|
|
8299
8715
|
function logsCommand(program2) {
|
|
8300
|
-
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 (
|
|
8716
|
+
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) => {
|
|
8301
8717
|
try {
|
|
8302
8718
|
const projectDir = process.cwd();
|
|
8303
8719
|
const logDir = path20.join(projectDir, LOG_DIR);
|
|
8304
8720
|
const lineCount = parseInt(options.lines || "50", 10);
|
|
8305
8721
|
const executorLog = path20.join(logDir, EXECUTOR_LOG_FILE);
|
|
8306
8722
|
const reviewerLog = path20.join(logDir, REVIEWER_LOG_FILE);
|
|
8723
|
+
const qaLog = path20.join(logDir, `${QA_LOG_NAME}.log`);
|
|
8724
|
+
const auditLog = path20.join(logDir, `${AUDIT_LOG_NAME}.log`);
|
|
8725
|
+
const plannerLog = path20.join(logDir, `${PLANNER_LOG_NAME}.log`);
|
|
8307
8726
|
const logType = options.type?.toLowerCase() || "all";
|
|
8308
8727
|
const showExecutor = logType === "all" || logType === "run" || logType === "executor";
|
|
8309
8728
|
const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
|
|
8729
|
+
const showQa = logType === "all" || logType === "qa";
|
|
8730
|
+
const showAudit = logType === "all" || logType === "audit";
|
|
8731
|
+
const showPlanner = logType === "all" || logType === "planner" || logType === "slice" || logType === "slicer";
|
|
8310
8732
|
if (options.follow) {
|
|
8311
8733
|
if (logType === "all") {
|
|
8312
8734
|
dim("Note: Following all logs is not supported. Showing executor log.");
|
|
8313
|
-
dim("Use --type
|
|
8314
|
-
}
|
|
8315
|
-
|
|
8735
|
+
dim("Use --type reviewer|qa|audit|planner for other logs.\n");
|
|
8736
|
+
}
|
|
8737
|
+
let targetLog = executorLog;
|
|
8738
|
+
if (showReviewer)
|
|
8739
|
+
targetLog = reviewerLog;
|
|
8740
|
+
else if (showQa)
|
|
8741
|
+
targetLog = qaLog;
|
|
8742
|
+
else if (showAudit)
|
|
8743
|
+
targetLog = auditLog;
|
|
8744
|
+
else if (showPlanner)
|
|
8745
|
+
targetLog = plannerLog;
|
|
8316
8746
|
followLog(targetLog);
|
|
8317
8747
|
return;
|
|
8318
8748
|
}
|
|
@@ -8329,10 +8759,28 @@ function logsCommand(program2) {
|
|
|
8329
8759
|
console.log();
|
|
8330
8760
|
console.log(getLastLines(reviewerLog, lineCount));
|
|
8331
8761
|
}
|
|
8762
|
+
if (showQa) {
|
|
8763
|
+
header("QA Log");
|
|
8764
|
+
dim(`File: ${qaLog}`);
|
|
8765
|
+
console.log();
|
|
8766
|
+
console.log(getLastLines(qaLog, lineCount));
|
|
8767
|
+
}
|
|
8768
|
+
if (showAudit) {
|
|
8769
|
+
header("Audit Log");
|
|
8770
|
+
dim(`File: ${auditLog}`);
|
|
8771
|
+
console.log();
|
|
8772
|
+
console.log(getLastLines(auditLog, lineCount));
|
|
8773
|
+
}
|
|
8774
|
+
if (showPlanner) {
|
|
8775
|
+
header("Planner Log");
|
|
8776
|
+
dim(`File: ${plannerLog}`);
|
|
8777
|
+
console.log();
|
|
8778
|
+
console.log(getLastLines(plannerLog, lineCount));
|
|
8779
|
+
}
|
|
8332
8780
|
console.log();
|
|
8333
8781
|
dim("---");
|
|
8334
8782
|
dim("Tip: Use -f to follow logs in real-time");
|
|
8335
|
-
dim(" Use --type
|
|
8783
|
+
dim(" Use --type executor|reviewer|qa|audit|planner to view specific logs");
|
|
8336
8784
|
} catch (err) {
|
|
8337
8785
|
console.error(`Error reading logs: ${err instanceof Error ? err.message : String(err)}`);
|
|
8338
8786
|
process.exit(1);
|
|
@@ -9845,10 +10293,14 @@ function createSchedulesTab() {
|
|
|
9845
10293
|
ctx.showMessage(`Saved but cron install failed: ${installResult.error}`, "error");
|
|
9846
10294
|
}
|
|
9847
10295
|
ctx.config = newConfig;
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
10296
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10297
|
+
ctx.snapshot = snap;
|
|
10298
|
+
renderCrontab(ctx);
|
|
10299
|
+
renderScheduleSettings(ctx);
|
|
10300
|
+
}).catch(() => {
|
|
10301
|
+
renderCrontab(ctx);
|
|
10302
|
+
renderScheduleSettings(ctx);
|
|
10303
|
+
});
|
|
9852
10304
|
}
|
|
9853
10305
|
function showCustomCronInput(ctx, field, label2) {
|
|
9854
10306
|
const currentValue = ctx.config[field];
|
|
@@ -9946,10 +10398,13 @@ function createSchedulesTab() {
|
|
|
9946
10398
|
} else {
|
|
9947
10399
|
ctx.showMessage(`Install failed: ${result.error}`, "error");
|
|
9948
10400
|
}
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
10401
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10402
|
+
ctx.snapshot = snap;
|
|
10403
|
+
renderCrontab(ctx);
|
|
10404
|
+
ctx.screen.render();
|
|
10405
|
+
}).catch(() => {
|
|
10406
|
+
ctx.screen.render();
|
|
10407
|
+
});
|
|
9953
10408
|
}
|
|
9954
10409
|
],
|
|
9955
10410
|
[
|
|
@@ -9961,10 +10416,13 @@ function createSchedulesTab() {
|
|
|
9961
10416
|
} else {
|
|
9962
10417
|
ctx.showMessage(`Uninstall failed: ${result.error}`, "error");
|
|
9963
10418
|
}
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
10419
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10420
|
+
ctx.snapshot = snap;
|
|
10421
|
+
renderCrontab(ctx);
|
|
10422
|
+
ctx.screen.render();
|
|
10423
|
+
}).catch(() => {
|
|
10424
|
+
ctx.screen.render();
|
|
10425
|
+
});
|
|
9968
10426
|
}
|
|
9969
10427
|
],
|
|
9970
10428
|
[
|
|
@@ -9977,11 +10435,14 @@ function createSchedulesTab() {
|
|
|
9977
10435
|
} else {
|
|
9978
10436
|
ctx.showMessage(`Reinstall failed: ${result.error}`, "error");
|
|
9979
10437
|
}
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
10438
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10439
|
+
ctx.snapshot = snap;
|
|
10440
|
+
renderCrontab(ctx);
|
|
10441
|
+
ctx.screen.render();
|
|
10442
|
+
}).catch(() => {
|
|
10443
|
+
ctx.screen.render();
|
|
10444
|
+
});
|
|
10445
|
+
}
|
|
9985
10446
|
]
|
|
9986
10447
|
];
|
|
9987
10448
|
for (const [keys, handler] of handlers) {
|
|
@@ -10108,9 +10569,12 @@ ${result.entries.map((e) => ` ${e}`).join("\n")}`);
|
|
|
10108
10569
|
outputBox.setContent(`{red-fg}Install failed: ${result.error}{/red-fg}`);
|
|
10109
10570
|
ctx.showMessage("Install failed", "error");
|
|
10110
10571
|
}
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10572
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10573
|
+
ctx.snapshot = snap;
|
|
10574
|
+
ctx.screen.render();
|
|
10575
|
+
}).catch(() => {
|
|
10576
|
+
ctx.screen.render();
|
|
10577
|
+
});
|
|
10114
10578
|
}
|
|
10115
10579
|
},
|
|
10116
10580
|
{
|
|
@@ -10126,9 +10590,12 @@ Removed ${result.removedCount} entries.`);
|
|
|
10126
10590
|
outputBox.setContent(`{red-fg}Uninstall failed: ${result.error}{/red-fg}`);
|
|
10127
10591
|
ctx.showMessage("Uninstall failed", "error");
|
|
10128
10592
|
}
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10593
|
+
ctx.refreshSnapshot().then((snap) => {
|
|
10594
|
+
ctx.snapshot = snap;
|
|
10595
|
+
ctx.screen.render();
|
|
10596
|
+
}).catch(() => {
|
|
10597
|
+
ctx.screen.render();
|
|
10598
|
+
});
|
|
10132
10599
|
}
|
|
10133
10600
|
},
|
|
10134
10601
|
{
|
|
@@ -10521,7 +10988,7 @@ function dashboardCommand(program2) {
|
|
|
10521
10988
|
}
|
|
10522
10989
|
let activeTabIndex = 0;
|
|
10523
10990
|
let isEditing = false;
|
|
10524
|
-
let snapshot = fetchStatusSnapshot(projectDir, config);
|
|
10991
|
+
let snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
10525
10992
|
const ctx = {
|
|
10526
10993
|
screen,
|
|
10527
10994
|
projectDir,
|
|
@@ -10532,8 +10999,8 @@ function dashboardCommand(program2) {
|
|
|
10532
10999
|
ctx.config = config;
|
|
10533
11000
|
return config;
|
|
10534
11001
|
},
|
|
10535
|
-
refreshSnapshot: () => {
|
|
10536
|
-
snapshot = fetchStatusSnapshot(projectDir, config);
|
|
11002
|
+
refreshSnapshot: async () => {
|
|
11003
|
+
snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
10537
11004
|
ctx.snapshot = snapshot;
|
|
10538
11005
|
return snapshot;
|
|
10539
11006
|
},
|
|
@@ -10577,10 +11044,10 @@ function dashboardCommand(program2) {
|
|
|
10577
11044
|
function updateHeader() {
|
|
10578
11045
|
headerBox.setContent(`{center}Night Watch: ${snapshot.projectName} | Provider: ${config.provider} | Last: ${snapshot.timestamp.toLocaleTimeString()} | Next: ${countdown}s{/center}`);
|
|
10579
11046
|
}
|
|
10580
|
-
function refreshData() {
|
|
11047
|
+
async function refreshData() {
|
|
10581
11048
|
config = loadConfig(projectDir);
|
|
10582
11049
|
ctx.config = config;
|
|
10583
|
-
snapshot = fetchStatusSnapshot(projectDir, config);
|
|
11050
|
+
snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
10584
11051
|
ctx.snapshot = snapshot;
|
|
10585
11052
|
countdown = intervalSeconds;
|
|
10586
11053
|
updateHeader();
|
|
@@ -10592,7 +11059,8 @@ function dashboardCommand(program2) {
|
|
|
10592
11059
|
updateHeader();
|
|
10593
11060
|
screen.render();
|
|
10594
11061
|
if (countdown <= 0) {
|
|
10595
|
-
refreshData()
|
|
11062
|
+
refreshData().catch(() => {
|
|
11063
|
+
});
|
|
10596
11064
|
}
|
|
10597
11065
|
}, 1e3);
|
|
10598
11066
|
screen.key(["q", "escape"], () => {
|
|
@@ -10608,7 +11076,8 @@ function dashboardCommand(program2) {
|
|
|
10608
11076
|
screen.key(["r"], () => {
|
|
10609
11077
|
if (isEditing)
|
|
10610
11078
|
return;
|
|
10611
|
-
refreshData()
|
|
11079
|
+
refreshData().catch(() => {
|
|
11080
|
+
});
|
|
10612
11081
|
});
|
|
10613
11082
|
for (let i = 0; i < tabs.length; i++) {
|
|
10614
11083
|
const idx = i;
|
|
@@ -10907,8 +11376,7 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
|
|
|
10907
11376
|
return setInterval(() => {
|
|
10908
11377
|
if (clients.size === 0)
|
|
10909
11378
|
return;
|
|
10910
|
-
|
|
10911
|
-
const snapshot = fetchStatusSnapshot(projectDir, getConfig());
|
|
11379
|
+
fetchStatusSnapshot(projectDir, getConfig()).then((snapshot) => {
|
|
10912
11380
|
const hash = JSON.stringify({
|
|
10913
11381
|
processes: snapshot.processes,
|
|
10914
11382
|
prds: snapshot.prds.map((p) => ({ n: p.name, s: p.status }))
|
|
@@ -10917,8 +11385,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
|
|
|
10917
11385
|
lastSnapshotHash = hash;
|
|
10918
11386
|
broadcastSSE(clients, "status_changed", snapshot);
|
|
10919
11387
|
}
|
|
10920
|
-
}
|
|
10921
|
-
}
|
|
11388
|
+
}).catch(() => {
|
|
11389
|
+
});
|
|
10922
11390
|
}, 2e3);
|
|
10923
11391
|
}
|
|
10924
11392
|
init_dist();
|
|
@@ -10989,11 +11457,17 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
|
|
|
10989
11457
|
lockPath = executorLockPath(projectDir);
|
|
10990
11458
|
} else if (command[0] === "review") {
|
|
10991
11459
|
lockPath = reviewerLockPath(projectDir);
|
|
11460
|
+
} else if (command[0] === "planner") {
|
|
11461
|
+
lockPath = plannerLockPath(projectDir);
|
|
10992
11462
|
}
|
|
10993
11463
|
if (lockPath) {
|
|
10994
11464
|
const lock = checkLockFile(lockPath);
|
|
10995
11465
|
if (lock.running) {
|
|
10996
|
-
|
|
11466
|
+
let processType = "Planner";
|
|
11467
|
+
if (command[0] === "run")
|
|
11468
|
+
processType = "Executor";
|
|
11469
|
+
else if (command[0] === "review")
|
|
11470
|
+
processType = "Reviewer";
|
|
10997
11471
|
res.status(409).json({
|
|
10998
11472
|
error: `${processType} is already running (PID ${lock.pid})`,
|
|
10999
11473
|
pid: lock.pid
|
|
@@ -11037,25 +11511,36 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
|
|
|
11037
11511
|
});
|
|
11038
11512
|
}
|
|
11039
11513
|
}
|
|
11040
|
-
function
|
|
11041
|
-
const {
|
|
11042
|
-
const
|
|
11043
|
-
router.post(
|
|
11514
|
+
function createActionRouteHandlers(ctx) {
|
|
11515
|
+
const router = Router({ mergeParams: true });
|
|
11516
|
+
const p = ctx.pathPrefix;
|
|
11517
|
+
router.post(`/${p}run`, (req, res) => {
|
|
11518
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11044
11519
|
spawnAction2(projectDir, ["run"], req, res, (pid) => {
|
|
11045
|
-
broadcastSSE(
|
|
11520
|
+
broadcastSSE(ctx.getSseClients(req), "executor_started", { pid });
|
|
11046
11521
|
});
|
|
11047
11522
|
});
|
|
11048
|
-
router.post(
|
|
11049
|
-
spawnAction2(
|
|
11523
|
+
router.post(`/${p}review`, (req, res) => {
|
|
11524
|
+
spawnAction2(ctx.getProjectDir(req), ["review"], req, res);
|
|
11525
|
+
});
|
|
11526
|
+
router.post(`/${p}qa`, (req, res) => {
|
|
11527
|
+
spawnAction2(ctx.getProjectDir(req), ["qa"], req, res);
|
|
11528
|
+
});
|
|
11529
|
+
router.post(`/${p}audit`, (req, res) => {
|
|
11530
|
+
spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
|
|
11050
11531
|
});
|
|
11051
|
-
router.post(
|
|
11052
|
-
spawnAction2(
|
|
11532
|
+
router.post(`/${p}planner`, (req, res) => {
|
|
11533
|
+
spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
|
|
11053
11534
|
});
|
|
11054
|
-
router.post(
|
|
11055
|
-
spawnAction2(
|
|
11535
|
+
router.post(`/${p}install-cron`, (req, res) => {
|
|
11536
|
+
spawnAction2(ctx.getProjectDir(req), ["install"], req, res);
|
|
11056
11537
|
});
|
|
11057
|
-
router.post(
|
|
11538
|
+
router.post(`/${p}uninstall-cron`, (req, res) => {
|
|
11539
|
+
spawnAction2(ctx.getProjectDir(req), ["uninstall"], req, res);
|
|
11540
|
+
});
|
|
11541
|
+
router.post(`/${p}cancel`, async (req, res) => {
|
|
11058
11542
|
try {
|
|
11543
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11059
11544
|
const { type = "all" } = req.body;
|
|
11060
11545
|
const validTypes = ["run", "review", "all"];
|
|
11061
11546
|
if (!validTypes.includes(type)) {
|
|
@@ -11076,9 +11561,10 @@ function createActionRoutes(deps) {
|
|
|
11076
11561
|
});
|
|
11077
11562
|
}
|
|
11078
11563
|
});
|
|
11079
|
-
router.post(
|
|
11564
|
+
router.post(`/${p}retry`, (req, res) => {
|
|
11080
11565
|
try {
|
|
11081
|
-
const
|
|
11566
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11567
|
+
const config = ctx.getConfig(req);
|
|
11082
11568
|
const { prdName } = req.body;
|
|
11083
11569
|
if (!prdName || typeof prdName !== "string") {
|
|
11084
11570
|
res.status(400).json({ error: "prdName is required" });
|
|
@@ -11108,9 +11594,10 @@ function createActionRoutes(deps) {
|
|
|
11108
11594
|
});
|
|
11109
11595
|
}
|
|
11110
11596
|
});
|
|
11111
|
-
router.post(
|
|
11597
|
+
router.post(`/${p}clear-lock`, async (req, res) => {
|
|
11112
11598
|
try {
|
|
11113
|
-
const
|
|
11599
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11600
|
+
const config = ctx.getConfig(req);
|
|
11114
11601
|
const lockPath = executorLockPath(projectDir);
|
|
11115
11602
|
const lock = checkLockFile(lockPath);
|
|
11116
11603
|
if (lock.running) {
|
|
@@ -11124,7 +11611,7 @@ function createActionRoutes(deps) {
|
|
|
11124
11611
|
if (fs24.existsSync(prdDir)) {
|
|
11125
11612
|
cleanOrphanedClaims(prdDir);
|
|
11126
11613
|
}
|
|
11127
|
-
broadcastSSE(
|
|
11614
|
+
broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
|
|
11128
11615
|
res.json({ cleared: true });
|
|
11129
11616
|
} catch (error2) {
|
|
11130
11617
|
res.status(500).json({
|
|
@@ -11134,110 +11621,28 @@ function createActionRoutes(deps) {
|
|
|
11134
11621
|
});
|
|
11135
11622
|
return router;
|
|
11136
11623
|
}
|
|
11624
|
+
function createActionRoutes(deps) {
|
|
11625
|
+
return createActionRouteHandlers({
|
|
11626
|
+
getConfig: () => deps.getConfig(),
|
|
11627
|
+
getProjectDir: () => deps.projectDir,
|
|
11628
|
+
getSseClients: () => deps.sseClients,
|
|
11629
|
+
pathPrefix: ""
|
|
11630
|
+
});
|
|
11631
|
+
}
|
|
11137
11632
|
function createProjectActionRoutes(deps) {
|
|
11138
11633
|
const { projectSseClients } = deps;
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
const clients = projectSseClients.get(projectDir);
|
|
11144
|
-
if (clients) {
|
|
11145
|
-
broadcastSSE(clients, "executor_started", { pid });
|
|
11146
|
-
}
|
|
11147
|
-
});
|
|
11148
|
-
});
|
|
11149
|
-
router.post("/actions/review", (req, res) => {
|
|
11150
|
-
spawnAction2(req.projectDir, ["review"], req, res);
|
|
11151
|
-
});
|
|
11152
|
-
router.post("/actions/install-cron", (req, res) => {
|
|
11153
|
-
spawnAction2(req.projectDir, ["install"], req, res);
|
|
11154
|
-
});
|
|
11155
|
-
router.post("/actions/uninstall-cron", (req, res) => {
|
|
11156
|
-
spawnAction2(req.projectDir, ["uninstall"], req, res);
|
|
11157
|
-
});
|
|
11158
|
-
router.post("/actions/cancel", async (req, res) => {
|
|
11159
|
-
try {
|
|
11160
|
-
const projectDir = req.projectDir;
|
|
11161
|
-
const { type = "all" } = req.body;
|
|
11162
|
-
const validTypes = ["run", "review", "all"];
|
|
11163
|
-
if (!validTypes.includes(type)) {
|
|
11164
|
-
res.status(400).json({
|
|
11165
|
-
error: `Invalid type. Must be one of: ${validTypes.join(", ")}`
|
|
11166
|
-
});
|
|
11167
|
-
return;
|
|
11168
|
-
}
|
|
11169
|
-
const results = await performCancel(projectDir, {
|
|
11170
|
-
type,
|
|
11171
|
-
force: true
|
|
11172
|
-
});
|
|
11173
|
-
const hasFailure = results.some((r) => !r.success);
|
|
11174
|
-
res.status(hasFailure ? 500 : 200).json({ results });
|
|
11175
|
-
} catch (error2) {
|
|
11176
|
-
res.status(500).json({
|
|
11177
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11178
|
-
});
|
|
11179
|
-
}
|
|
11180
|
-
});
|
|
11181
|
-
router.post("/actions/retry", (req, res) => {
|
|
11182
|
-
try {
|
|
11183
|
-
const projectDir = req.projectDir;
|
|
11184
|
-
const config = req.projectConfig;
|
|
11185
|
-
const { prdName } = req.body;
|
|
11186
|
-
if (!prdName || typeof prdName !== "string") {
|
|
11187
|
-
res.status(400).json({ error: "prdName is required" });
|
|
11188
|
-
return;
|
|
11189
|
-
}
|
|
11190
|
-
if (!validatePrdName(prdName)) {
|
|
11191
|
-
res.status(400).json({ error: "Invalid PRD name" });
|
|
11192
|
-
return;
|
|
11193
|
-
}
|
|
11194
|
-
const prdDir = path24.join(projectDir, config.prdDir);
|
|
11195
|
-
const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
|
|
11196
|
-
const pendingPath = path24.join(prdDir, normalized);
|
|
11197
|
-
const donePath = path24.join(prdDir, "done", normalized);
|
|
11198
|
-
if (fs24.existsSync(pendingPath)) {
|
|
11199
|
-
res.json({ message: `"${normalized}" is already pending` });
|
|
11200
|
-
return;
|
|
11201
|
-
}
|
|
11202
|
-
if (!fs24.existsSync(donePath)) {
|
|
11203
|
-
res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
|
|
11204
|
-
return;
|
|
11205
|
-
}
|
|
11206
|
-
fs24.renameSync(donePath, pendingPath);
|
|
11207
|
-
res.json({ message: `Moved "${normalized}" back to pending` });
|
|
11208
|
-
} catch (error2) {
|
|
11209
|
-
res.status(500).json({
|
|
11210
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11211
|
-
});
|
|
11212
|
-
}
|
|
11213
|
-
});
|
|
11214
|
-
router.post("/actions/clear-lock", (req, res) => {
|
|
11215
|
-
try {
|
|
11634
|
+
return createActionRouteHandlers({
|
|
11635
|
+
getConfig: (req) => req.projectConfig,
|
|
11636
|
+
getProjectDir: (req) => req.projectDir,
|
|
11637
|
+
getSseClients: (req) => {
|
|
11216
11638
|
const projectDir = req.projectDir;
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
const lock = checkLockFile(lockPath);
|
|
11220
|
-
if (lock.running) {
|
|
11221
|
-
res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
|
|
11222
|
-
return;
|
|
11223
|
-
}
|
|
11224
|
-
if (fs24.existsSync(lockPath)) {
|
|
11225
|
-
fs24.unlinkSync(lockPath);
|
|
11639
|
+
if (!projectSseClients.has(projectDir)) {
|
|
11640
|
+
projectSseClients.set(projectDir, /* @__PURE__ */ new Set());
|
|
11226
11641
|
}
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
}
|
|
11231
|
-
const clients = projectSseClients.get(projectDir) ?? /* @__PURE__ */ new Set();
|
|
11232
|
-
broadcastSSE(clients, "status_changed", fetchStatusSnapshot(projectDir, config));
|
|
11233
|
-
res.json({ cleared: true });
|
|
11234
|
-
} catch (error2) {
|
|
11235
|
-
res.status(500).json({
|
|
11236
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11237
|
-
});
|
|
11238
|
-
}
|
|
11642
|
+
return projectSseClients.get(projectDir);
|
|
11643
|
+
},
|
|
11644
|
+
pathPrefix: "actions/"
|
|
11239
11645
|
});
|
|
11240
|
-
return router;
|
|
11241
11646
|
}
|
|
11242
11647
|
init_dist();
|
|
11243
11648
|
function createAgentRoutes() {
|
|
@@ -11340,12 +11745,13 @@ function createAgentRoutes() {
|
|
|
11340
11745
|
return router;
|
|
11341
11746
|
}
|
|
11342
11747
|
init_dist();
|
|
11343
|
-
function
|
|
11344
|
-
const {
|
|
11345
|
-
const
|
|
11346
|
-
router.get(
|
|
11748
|
+
function createBoardRouteHandlers(ctx) {
|
|
11749
|
+
const router = Router3({ mergeParams: true });
|
|
11750
|
+
const p = ctx.pathPrefix;
|
|
11751
|
+
router.get(`/${p}status`, async (req, res) => {
|
|
11347
11752
|
try {
|
|
11348
|
-
const config = getConfig();
|
|
11753
|
+
const config = ctx.getConfig(req);
|
|
11754
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11349
11755
|
const provider = getBoardProvider(config, projectDir);
|
|
11350
11756
|
if (!provider) {
|
|
11351
11757
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11375,9 +11781,10 @@ function createBoardRoutes(deps) {
|
|
|
11375
11781
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
11376
11782
|
}
|
|
11377
11783
|
});
|
|
11378
|
-
router.get(
|
|
11784
|
+
router.get(`/${p}issues`, async (req, res) => {
|
|
11379
11785
|
try {
|
|
11380
|
-
const config = getConfig();
|
|
11786
|
+
const config = ctx.getConfig(req);
|
|
11787
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11381
11788
|
const provider = getBoardProvider(config, projectDir);
|
|
11382
11789
|
if (!provider) {
|
|
11383
11790
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11389,9 +11796,10 @@ function createBoardRoutes(deps) {
|
|
|
11389
11796
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
11390
11797
|
}
|
|
11391
11798
|
});
|
|
11392
|
-
router.post(
|
|
11799
|
+
router.post(`/${p}issues`, async (req, res) => {
|
|
11393
11800
|
try {
|
|
11394
|
-
const config = getConfig();
|
|
11801
|
+
const config = ctx.getConfig(req);
|
|
11802
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11395
11803
|
const provider = getBoardProvider(config, projectDir);
|
|
11396
11804
|
if (!provider) {
|
|
11397
11805
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11419,9 +11827,10 @@ function createBoardRoutes(deps) {
|
|
|
11419
11827
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
11420
11828
|
}
|
|
11421
11829
|
});
|
|
11422
|
-
router.patch(
|
|
11830
|
+
router.patch(`/${p}issues/:number/move`, async (req, res) => {
|
|
11423
11831
|
try {
|
|
11424
|
-
const config = getConfig();
|
|
11832
|
+
const config = ctx.getConfig(req);
|
|
11833
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11425
11834
|
const provider = getBoardProvider(config, projectDir);
|
|
11426
11835
|
if (!provider) {
|
|
11427
11836
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11448,9 +11857,10 @@ function createBoardRoutes(deps) {
|
|
|
11448
11857
|
});
|
|
11449
11858
|
}
|
|
11450
11859
|
});
|
|
11451
|
-
router.post(
|
|
11860
|
+
router.post(`/${p}issues/:number/comment`, async (req, res) => {
|
|
11452
11861
|
try {
|
|
11453
|
-
const config = getConfig();
|
|
11862
|
+
const config = ctx.getConfig(req);
|
|
11863
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11454
11864
|
const provider = getBoardProvider(config, projectDir);
|
|
11455
11865
|
if (!provider) {
|
|
11456
11866
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11475,9 +11885,10 @@ function createBoardRoutes(deps) {
|
|
|
11475
11885
|
});
|
|
11476
11886
|
}
|
|
11477
11887
|
});
|
|
11478
|
-
router.delete(
|
|
11888
|
+
router.delete(`/${p}issues/:number`, async (req, res) => {
|
|
11479
11889
|
try {
|
|
11480
|
-
const config = getConfig();
|
|
11890
|
+
const config = ctx.getConfig(req);
|
|
11891
|
+
const projectDir = ctx.getProjectDir(req);
|
|
11481
11892
|
const provider = getBoardProvider(config, projectDir);
|
|
11482
11893
|
if (!provider) {
|
|
11483
11894
|
res.status(404).json({ error: "Board not configured" });
|
|
@@ -11499,175 +11910,19 @@ function createBoardRoutes(deps) {
|
|
|
11499
11910
|
});
|
|
11500
11911
|
return router;
|
|
11501
11912
|
}
|
|
11502
|
-
function
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
const projectDir = req.projectDir;
|
|
11508
|
-
const provider = getBoardProvider(config, projectDir);
|
|
11509
|
-
if (!provider) {
|
|
11510
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11511
|
-
return;
|
|
11512
|
-
}
|
|
11513
|
-
const cached = getCachedBoardData(projectDir);
|
|
11514
|
-
if (cached) {
|
|
11515
|
-
res.json(cached);
|
|
11516
|
-
return;
|
|
11517
|
-
}
|
|
11518
|
-
const issues = await provider.getAllIssues();
|
|
11519
|
-
const columns = {
|
|
11520
|
-
Draft: [],
|
|
11521
|
-
Ready: [],
|
|
11522
|
-
"In Progress": [],
|
|
11523
|
-
Review: [],
|
|
11524
|
-
Done: []
|
|
11525
|
-
};
|
|
11526
|
-
for (const issue of issues) {
|
|
11527
|
-
const col = issue.column ?? "Draft";
|
|
11528
|
-
columns[col].push(issue);
|
|
11529
|
-
}
|
|
11530
|
-
const result = { enabled: true, columns };
|
|
11531
|
-
setCachedBoardData(projectDir, result);
|
|
11532
|
-
res.json(result);
|
|
11533
|
-
} catch (error2) {
|
|
11534
|
-
res.status(500).json({
|
|
11535
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11536
|
-
});
|
|
11537
|
-
}
|
|
11538
|
-
});
|
|
11539
|
-
router.get("/board/issues", async (_req, res) => {
|
|
11540
|
-
try {
|
|
11541
|
-
const config = _req.projectConfig;
|
|
11542
|
-
const projectDir = _req.projectDir;
|
|
11543
|
-
const provider = getBoardProvider(config, projectDir);
|
|
11544
|
-
if (!provider) {
|
|
11545
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11546
|
-
return;
|
|
11547
|
-
}
|
|
11548
|
-
const issues = await provider.getAllIssues();
|
|
11549
|
-
res.json(issues);
|
|
11550
|
-
} catch (error2) {
|
|
11551
|
-
res.status(500).json({
|
|
11552
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11553
|
-
});
|
|
11554
|
-
}
|
|
11555
|
-
});
|
|
11556
|
-
router.post("/board/issues", async (req, res) => {
|
|
11557
|
-
try {
|
|
11558
|
-
const config = req.projectConfig;
|
|
11559
|
-
const projectDir = req.projectDir;
|
|
11560
|
-
const provider = getBoardProvider(config, projectDir);
|
|
11561
|
-
if (!provider) {
|
|
11562
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11563
|
-
return;
|
|
11564
|
-
}
|
|
11565
|
-
const { title, body, column } = req.body;
|
|
11566
|
-
if (!title || typeof title !== "string" || title.trim().length === 0) {
|
|
11567
|
-
res.status(400).json({ error: "title is required" });
|
|
11568
|
-
return;
|
|
11569
|
-
}
|
|
11570
|
-
if (column && !BOARD_COLUMNS.includes(column)) {
|
|
11571
|
-
res.status(400).json({
|
|
11572
|
-
error: `Invalid column. Must be one of: ${BOARD_COLUMNS.join(", ")}`
|
|
11573
|
-
});
|
|
11574
|
-
return;
|
|
11575
|
-
}
|
|
11576
|
-
const issue = await provider.createIssue({
|
|
11577
|
-
title: title.trim(),
|
|
11578
|
-
body: body ?? "",
|
|
11579
|
-
column
|
|
11580
|
-
});
|
|
11581
|
-
invalidateBoardCache(projectDir);
|
|
11582
|
-
res.status(201).json(issue);
|
|
11583
|
-
} catch (error2) {
|
|
11584
|
-
res.status(500).json({
|
|
11585
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11586
|
-
});
|
|
11587
|
-
}
|
|
11588
|
-
});
|
|
11589
|
-
router.patch("/board/issues/:number/move", async (req, res) => {
|
|
11590
|
-
try {
|
|
11591
|
-
const config = req.projectConfig;
|
|
11592
|
-
const projectDir = req.projectDir;
|
|
11593
|
-
const provider = getBoardProvider(config, projectDir);
|
|
11594
|
-
if (!provider) {
|
|
11595
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11596
|
-
return;
|
|
11597
|
-
}
|
|
11598
|
-
const issueNumber = parseInt(req.params.number, 10);
|
|
11599
|
-
if (isNaN(issueNumber)) {
|
|
11600
|
-
res.status(400).json({ error: "Invalid issue number" });
|
|
11601
|
-
return;
|
|
11602
|
-
}
|
|
11603
|
-
const { column } = req.body;
|
|
11604
|
-
if (!column || !BOARD_COLUMNS.includes(column)) {
|
|
11605
|
-
res.status(400).json({
|
|
11606
|
-
error: `Invalid column. Must be one of: ${BOARD_COLUMNS.join(", ")}`
|
|
11607
|
-
});
|
|
11608
|
-
return;
|
|
11609
|
-
}
|
|
11610
|
-
await provider.moveIssue(issueNumber, column);
|
|
11611
|
-
invalidateBoardCache(projectDir);
|
|
11612
|
-
res.json({ moved: true });
|
|
11613
|
-
} catch (error2) {
|
|
11614
|
-
res.status(500).json({
|
|
11615
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11616
|
-
});
|
|
11617
|
-
}
|
|
11618
|
-
});
|
|
11619
|
-
router.post("/board/issues/:number/comment", async (req, res) => {
|
|
11620
|
-
try {
|
|
11621
|
-
const config = req.projectConfig;
|
|
11622
|
-
const projectDir = req.projectDir;
|
|
11623
|
-
const provider = getBoardProvider(config, projectDir);
|
|
11624
|
-
if (!provider) {
|
|
11625
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11626
|
-
return;
|
|
11627
|
-
}
|
|
11628
|
-
const issueNumber = parseInt(req.params.number, 10);
|
|
11629
|
-
if (isNaN(issueNumber)) {
|
|
11630
|
-
res.status(400).json({ error: "Invalid issue number" });
|
|
11631
|
-
return;
|
|
11632
|
-
}
|
|
11633
|
-
const { body } = req.body;
|
|
11634
|
-
if (!body || typeof body !== "string" || body.trim().length === 0) {
|
|
11635
|
-
res.status(400).json({ error: "body is required" });
|
|
11636
|
-
return;
|
|
11637
|
-
}
|
|
11638
|
-
await provider.commentOnIssue(issueNumber, body);
|
|
11639
|
-
invalidateBoardCache(projectDir);
|
|
11640
|
-
res.json({ commented: true });
|
|
11641
|
-
} catch (error2) {
|
|
11642
|
-
res.status(500).json({
|
|
11643
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11644
|
-
});
|
|
11645
|
-
}
|
|
11913
|
+
function createBoardRoutes(deps) {
|
|
11914
|
+
return createBoardRouteHandlers({
|
|
11915
|
+
getConfig: () => deps.getConfig(),
|
|
11916
|
+
getProjectDir: () => deps.projectDir,
|
|
11917
|
+
pathPrefix: ""
|
|
11646
11918
|
});
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
res.status(404).json({ error: "Board not configured" });
|
|
11654
|
-
return;
|
|
11655
|
-
}
|
|
11656
|
-
const issueNumber = parseInt(req.params.number, 10);
|
|
11657
|
-
if (isNaN(issueNumber)) {
|
|
11658
|
-
res.status(400).json({ error: "Invalid issue number" });
|
|
11659
|
-
return;
|
|
11660
|
-
}
|
|
11661
|
-
await provider.closeIssue(issueNumber);
|
|
11662
|
-
invalidateBoardCache(projectDir);
|
|
11663
|
-
res.json({ closed: true });
|
|
11664
|
-
} catch (error2) {
|
|
11665
|
-
res.status(500).json({
|
|
11666
|
-
error: error2 instanceof Error ? error2.message : String(error2)
|
|
11667
|
-
});
|
|
11668
|
-
}
|
|
11919
|
+
}
|
|
11920
|
+
function createProjectBoardRoutes() {
|
|
11921
|
+
return createBoardRouteHandlers({
|
|
11922
|
+
getConfig: (req) => req.projectConfig,
|
|
11923
|
+
getProjectDir: (req) => req.projectDir,
|
|
11924
|
+
pathPrefix: "board/"
|
|
11669
11925
|
});
|
|
11670
|
-
return router;
|
|
11671
11926
|
}
|
|
11672
11927
|
init_dist();
|
|
11673
11928
|
function validateConfigChanges(changes) {
|
|
@@ -11683,6 +11938,9 @@ function validateConfigChanges(changes) {
|
|
|
11683
11938
|
if (changes.reviewerEnabled !== void 0 && typeof changes.reviewerEnabled !== "boolean") {
|
|
11684
11939
|
return "reviewerEnabled must be a boolean";
|
|
11685
11940
|
}
|
|
11941
|
+
if (changes.executorEnabled !== void 0 && typeof changes.executorEnabled !== "boolean") {
|
|
11942
|
+
return "executorEnabled must be a boolean";
|
|
11943
|
+
}
|
|
11686
11944
|
if (changes.maxRuntime !== void 0 && (typeof changes.maxRuntime !== "number" || changes.maxRuntime < 60)) {
|
|
11687
11945
|
return "maxRuntime must be a number >= 60";
|
|
11688
11946
|
}
|
|
@@ -11755,6 +12013,82 @@ function validateConfigChanges(changes) {
|
|
|
11755
12013
|
}
|
|
11756
12014
|
}
|
|
11757
12015
|
}
|
|
12016
|
+
if (changes.prdDir !== void 0 && (typeof changes.prdDir !== "string" || changes.prdDir.trim().length === 0)) {
|
|
12017
|
+
return "prdDir must be a non-empty string";
|
|
12018
|
+
}
|
|
12019
|
+
if (changes.cronScheduleOffset !== void 0 && (typeof changes.cronScheduleOffset !== "number" || changes.cronScheduleOffset < 0 || changes.cronScheduleOffset > 59)) {
|
|
12020
|
+
return "cronScheduleOffset must be a number between 0 and 59";
|
|
12021
|
+
}
|
|
12022
|
+
if (changes.fallbackOnRateLimit !== void 0 && typeof changes.fallbackOnRateLimit !== "boolean") {
|
|
12023
|
+
return "fallbackOnRateLimit must be a boolean";
|
|
12024
|
+
}
|
|
12025
|
+
if (changes.claudeModel !== void 0 && !VALID_CLAUDE_MODELS.includes(changes.claudeModel)) {
|
|
12026
|
+
return `Invalid claudeModel. Must be one of: ${VALID_CLAUDE_MODELS.join(", ")}`;
|
|
12027
|
+
}
|
|
12028
|
+
if (changes.qa !== void 0) {
|
|
12029
|
+
if (typeof changes.qa !== "object" || changes.qa === null) {
|
|
12030
|
+
return "qa must be an object";
|
|
12031
|
+
}
|
|
12032
|
+
const qa = changes.qa;
|
|
12033
|
+
if (qa.enabled !== void 0 && typeof qa.enabled !== "boolean") {
|
|
12034
|
+
return "qa.enabled must be a boolean";
|
|
12035
|
+
}
|
|
12036
|
+
if (qa.schedule !== void 0 && (typeof qa.schedule !== "string" || qa.schedule.trim().length === 0)) {
|
|
12037
|
+
return "qa.schedule must be a non-empty string";
|
|
12038
|
+
}
|
|
12039
|
+
if (qa.maxRuntime !== void 0 && (typeof qa.maxRuntime !== "number" || qa.maxRuntime < 60)) {
|
|
12040
|
+
return "qa.maxRuntime must be a number >= 60";
|
|
12041
|
+
}
|
|
12042
|
+
if (qa.branchPatterns !== void 0) {
|
|
12043
|
+
if (!Array.isArray(qa.branchPatterns) || !qa.branchPatterns.every((p) => typeof p === "string")) {
|
|
12044
|
+
return "qa.branchPatterns must be an array of strings";
|
|
12045
|
+
}
|
|
12046
|
+
}
|
|
12047
|
+
if (qa.artifacts !== void 0) {
|
|
12048
|
+
const validArtifacts = ["screenshot", "video", "both"];
|
|
12049
|
+
if (!validArtifacts.includes(qa.artifacts)) {
|
|
12050
|
+
return `Invalid qa.artifacts. Must be one of: ${validArtifacts.join(", ")}`;
|
|
12051
|
+
}
|
|
12052
|
+
}
|
|
12053
|
+
if (qa.skipLabel !== void 0 && typeof qa.skipLabel !== "string") {
|
|
12054
|
+
return "qa.skipLabel must be a string";
|
|
12055
|
+
}
|
|
12056
|
+
if (qa.autoInstallPlaywright !== void 0 && typeof qa.autoInstallPlaywright !== "boolean") {
|
|
12057
|
+
return "qa.autoInstallPlaywright must be a boolean";
|
|
12058
|
+
}
|
|
12059
|
+
}
|
|
12060
|
+
if (changes.audit !== void 0) {
|
|
12061
|
+
if (typeof changes.audit !== "object" || changes.audit === null) {
|
|
12062
|
+
return "audit must be an object";
|
|
12063
|
+
}
|
|
12064
|
+
const audit = changes.audit;
|
|
12065
|
+
if (audit.enabled !== void 0 && typeof audit.enabled !== "boolean") {
|
|
12066
|
+
return "audit.enabled must be a boolean";
|
|
12067
|
+
}
|
|
12068
|
+
if (audit.schedule !== void 0 && (typeof audit.schedule !== "string" || audit.schedule.trim().length === 0)) {
|
|
12069
|
+
return "audit.schedule must be a non-empty string";
|
|
12070
|
+
}
|
|
12071
|
+
if (audit.maxRuntime !== void 0 && (typeof audit.maxRuntime !== "number" || audit.maxRuntime < 60)) {
|
|
12072
|
+
return "audit.maxRuntime must be a number >= 60";
|
|
12073
|
+
}
|
|
12074
|
+
}
|
|
12075
|
+
if (changes.roadmapScanner !== void 0) {
|
|
12076
|
+
const rs = changes.roadmapScanner;
|
|
12077
|
+
if (rs.slicerSchedule !== void 0 && (typeof rs.slicerSchedule !== "string" || rs.slicerSchedule.trim().length === 0)) {
|
|
12078
|
+
return "roadmapScanner.slicerSchedule must be a non-empty string";
|
|
12079
|
+
}
|
|
12080
|
+
if (rs.slicerMaxRuntime !== void 0 && (typeof rs.slicerMaxRuntime !== "number" || rs.slicerMaxRuntime < 60)) {
|
|
12081
|
+
return "roadmapScanner.slicerMaxRuntime must be a number >= 60";
|
|
12082
|
+
}
|
|
12083
|
+
}
|
|
12084
|
+
if (changes.boardProvider !== void 0) {
|
|
12085
|
+
if (typeof changes.boardProvider !== "object" || changes.boardProvider === null) {
|
|
12086
|
+
return "boardProvider must be an object";
|
|
12087
|
+
}
|
|
12088
|
+
if (changes.boardProvider.enabled !== void 0 && typeof changes.boardProvider.enabled !== "boolean") {
|
|
12089
|
+
return "boardProvider.enabled must be a boolean";
|
|
12090
|
+
}
|
|
12091
|
+
}
|
|
11758
12092
|
return null;
|
|
11759
12093
|
}
|
|
11760
12094
|
function createConfigRoutes(deps) {
|
|
@@ -11822,7 +12156,7 @@ init_dist();
|
|
|
11822
12156
|
function runDoctorChecks(projectDir, config) {
|
|
11823
12157
|
const checks = [];
|
|
11824
12158
|
try {
|
|
11825
|
-
|
|
12159
|
+
execSync5("git rev-parse --is-inside-work-tree", {
|
|
11826
12160
|
cwd: projectDir,
|
|
11827
12161
|
stdio: "pipe"
|
|
11828
12162
|
});
|
|
@@ -11831,7 +12165,7 @@ function runDoctorChecks(projectDir, config) {
|
|
|
11831
12165
|
checks.push({ name: "git", status: "fail", detail: "Not a git repository" });
|
|
11832
12166
|
}
|
|
11833
12167
|
try {
|
|
11834
|
-
|
|
12168
|
+
execSync5(`which ${config.provider}`, { stdio: "pipe" });
|
|
11835
12169
|
checks.push({
|
|
11836
12170
|
name: "provider",
|
|
11837
12171
|
status: "pass",
|
|
@@ -11927,7 +12261,7 @@ function createLogRoutes(deps) {
|
|
|
11927
12261
|
router.get("/:name", (req, res) => {
|
|
11928
12262
|
try {
|
|
11929
12263
|
const { name } = req.params;
|
|
11930
|
-
const validNames = ["executor", "reviewer", "qa"];
|
|
12264
|
+
const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
|
|
11931
12265
|
if (!validNames.includes(name)) {
|
|
11932
12266
|
res.status(400).json({
|
|
11933
12267
|
error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
|
|
@@ -11953,7 +12287,7 @@ function createProjectLogRoutes() {
|
|
|
11953
12287
|
try {
|
|
11954
12288
|
const projectDir = req.projectDir;
|
|
11955
12289
|
const { name } = req.params;
|
|
11956
|
-
const validNames = ["executor", "reviewer", "qa"];
|
|
12290
|
+
const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
|
|
11957
12291
|
if (!validNames.includes(name)) {
|
|
11958
12292
|
res.status(400).json({
|
|
11959
12293
|
error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
|
|
@@ -11994,12 +12328,13 @@ function createProjectPrdRoutes() {
|
|
|
11994
12328
|
return router;
|
|
11995
12329
|
}
|
|
11996
12330
|
init_dist();
|
|
11997
|
-
function
|
|
11998
|
-
const {
|
|
11999
|
-
const
|
|
12000
|
-
router.get(
|
|
12331
|
+
function createRoadmapRouteHandlers(ctx) {
|
|
12332
|
+
const router = Router8({ mergeParams: true });
|
|
12333
|
+
const p = ctx.pathPrefix;
|
|
12334
|
+
router.get(`/${p}`, (req, res) => {
|
|
12001
12335
|
try {
|
|
12002
|
-
const config = getConfig();
|
|
12336
|
+
const config = ctx.getConfig(req);
|
|
12337
|
+
const projectDir = ctx.getProjectDir(req);
|
|
12003
12338
|
const status = getRoadmapStatus(projectDir, config);
|
|
12004
12339
|
const prdDir = path27.join(projectDir, config.prdDir);
|
|
12005
12340
|
const state = loadRoadmapState(prdDir);
|
|
@@ -12012,9 +12347,10 @@ function createRoadmapRoutes(deps) {
|
|
|
12012
12347
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12013
12348
|
}
|
|
12014
12349
|
});
|
|
12015
|
-
router.post(
|
|
12350
|
+
router.post(`/${p}scan`, async (req, res) => {
|
|
12016
12351
|
try {
|
|
12017
|
-
const config = getConfig();
|
|
12352
|
+
const config = ctx.getConfig(req);
|
|
12353
|
+
const projectDir = ctx.getProjectDir(req);
|
|
12018
12354
|
if (!config.roadmapScanner.enabled) {
|
|
12019
12355
|
res.status(409).json({ error: "Roadmap scanner is disabled" });
|
|
12020
12356
|
return;
|
|
@@ -12027,14 +12363,15 @@ function createRoadmapRoutes(deps) {
|
|
|
12027
12363
|
});
|
|
12028
12364
|
}
|
|
12029
12365
|
});
|
|
12030
|
-
router.put(
|
|
12366
|
+
router.put(`/${p}toggle`, (req, res) => {
|
|
12031
12367
|
try {
|
|
12032
12368
|
const { enabled } = req.body;
|
|
12033
12369
|
if (typeof enabled !== "boolean") {
|
|
12034
12370
|
res.status(400).json({ error: "enabled must be a boolean" });
|
|
12035
12371
|
return;
|
|
12036
12372
|
}
|
|
12037
|
-
const
|
|
12373
|
+
const projectDir = ctx.getProjectDir(req);
|
|
12374
|
+
const currentConfig = ctx.getConfig(req);
|
|
12038
12375
|
const result = saveConfig(projectDir, {
|
|
12039
12376
|
roadmapScanner: {
|
|
12040
12377
|
...currentConfig.roadmapScanner,
|
|
@@ -12045,71 +12382,30 @@ function createRoadmapRoutes(deps) {
|
|
|
12045
12382
|
res.status(500).json({ error: result.error });
|
|
12046
12383
|
return;
|
|
12047
12384
|
}
|
|
12048
|
-
|
|
12049
|
-
res.json(
|
|
12385
|
+
ctx.afterToggle(req);
|
|
12386
|
+
res.json(loadConfig(projectDir));
|
|
12050
12387
|
} catch (error2) {
|
|
12051
12388
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12052
12389
|
}
|
|
12053
12390
|
});
|
|
12054
12391
|
return router;
|
|
12055
12392
|
}
|
|
12056
|
-
function
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
const status = getRoadmapStatus(projectDir, config);
|
|
12063
|
-
const prdDir = path27.join(projectDir, config.prdDir);
|
|
12064
|
-
const state = loadRoadmapState(prdDir);
|
|
12065
|
-
res.json({
|
|
12066
|
-
...status,
|
|
12067
|
-
lastScan: state.lastScan || null,
|
|
12068
|
-
autoScanInterval: config.roadmapScanner.autoScanInterval
|
|
12069
|
-
});
|
|
12070
|
-
} catch (error2) {
|
|
12071
|
-
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12072
|
-
}
|
|
12073
|
-
});
|
|
12074
|
-
router.post("/roadmap/scan", async (req, res) => {
|
|
12075
|
-
try {
|
|
12076
|
-
const config = req.projectConfig;
|
|
12077
|
-
const projectDir = req.projectDir;
|
|
12078
|
-
if (!config.roadmapScanner.enabled) {
|
|
12079
|
-
res.status(409).json({ error: "Roadmap scanner is disabled" });
|
|
12080
|
-
return;
|
|
12081
|
-
}
|
|
12082
|
-
const result = await scanRoadmap(projectDir, config);
|
|
12083
|
-
res.json(result);
|
|
12084
|
-
} catch (error2) {
|
|
12085
|
-
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12086
|
-
}
|
|
12393
|
+
function createRoadmapRoutes(deps) {
|
|
12394
|
+
return createRoadmapRouteHandlers({
|
|
12395
|
+
getConfig: () => deps.getConfig(),
|
|
12396
|
+
getProjectDir: () => deps.projectDir,
|
|
12397
|
+
afterToggle: () => deps.reloadConfig(),
|
|
12398
|
+
pathPrefix: ""
|
|
12087
12399
|
});
|
|
12088
|
-
|
|
12089
|
-
|
|
12090
|
-
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
const currentConfig = req.projectConfig;
|
|
12097
|
-
const result = saveConfig(projectDir, {
|
|
12098
|
-
roadmapScanner: {
|
|
12099
|
-
...currentConfig.roadmapScanner,
|
|
12100
|
-
enabled
|
|
12101
|
-
}
|
|
12102
|
-
});
|
|
12103
|
-
if (!result.success) {
|
|
12104
|
-
res.status(500).json({ error: result.error });
|
|
12105
|
-
return;
|
|
12106
|
-
}
|
|
12107
|
-
res.json(loadConfig(projectDir));
|
|
12108
|
-
} catch (error2) {
|
|
12109
|
-
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12110
|
-
}
|
|
12400
|
+
}
|
|
12401
|
+
function createProjectRoadmapRoutes() {
|
|
12402
|
+
return createRoadmapRouteHandlers({
|
|
12403
|
+
getConfig: (req) => req.projectConfig,
|
|
12404
|
+
getProjectDir: (req) => req.projectDir,
|
|
12405
|
+
afterToggle: () => {
|
|
12406
|
+
},
|
|
12407
|
+
pathPrefix: "roadmap/"
|
|
12111
12408
|
});
|
|
12112
|
-
return router;
|
|
12113
12409
|
}
|
|
12114
12410
|
init_dist();
|
|
12115
12411
|
function createStatusRoutes(deps) {
|
|
@@ -12121,21 +12417,20 @@ function createStatusRoutes(deps) {
|
|
|
12121
12417
|
res.setHeader("Connection", "keep-alive");
|
|
12122
12418
|
res.flushHeaders();
|
|
12123
12419
|
sseClients.add(res);
|
|
12124
|
-
|
|
12125
|
-
const snapshot = fetchStatusSnapshot(projectDir, getConfig());
|
|
12420
|
+
fetchStatusSnapshot(projectDir, getConfig()).then((snapshot) => {
|
|
12126
12421
|
res.write(`event: status_changed
|
|
12127
12422
|
data: ${JSON.stringify(snapshot)}
|
|
12128
12423
|
|
|
12129
12424
|
`);
|
|
12130
|
-
}
|
|
12131
|
-
}
|
|
12425
|
+
}).catch(() => {
|
|
12426
|
+
});
|
|
12132
12427
|
req.on("close", () => {
|
|
12133
12428
|
sseClients.delete(res);
|
|
12134
12429
|
});
|
|
12135
12430
|
});
|
|
12136
|
-
router.get("/", (_req, res) => {
|
|
12431
|
+
router.get("/", async (_req, res) => {
|
|
12137
12432
|
try {
|
|
12138
|
-
const snapshot = fetchStatusSnapshot(projectDir, getConfig());
|
|
12433
|
+
const snapshot = await fetchStatusSnapshot(projectDir, getConfig());
|
|
12139
12434
|
res.json(snapshot);
|
|
12140
12435
|
} catch (error2) {
|
|
12141
12436
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
@@ -12151,30 +12446,49 @@ function computeNextRun(cronExpr) {
|
|
|
12151
12446
|
return null;
|
|
12152
12447
|
}
|
|
12153
12448
|
}
|
|
12449
|
+
function hasScheduledCommand(entries, command) {
|
|
12450
|
+
const commandPattern = new RegExp(`\\s${command}\\s+>>`);
|
|
12451
|
+
return entries.some((entry) => commandPattern.test(entry));
|
|
12452
|
+
}
|
|
12154
12453
|
function createScheduleInfoRoutes(deps) {
|
|
12155
12454
|
const { projectDir, getConfig } = deps;
|
|
12156
12455
|
const router = Router9();
|
|
12157
|
-
router.get("/", (_req, res) => {
|
|
12456
|
+
router.get("/", async (_req, res) => {
|
|
12158
12457
|
try {
|
|
12159
12458
|
const config = getConfig();
|
|
12160
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
12459
|
+
const snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
12161
12460
|
const installed = snapshot.crontab.installed;
|
|
12162
12461
|
const entries = snapshot.crontab.entries;
|
|
12462
|
+
const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
|
|
12463
|
+
const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
|
|
12464
|
+
const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
|
|
12465
|
+
const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
|
|
12466
|
+
const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
|
|
12163
12467
|
res.json({
|
|
12164
12468
|
executor: {
|
|
12165
12469
|
schedule: config.cronSchedule,
|
|
12166
|
-
installed,
|
|
12167
|
-
nextRun:
|
|
12470
|
+
installed: executorInstalled,
|
|
12471
|
+
nextRun: executorInstalled ? computeNextRun(config.cronSchedule) : null
|
|
12168
12472
|
},
|
|
12169
12473
|
reviewer: {
|
|
12170
12474
|
schedule: config.reviewerSchedule,
|
|
12171
|
-
installed:
|
|
12172
|
-
nextRun:
|
|
12475
|
+
installed: reviewerInstalled,
|
|
12476
|
+
nextRun: reviewerInstalled ? computeNextRun(config.reviewerSchedule) : null
|
|
12173
12477
|
},
|
|
12174
12478
|
qa: {
|
|
12175
12479
|
schedule: config.qa.schedule,
|
|
12176
|
-
installed:
|
|
12177
|
-
nextRun:
|
|
12480
|
+
installed: qaInstalled,
|
|
12481
|
+
nextRun: qaInstalled ? computeNextRun(config.qa.schedule) : null
|
|
12482
|
+
},
|
|
12483
|
+
audit: {
|
|
12484
|
+
schedule: config.audit.schedule,
|
|
12485
|
+
installed: auditInstalled,
|
|
12486
|
+
nextRun: auditInstalled ? computeNextRun(config.audit.schedule) : null
|
|
12487
|
+
},
|
|
12488
|
+
planner: {
|
|
12489
|
+
schedule: config.roadmapScanner.slicerSchedule,
|
|
12490
|
+
installed: plannerInstalled,
|
|
12491
|
+
nextRun: plannerInstalled ? computeNextRun(config.roadmapScanner.slicerSchedule) : null
|
|
12178
12492
|
},
|
|
12179
12493
|
paused: !installed,
|
|
12180
12494
|
entries
|
|
@@ -12190,13 +12504,12 @@ function createProjectSseRoutes(deps) {
|
|
|
12190
12504
|
const router = Router9({ mergeParams: true });
|
|
12191
12505
|
router.get("/status/events", (req, res) => {
|
|
12192
12506
|
const projectDir = req.projectDir;
|
|
12193
|
-
const config = req.projectConfig;
|
|
12194
12507
|
if (!projectSseClients.has(projectDir)) {
|
|
12195
12508
|
projectSseClients.set(projectDir, /* @__PURE__ */ new Set());
|
|
12196
12509
|
}
|
|
12197
12510
|
const clients = projectSseClients.get(projectDir);
|
|
12198
12511
|
if (!projectSseWatchers.has(projectDir)) {
|
|
12199
|
-
const watcher = startSseStatusWatcher(clients, projectDir, () =>
|
|
12512
|
+
const watcher = startSseStatusWatcher(clients, projectDir, () => loadConfig(projectDir));
|
|
12200
12513
|
projectSseWatchers.set(projectDir, watcher);
|
|
12201
12514
|
}
|
|
12202
12515
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -12204,48 +12517,69 @@ function createProjectSseRoutes(deps) {
|
|
|
12204
12517
|
res.setHeader("Connection", "keep-alive");
|
|
12205
12518
|
res.flushHeaders();
|
|
12206
12519
|
clients.add(res);
|
|
12207
|
-
|
|
12208
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
12520
|
+
fetchStatusSnapshot(projectDir, loadConfig(projectDir)).then((snapshot) => {
|
|
12209
12521
|
res.write(`event: status_changed
|
|
12210
12522
|
data: ${JSON.stringify(snapshot)}
|
|
12211
12523
|
|
|
12212
12524
|
`);
|
|
12213
|
-
}
|
|
12214
|
-
}
|
|
12525
|
+
}).catch(() => {
|
|
12526
|
+
});
|
|
12215
12527
|
req.on("close", () => {
|
|
12216
12528
|
clients.delete(res);
|
|
12529
|
+
if (clients.size === 0) {
|
|
12530
|
+
const watcher = projectSseWatchers.get(projectDir);
|
|
12531
|
+
if (watcher !== void 0) {
|
|
12532
|
+
clearInterval(watcher);
|
|
12533
|
+
projectSseWatchers.delete(projectDir);
|
|
12534
|
+
}
|
|
12535
|
+
}
|
|
12217
12536
|
});
|
|
12218
12537
|
});
|
|
12219
|
-
router.get("/status", (req, res) => {
|
|
12538
|
+
router.get("/status", async (req, res) => {
|
|
12220
12539
|
try {
|
|
12221
|
-
const snapshot = fetchStatusSnapshot(req.projectDir, req.projectConfig);
|
|
12540
|
+
const snapshot = await fetchStatusSnapshot(req.projectDir, req.projectConfig);
|
|
12222
12541
|
res.json(snapshot);
|
|
12223
12542
|
} catch (error2) {
|
|
12224
12543
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12225
12544
|
}
|
|
12226
12545
|
});
|
|
12227
|
-
router.get("/schedule-info", (req, res) => {
|
|
12546
|
+
router.get("/schedule-info", async (req, res) => {
|
|
12228
12547
|
try {
|
|
12229
12548
|
const config = req.projectConfig;
|
|
12230
12549
|
const projectDir = req.projectDir;
|
|
12231
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
12550
|
+
const snapshot = await fetchStatusSnapshot(projectDir, config);
|
|
12232
12551
|
const installed = snapshot.crontab.installed;
|
|
12233
12552
|
const entries = snapshot.crontab.entries;
|
|
12553
|
+
const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
|
|
12554
|
+
const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
|
|
12555
|
+
const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
|
|
12556
|
+
const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
|
|
12557
|
+
const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
|
|
12234
12558
|
res.json({
|
|
12235
12559
|
executor: {
|
|
12236
12560
|
schedule: config.cronSchedule,
|
|
12237
|
-
installed,
|
|
12238
|
-
nextRun:
|
|
12561
|
+
installed: executorInstalled,
|
|
12562
|
+
nextRun: executorInstalled ? computeNextRun(config.cronSchedule) : null
|
|
12239
12563
|
},
|
|
12240
12564
|
reviewer: {
|
|
12241
12565
|
schedule: config.reviewerSchedule,
|
|
12242
|
-
installed:
|
|
12243
|
-
nextRun:
|
|
12566
|
+
installed: reviewerInstalled,
|
|
12567
|
+
nextRun: reviewerInstalled ? computeNextRun(config.reviewerSchedule) : null
|
|
12244
12568
|
},
|
|
12245
12569
|
qa: {
|
|
12246
12570
|
schedule: config.qa.schedule,
|
|
12247
|
-
installed:
|
|
12248
|
-
nextRun:
|
|
12571
|
+
installed: qaInstalled,
|
|
12572
|
+
nextRun: qaInstalled ? computeNextRun(config.qa.schedule) : null
|
|
12573
|
+
},
|
|
12574
|
+
audit: {
|
|
12575
|
+
schedule: config.audit.schedule,
|
|
12576
|
+
installed: auditInstalled,
|
|
12577
|
+
nextRun: auditInstalled ? computeNextRun(config.audit.schedule) : null
|
|
12578
|
+
},
|
|
12579
|
+
planner: {
|
|
12580
|
+
schedule: config.roadmapScanner.slicerSchedule,
|
|
12581
|
+
installed: plannerInstalled,
|
|
12582
|
+
nextRun: plannerInstalled ? computeNextRun(config.roadmapScanner.slicerSchedule) : null
|
|
12249
12583
|
},
|
|
12250
12584
|
paused: !installed,
|
|
12251
12585
|
entries
|
|
@@ -12320,9 +12654,9 @@ function createApp(projectDir) {
|
|
|
12320
12654
|
app.use("/api/roadmap", createRoadmapRoutes({ projectDir, getConfig: () => config, reloadConfig }));
|
|
12321
12655
|
app.use("/api/logs", createLogRoutes({ projectDir }));
|
|
12322
12656
|
app.use("/api/doctor", createDoctorRoutes({ projectDir, getConfig: () => config }));
|
|
12323
|
-
app.get("/api/prs", (_req, res) => {
|
|
12657
|
+
app.get("/api/prs", async (_req, res) => {
|
|
12324
12658
|
try {
|
|
12325
|
-
res.json(collectPrInfo(projectDir, config.branchPatterns));
|
|
12659
|
+
res.json(await collectPrInfo(projectDir, config.branchPatterns));
|
|
12326
12660
|
} catch (error2) {
|
|
12327
12661
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12328
12662
|
}
|
|
@@ -12366,9 +12700,9 @@ function createProjectRouter() {
|
|
|
12366
12700
|
router.use("/agents", createAgentRoutes());
|
|
12367
12701
|
router.use(createProjectActionRoutes({ projectSseClients }));
|
|
12368
12702
|
router.use(createProjectRoadmapRoutes());
|
|
12369
|
-
router.get("/prs", (req, res) => {
|
|
12703
|
+
router.get("/prs", async (req, res) => {
|
|
12370
12704
|
try {
|
|
12371
|
-
res.json(collectPrInfo(req.projectDir, req.projectConfig.branchPatterns));
|
|
12705
|
+
res.json(await collectPrInfo(req.projectDir, req.projectConfig.branchPatterns));
|
|
12372
12706
|
} catch (error2) {
|
|
12373
12707
|
res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
|
|
12374
12708
|
}
|
|
@@ -12758,7 +13092,7 @@ function prsCommand(program2) {
|
|
|
12758
13092
|
}
|
|
12759
13093
|
const projectDir = process.cwd();
|
|
12760
13094
|
const config = loadConfig(projectDir);
|
|
12761
|
-
const prs = collectPrInfo(projectDir, config.branchPatterns);
|
|
13095
|
+
const prs = await collectPrInfo(projectDir, config.branchPatterns);
|
|
12762
13096
|
if (options.json) {
|
|
12763
13097
|
const output = {
|
|
12764
13098
|
prs,
|
|
@@ -12805,7 +13139,7 @@ function prsCommand(program2) {
|
|
|
12805
13139
|
init_dist();
|
|
12806
13140
|
function getOpenPrBranches(projectDir) {
|
|
12807
13141
|
try {
|
|
12808
|
-
|
|
13142
|
+
execSync6("git rev-parse --git-dir", {
|
|
12809
13143
|
cwd: projectDir,
|
|
12810
13144
|
encoding: "utf-8",
|
|
12811
13145
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -12814,12 +13148,12 @@ function getOpenPrBranches(projectDir) {
|
|
|
12814
13148
|
return /* @__PURE__ */ new Set();
|
|
12815
13149
|
}
|
|
12816
13150
|
try {
|
|
12817
|
-
|
|
13151
|
+
execSync6("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
12818
13152
|
} catch {
|
|
12819
13153
|
return /* @__PURE__ */ new Set();
|
|
12820
13154
|
}
|
|
12821
13155
|
try {
|
|
12822
|
-
const output =
|
|
13156
|
+
const output = execSync6("gh pr list --state open --json headRefName", {
|
|
12823
13157
|
cwd: projectDir,
|
|
12824
13158
|
encoding: "utf-8",
|
|
12825
13159
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -13120,6 +13454,36 @@ function cancelCommand(program2) {
|
|
|
13120
13454
|
});
|
|
13121
13455
|
}
|
|
13122
13456
|
init_dist();
|
|
13457
|
+
function getTelegramStatusWebhooks3(config) {
|
|
13458
|
+
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 }));
|
|
13459
|
+
}
|
|
13460
|
+
function plannerLockPath2(projectDir) {
|
|
13461
|
+
return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
|
|
13462
|
+
}
|
|
13463
|
+
function acquirePlannerLock(projectDir) {
|
|
13464
|
+
const lockFile = plannerLockPath2(projectDir);
|
|
13465
|
+
if (fs31.existsSync(lockFile)) {
|
|
13466
|
+
const pidRaw = fs31.readFileSync(lockFile, "utf-8").trim();
|
|
13467
|
+
const pid = parseInt(pidRaw, 10);
|
|
13468
|
+
if (!Number.isNaN(pid) && isProcessRunning(pid)) {
|
|
13469
|
+
return { acquired: false, lockFile, pid };
|
|
13470
|
+
}
|
|
13471
|
+
try {
|
|
13472
|
+
fs31.unlinkSync(lockFile);
|
|
13473
|
+
} catch {
|
|
13474
|
+
}
|
|
13475
|
+
}
|
|
13476
|
+
fs31.writeFileSync(lockFile, String(process.pid));
|
|
13477
|
+
return { acquired: true, lockFile };
|
|
13478
|
+
}
|
|
13479
|
+
function releasePlannerLock(lockFile) {
|
|
13480
|
+
try {
|
|
13481
|
+
if (fs31.existsSync(lockFile)) {
|
|
13482
|
+
fs31.unlinkSync(lockFile);
|
|
13483
|
+
}
|
|
13484
|
+
} catch {
|
|
13485
|
+
}
|
|
13486
|
+
}
|
|
13123
13487
|
function buildEnvVars5(config, options) {
|
|
13124
13488
|
const env = {};
|
|
13125
13489
|
const slicerProvider = resolveJobProvider(config, "slicer");
|
|
@@ -13130,6 +13494,12 @@ function buildEnvVars5(config, options) {
|
|
|
13130
13494
|
if (config.providerEnv) {
|
|
13131
13495
|
Object.assign(env, config.providerEnv);
|
|
13132
13496
|
}
|
|
13497
|
+
const telegramWebhooks = getTelegramStatusWebhooks3(config);
|
|
13498
|
+
if (telegramWebhooks.length > 0) {
|
|
13499
|
+
env.NW_TELEGRAM_STATUS_WEBHOOKS = JSON.stringify(telegramWebhooks);
|
|
13500
|
+
env.NW_TELEGRAM_BOT_TOKEN = telegramWebhooks[0].botToken;
|
|
13501
|
+
env.NW_TELEGRAM_CHAT_ID = telegramWebhooks[0].chatId;
|
|
13502
|
+
}
|
|
13133
13503
|
if (options.dryRun) {
|
|
13134
13504
|
env.NW_DRY_RUN = "1";
|
|
13135
13505
|
}
|
|
@@ -13153,13 +13523,20 @@ function applyCliOverrides4(config, options) {
|
|
|
13153
13523
|
return overridden;
|
|
13154
13524
|
}
|
|
13155
13525
|
function sliceCommand(program2) {
|
|
13156
|
-
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) => {
|
|
13526
|
+
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) => {
|
|
13157
13527
|
const projectDir = process.cwd();
|
|
13528
|
+
const lockResult = acquirePlannerLock(projectDir);
|
|
13529
|
+
if (!lockResult.acquired) {
|
|
13530
|
+
info(`Planner is already running${lockResult.pid ? ` (PID ${lockResult.pid})` : ""}`);
|
|
13531
|
+
process.exit(0);
|
|
13532
|
+
}
|
|
13533
|
+
const cleanupLock = () => releasePlannerLock(lockResult.lockFile);
|
|
13534
|
+
process.on("exit", cleanupLock);
|
|
13158
13535
|
let config = loadConfig(projectDir);
|
|
13159
13536
|
config = applyCliOverrides4(config, options);
|
|
13160
13537
|
const envVars = buildEnvVars5(config, options);
|
|
13161
13538
|
if (options.dryRun) {
|
|
13162
|
-
header("Dry Run:
|
|
13539
|
+
header("Dry Run: Planner");
|
|
13163
13540
|
const slicerProvider = resolveJobProvider(config, "slicer");
|
|
13164
13541
|
header("Configuration");
|
|
13165
13542
|
const configTable = createTable({ head: ["Setting", "Value"] });
|
|
@@ -13168,10 +13545,10 @@ function sliceCommand(program2) {
|
|
|
13168
13545
|
configTable.push(["PRD Directory", config.prdDir]);
|
|
13169
13546
|
configTable.push(["Roadmap Path", config.roadmapScanner.roadmapPath]);
|
|
13170
13547
|
configTable.push([
|
|
13171
|
-
"
|
|
13548
|
+
"Planner Max Runtime",
|
|
13172
13549
|
`${config.roadmapScanner.slicerMaxRuntime}s (${Math.floor(config.roadmapScanner.slicerMaxRuntime / 60)}min)`
|
|
13173
13550
|
]);
|
|
13174
|
-
configTable.push(["
|
|
13551
|
+
configTable.push(["Planner Schedule", config.roadmapScanner.slicerSchedule]);
|
|
13175
13552
|
configTable.push(["Scanner Enabled", config.roadmapScanner.enabled ? "Yes" : "No"]);
|
|
13176
13553
|
console.log(configTable.toString());
|
|
13177
13554
|
header("Roadmap Status");
|
|
@@ -13215,38 +13592,51 @@ function sliceCommand(program2) {
|
|
|
13215
13592
|
process.exit(0);
|
|
13216
13593
|
}
|
|
13217
13594
|
if (!config.roadmapScanner.enabled) {
|
|
13218
|
-
|
|
13219
|
-
process.exit(
|
|
13595
|
+
info("Planner is disabled in config; skipping run.");
|
|
13596
|
+
process.exit(0);
|
|
13220
13597
|
}
|
|
13221
|
-
const spinner = createSpinner("Running
|
|
13598
|
+
const spinner = createSpinner("Running Planner...");
|
|
13222
13599
|
spinner.start();
|
|
13223
13600
|
try {
|
|
13601
|
+
if (!options.dryRun) {
|
|
13602
|
+
await sendNotifications(config, {
|
|
13603
|
+
event: "run_started",
|
|
13604
|
+
projectName: path31.basename(projectDir),
|
|
13605
|
+
exitCode: 0,
|
|
13606
|
+
provider: config.provider
|
|
13607
|
+
});
|
|
13608
|
+
}
|
|
13224
13609
|
const result = await sliceNextItem(projectDir, config);
|
|
13225
13610
|
if (result.sliced) {
|
|
13226
|
-
spinner.succeed(`
|
|
13611
|
+
spinner.succeed(`Planner completed successfully: Created ${result.file}`);
|
|
13227
13612
|
} else if (result.error) {
|
|
13228
13613
|
if (result.error === "No pending items to process") {
|
|
13229
13614
|
spinner.succeed("No pending items to process");
|
|
13230
13615
|
} else {
|
|
13231
|
-
spinner.fail(`
|
|
13616
|
+
spinner.fail(`Planner failed: ${result.error}`);
|
|
13232
13617
|
}
|
|
13233
13618
|
}
|
|
13234
13619
|
const nothingPending = result.error === "No pending items to process";
|
|
13235
13620
|
const exitCode = result.sliced || nothingPending ? 0 : 1;
|
|
13236
13621
|
if (!options.dryRun && result.sliced) {
|
|
13237
|
-
|
|
13238
|
-
|
|
13239
|
-
event,
|
|
13622
|
+
await sendNotifications(config, {
|
|
13623
|
+
event: "run_succeeded",
|
|
13240
13624
|
projectName: path31.basename(projectDir),
|
|
13241
13625
|
exitCode,
|
|
13242
13626
|
provider: config.provider,
|
|
13243
13627
|
prTitle: result.item?.title
|
|
13244
|
-
};
|
|
13245
|
-
|
|
13628
|
+
});
|
|
13629
|
+
} else if (!options.dryRun && !nothingPending) {
|
|
13630
|
+
await sendNotifications(config, {
|
|
13631
|
+
event: "run_failed",
|
|
13632
|
+
projectName: path31.basename(projectDir),
|
|
13633
|
+
exitCode,
|
|
13634
|
+
provider: config.provider
|
|
13635
|
+
});
|
|
13246
13636
|
}
|
|
13247
13637
|
process.exit(exitCode);
|
|
13248
13638
|
} catch (err) {
|
|
13249
|
-
spinner.fail("Failed to execute
|
|
13639
|
+
spinner.fail("Failed to execute planner command");
|
|
13250
13640
|
error(`${err instanceof Error ? err.message : String(err)}`);
|
|
13251
13641
|
process.exit(1);
|
|
13252
13642
|
}
|
|
@@ -13351,7 +13741,7 @@ async function confirmPrompt(question) {
|
|
|
13351
13741
|
}
|
|
13352
13742
|
async function createGitHubLabel(label2, cwd) {
|
|
13353
13743
|
try {
|
|
13354
|
-
|
|
13744
|
+
execFileSync3("gh", ["label", "create", label2.name, "--description", label2.description, "--color", label2.color], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
13355
13745
|
return { created: true, skipped: false };
|
|
13356
13746
|
} catch (err) {
|
|
13357
13747
|
const output = err instanceof Error ? err.message : String(err);
|
|
@@ -13491,11 +13881,11 @@ function boardCommand(program2) {
|
|
|
13491
13881
|
let body = options.body ?? "";
|
|
13492
13882
|
if (options.bodyFile) {
|
|
13493
13883
|
const filePath = options.bodyFile;
|
|
13494
|
-
if (!
|
|
13884
|
+
if (!fs32.existsSync(filePath)) {
|
|
13495
13885
|
console.error(`File not found: ${filePath}`);
|
|
13496
13886
|
process.exit(1);
|
|
13497
13887
|
}
|
|
13498
|
-
body =
|
|
13888
|
+
body = fs32.readFileSync(filePath, "utf-8");
|
|
13499
13889
|
}
|
|
13500
13890
|
const labels = [];
|
|
13501
13891
|
if (options.label) {
|
|
@@ -13704,11 +14094,11 @@ function boardCommand(program2) {
|
|
|
13704
14094
|
const provider = getProvider(config, cwd);
|
|
13705
14095
|
await ensureBoardConfigured(config, cwd, provider);
|
|
13706
14096
|
const roadmapPath = options.roadmap ?? path33.join(cwd, "ROADMAP.md");
|
|
13707
|
-
if (!
|
|
14097
|
+
if (!fs32.existsSync(roadmapPath)) {
|
|
13708
14098
|
console.error(`Roadmap file not found: ${roadmapPath}`);
|
|
13709
14099
|
process.exit(1);
|
|
13710
14100
|
}
|
|
13711
|
-
const roadmapContent =
|
|
14101
|
+
const roadmapContent = fs32.readFileSync(roadmapPath, "utf-8");
|
|
13712
14102
|
const items = parseRoadmap(roadmapContent);
|
|
13713
14103
|
const uncheckedItems = getUncheckedItems(items);
|
|
13714
14104
|
if (uncheckedItems.length === 0) {
|
|
@@ -13802,7 +14192,7 @@ function boardCommand(program2) {
|
|
|
13802
14192
|
try {
|
|
13803
14193
|
const labelsToAdd = [category, horizon].filter((l) => !issue.labels.includes(l));
|
|
13804
14194
|
if (labelsToAdd.length > 0) {
|
|
13805
|
-
|
|
14195
|
+
execFileSync3("gh", ["issue", "edit", String(issue.number), "--add-label", labelsToAdd.join(",")], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
13806
14196
|
}
|
|
13807
14197
|
updated++;
|
|
13808
14198
|
success(`Updated labels on #${issue.number}: ${item.title}`);
|
|
@@ -13826,14 +14216,14 @@ var __dirname4 = dirname8(__filename3);
|
|
|
13826
14216
|
function findPackageRoot(dir) {
|
|
13827
14217
|
let d = dir;
|
|
13828
14218
|
for (let i = 0; i < 5; i++) {
|
|
13829
|
-
if (
|
|
14219
|
+
if (existsSync26(join30(d, "package.json")))
|
|
13830
14220
|
return d;
|
|
13831
14221
|
d = dirname8(d);
|
|
13832
14222
|
}
|
|
13833
14223
|
return dir;
|
|
13834
14224
|
}
|
|
13835
14225
|
var packageRoot = findPackageRoot(__dirname4);
|
|
13836
|
-
var packageJson = JSON.parse(
|
|
14226
|
+
var packageJson = JSON.parse(readFileSync16(join30(packageRoot, "package.json"), "utf-8"));
|
|
13837
14227
|
var program = new Command2();
|
|
13838
14228
|
program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
|
|
13839
14229
|
initCommand(program);
|