@jonit-dev/night-watch-cli 1.7.99 → 1.8.2
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 +756 -289
- package/dist/cli.js.map +1 -1
- package/dist/commands/analytics.d.ts.map +1 -1
- package/dist/commands/analytics.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +24 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +5 -0
- package/dist/commands/review.js.map +1 -1
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +29 -2
- package/dist/web/assets/index-B_l_3wnA.js +370 -0
- package/dist/web/assets/index-Bbb4-39N.js +370 -0
- package/dist/web/assets/index-CLuRf7Zt.js +381 -0
- package/dist/web/assets/index-CvUk-33B.css +1 -0
- package/dist/web/assets/index-DI4kFgOi.js +370 -0
- package/dist/web/assets/index-DgOAgkZy.css +1 -0
- package/dist/web/assets/index-aCHmkAcJ.css +1 -0
- package/dist/web/assets/index-oOp_MFeE.js +376 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ function resolveProviderBucketKey(provider, providerEnv) {
|
|
|
31
31
|
return `claude-proxy:${baseUrl}`;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_MAX_RUNTIME, DEFAULT_REVIEWER_MAX_RUNTIME, DEFAULT_CRON_SCHEDULE, DEFAULT_REVIEWER_SCHEDULE, DEFAULT_CRON_SCHEDULE_OFFSET, DEFAULT_MAX_RETRIES, DEFAULT_REVIEWER_MAX_RETRIES, DEFAULT_REVIEWER_RETRY_DELAY, DEFAULT_BRANCH_PREFIX, DEFAULT_BRANCH_PATTERNS, DEFAULT_MIN_REVIEW_SCORE, DEFAULT_MAX_LOG_SIZE, DEFAULT_PROVIDER, DEFAULT_EXECUTOR_ENABLED, DEFAULT_REVIEWER_ENABLED, DEFAULT_PROVIDER_ENV, DEFAULT_FALLBACK_ON_RATE_LIMIT, DEFAULT_CLAUDE_MODEL, DEFAULT_PRIMARY_FALLBACK_MODEL, DEFAULT_SECONDARY_FALLBACK_MODEL, VALID_CLAUDE_MODELS, CLAUDE_MODEL_IDS, DEFAULT_NOTIFICATIONS, DEFAULT_PRD_PRIORITY, DEFAULT_SLICER_SCHEDULE, DEFAULT_SLICER_MAX_RUNTIME, DEFAULT_ROADMAP_SCANNER, DEFAULT_TEMPLATES_DIR, DEFAULT_BOARD_PROVIDER, DEFAULT_LOCAL_BOARD_INFO, DEFAULT_AUTO_MERGE, DEFAULT_AUTO_MERGE_METHOD, VALID_MERGE_METHODS, DEFAULT_QA_ENABLED, DEFAULT_QA_SCHEDULE, DEFAULT_QA_MAX_RUNTIME, DEFAULT_QA_ARTIFACTS, DEFAULT_QA_SKIP_LABEL, DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT, DEFAULT_QA, QA_LOG_NAME, DEFAULT_AUDIT_ENABLED, DEFAULT_AUDIT_SCHEDULE, DEFAULT_AUDIT_MAX_RUNTIME, DEFAULT_AUDIT, AUDIT_LOG_NAME, PLANNER_LOG_NAME, VALID_PROVIDERS, VALID_JOB_TYPES, DEFAULT_JOB_PROVIDERS, BUILT_IN_PRESETS, BUILT_IN_PRESET_IDS, PROVIDER_COMMANDS, CONFIG_FILE_NAME, LOCK_FILE_PREFIX, LOG_DIR, CLAIM_FILE_EXTENSION, EXECUTOR_LOG_NAME, REVIEWER_LOG_NAME, EXECUTOR_LOG_FILE, REVIEWER_LOG_FILE, LOG_FILE_NAMES, GLOBAL_CONFIG_DIR, REGISTRY_FILE_NAME, HISTORY_FILE_NAME, PRD_STATES_FILE_NAME, STATE_DB_FILE_NAME, MAX_HISTORY_RECORDS_PER_PRD, DEFAULT_QUEUE_ENABLED, DEFAULT_QUEUE_MODE, DEFAULT_QUEUE_MAX_CONCURRENCY, DEFAULT_QUEUE_MAX_WAIT_TIME, DEFAULT_QUEUE_PRIORITY, DEFAULT_QUEUE, DEFAULT_SCHEDULING_PRIORITY, QUEUE_LOCK_FILE_NAME;
|
|
34
|
+
var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_MAX_RUNTIME, DEFAULT_REVIEWER_MAX_RUNTIME, DEFAULT_CRON_SCHEDULE, DEFAULT_REVIEWER_SCHEDULE, DEFAULT_CRON_SCHEDULE_OFFSET, DEFAULT_MAX_RETRIES, DEFAULT_REVIEWER_MAX_RETRIES, DEFAULT_REVIEWER_RETRY_DELAY, DEFAULT_REVIEWER_MAX_PRS_PER_RUN, DEFAULT_BRANCH_PREFIX, DEFAULT_BRANCH_PATTERNS, DEFAULT_MIN_REVIEW_SCORE, DEFAULT_MAX_LOG_SIZE, DEFAULT_PROVIDER, DEFAULT_EXECUTOR_ENABLED, DEFAULT_REVIEWER_ENABLED, DEFAULT_PROVIDER_ENV, DEFAULT_FALLBACK_ON_RATE_LIMIT, DEFAULT_CLAUDE_MODEL, DEFAULT_PRIMARY_FALLBACK_MODEL, DEFAULT_SECONDARY_FALLBACK_MODEL, VALID_CLAUDE_MODELS, CLAUDE_MODEL_IDS, DEFAULT_NOTIFICATIONS, DEFAULT_PRD_PRIORITY, DEFAULT_SLICER_SCHEDULE, DEFAULT_SLICER_MAX_RUNTIME, DEFAULT_ROADMAP_SCANNER, DEFAULT_TEMPLATES_DIR, DEFAULT_BOARD_PROVIDER, DEFAULT_LOCAL_BOARD_INFO, DEFAULT_AUTO_MERGE, DEFAULT_AUTO_MERGE_METHOD, VALID_MERGE_METHODS, DEFAULT_QA_ENABLED, DEFAULT_QA_SCHEDULE, DEFAULT_QA_MAX_RUNTIME, DEFAULT_QA_ARTIFACTS, DEFAULT_QA_SKIP_LABEL, DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT, DEFAULT_QA, QA_LOG_NAME, DEFAULT_AUDIT_ENABLED, DEFAULT_AUDIT_SCHEDULE, DEFAULT_AUDIT_MAX_RUNTIME, DEFAULT_AUDIT, DEFAULT_ANALYTICS_ENABLED, DEFAULT_ANALYTICS_SCHEDULE, DEFAULT_ANALYTICS_MAX_RUNTIME, DEFAULT_ANALYTICS_LOOKBACK_DAYS, DEFAULT_ANALYTICS_TARGET_COLUMN, DEFAULT_ANALYTICS_PROMPT, DEFAULT_ANALYTICS, AUDIT_LOG_NAME, PLANNER_LOG_NAME, ANALYTICS_LOG_NAME, VALID_PROVIDERS, VALID_JOB_TYPES, DEFAULT_JOB_PROVIDERS, BUILT_IN_PRESETS, BUILT_IN_PRESET_IDS, PROVIDER_COMMANDS, CONFIG_FILE_NAME, LOCK_FILE_PREFIX, LOG_DIR, CLAIM_FILE_EXTENSION, EXECUTOR_LOG_NAME, REVIEWER_LOG_NAME, EXECUTOR_LOG_FILE, REVIEWER_LOG_FILE, LOG_FILE_NAMES, GLOBAL_CONFIG_DIR, REGISTRY_FILE_NAME, HISTORY_FILE_NAME, PRD_STATES_FILE_NAME, STATE_DB_FILE_NAME, MAX_HISTORY_RECORDS_PER_PRD, DEFAULT_QUEUE_ENABLED, DEFAULT_QUEUE_MODE, DEFAULT_QUEUE_MAX_CONCURRENCY, DEFAULT_QUEUE_MAX_WAIT_TIME, DEFAULT_QUEUE_PRIORITY, DEFAULT_QUEUE, DEFAULT_SCHEDULING_PRIORITY, QUEUE_LOCK_FILE_NAME;
|
|
35
35
|
var init_constants = __esm({
|
|
36
36
|
"../core/dist/constants.js"() {
|
|
37
37
|
"use strict";
|
|
@@ -45,6 +45,7 @@ var init_constants = __esm({
|
|
|
45
45
|
DEFAULT_MAX_RETRIES = 3;
|
|
46
46
|
DEFAULT_REVIEWER_MAX_RETRIES = 2;
|
|
47
47
|
DEFAULT_REVIEWER_RETRY_DELAY = 30;
|
|
48
|
+
DEFAULT_REVIEWER_MAX_PRS_PER_RUN = 0;
|
|
48
49
|
DEFAULT_BRANCH_PREFIX = "night-watch";
|
|
49
50
|
DEFAULT_BRANCH_PATTERNS = ["feat/", "night-watch/"];
|
|
50
51
|
DEFAULT_MIN_REVIEW_SCORE = 80;
|
|
@@ -108,10 +109,36 @@ var init_constants = __esm({
|
|
|
108
109
|
schedule: DEFAULT_AUDIT_SCHEDULE,
|
|
109
110
|
maxRuntime: DEFAULT_AUDIT_MAX_RUNTIME
|
|
110
111
|
};
|
|
112
|
+
DEFAULT_ANALYTICS_ENABLED = false;
|
|
113
|
+
DEFAULT_ANALYTICS_SCHEDULE = "0 6 * * 1";
|
|
114
|
+
DEFAULT_ANALYTICS_MAX_RUNTIME = 900;
|
|
115
|
+
DEFAULT_ANALYTICS_LOOKBACK_DAYS = 7;
|
|
116
|
+
DEFAULT_ANALYTICS_TARGET_COLUMN = "Draft";
|
|
117
|
+
DEFAULT_ANALYTICS_PROMPT = `You are an analytics reviewer. Analyze the following Amplitude product analytics data.
|
|
118
|
+
Identify significant trends, anomalies, or drops that warrant engineering attention.
|
|
119
|
+
For each actionable finding, output a JSON array of issues:
|
|
120
|
+
[{ "title": "...", "body": "...", "labels": ["analytics"] }]
|
|
121
|
+
If no issues are warranted, output an empty array: []`;
|
|
122
|
+
DEFAULT_ANALYTICS = {
|
|
123
|
+
enabled: DEFAULT_ANALYTICS_ENABLED,
|
|
124
|
+
schedule: DEFAULT_ANALYTICS_SCHEDULE,
|
|
125
|
+
maxRuntime: DEFAULT_ANALYTICS_MAX_RUNTIME,
|
|
126
|
+
lookbackDays: DEFAULT_ANALYTICS_LOOKBACK_DAYS,
|
|
127
|
+
targetColumn: DEFAULT_ANALYTICS_TARGET_COLUMN,
|
|
128
|
+
analysisPrompt: DEFAULT_ANALYTICS_PROMPT
|
|
129
|
+
};
|
|
111
130
|
AUDIT_LOG_NAME = "audit";
|
|
112
131
|
PLANNER_LOG_NAME = "slicer";
|
|
132
|
+
ANALYTICS_LOG_NAME = "analytics";
|
|
113
133
|
VALID_PROVIDERS = ["claude", "codex"];
|
|
114
|
-
VALID_JOB_TYPES = [
|
|
134
|
+
VALID_JOB_TYPES = [
|
|
135
|
+
"executor",
|
|
136
|
+
"reviewer",
|
|
137
|
+
"qa",
|
|
138
|
+
"audit",
|
|
139
|
+
"slicer",
|
|
140
|
+
"analytics"
|
|
141
|
+
];
|
|
115
142
|
DEFAULT_JOB_PROVIDERS = {};
|
|
116
143
|
BUILT_IN_PRESETS = {
|
|
117
144
|
claude: {
|
|
@@ -190,7 +217,8 @@ var init_constants = __esm({
|
|
|
190
217
|
reviewer: REVIEWER_LOG_NAME,
|
|
191
218
|
qa: QA_LOG_NAME,
|
|
192
219
|
audit: AUDIT_LOG_NAME,
|
|
193
|
-
planner: PLANNER_LOG_NAME
|
|
220
|
+
planner: PLANNER_LOG_NAME,
|
|
221
|
+
analytics: ANALYTICS_LOG_NAME
|
|
194
222
|
};
|
|
195
223
|
GLOBAL_CONFIG_DIR = ".night-watch";
|
|
196
224
|
REGISTRY_FILE_NAME = "projects.json";
|
|
@@ -207,7 +235,8 @@ var init_constants = __esm({
|
|
|
207
235
|
reviewer: 40,
|
|
208
236
|
slicer: 30,
|
|
209
237
|
qa: 20,
|
|
210
|
-
audit: 10
|
|
238
|
+
audit: 10,
|
|
239
|
+
analytics: 10
|
|
211
240
|
};
|
|
212
241
|
DEFAULT_QUEUE = {
|
|
213
242
|
enabled: DEFAULT_QUEUE_ENABLED,
|
|
@@ -222,6 +251,15 @@ var init_constants = __esm({
|
|
|
222
251
|
}
|
|
223
252
|
});
|
|
224
253
|
|
|
254
|
+
// ../core/dist/board/types.js
|
|
255
|
+
var BOARD_COLUMNS;
|
|
256
|
+
var init_types2 = __esm({
|
|
257
|
+
"../core/dist/board/types.js"() {
|
|
258
|
+
"use strict";
|
|
259
|
+
BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
225
263
|
// ../core/dist/config-normalize.js
|
|
226
264
|
function validateProvider(value) {
|
|
227
265
|
const trimmed = value.trim();
|
|
@@ -263,6 +301,7 @@ function normalizeConfig(rawConfig) {
|
|
|
263
301
|
normalized.maxRetries = readNumber(rawConfig.maxRetries);
|
|
264
302
|
normalized.reviewerMaxRetries = readNumber(rawConfig.reviewerMaxRetries);
|
|
265
303
|
normalized.reviewerRetryDelay = readNumber(rawConfig.reviewerRetryDelay);
|
|
304
|
+
normalized.reviewerMaxPrsPerRun = readNumber(rawConfig.reviewerMaxPrsPerRun);
|
|
266
305
|
normalized.provider = validateProvider(String(rawConfig.provider ?? "")) ?? void 0;
|
|
267
306
|
normalized.executorEnabled = readBoolean(rawConfig.executorEnabled);
|
|
268
307
|
normalized.reviewerEnabled = readBoolean(rawConfig.reviewerEnabled);
|
|
@@ -416,6 +455,20 @@ function normalizeConfig(rawConfig) {
|
|
|
416
455
|
};
|
|
417
456
|
normalized.audit = audit;
|
|
418
457
|
}
|
|
458
|
+
const rawAnalytics = readObject(rawConfig.analytics);
|
|
459
|
+
if (rawAnalytics) {
|
|
460
|
+
const targetColumnRaw = readString(rawAnalytics.targetColumn);
|
|
461
|
+
const targetColumn = targetColumnRaw && BOARD_COLUMNS.includes(targetColumnRaw) ? targetColumnRaw : DEFAULT_ANALYTICS.targetColumn;
|
|
462
|
+
const analytics = {
|
|
463
|
+
enabled: readBoolean(rawAnalytics.enabled) ?? DEFAULT_ANALYTICS.enabled,
|
|
464
|
+
schedule: readString(rawAnalytics.schedule) ?? DEFAULT_ANALYTICS.schedule,
|
|
465
|
+
maxRuntime: readNumber(rawAnalytics.maxRuntime) ?? DEFAULT_ANALYTICS.maxRuntime,
|
|
466
|
+
lookbackDays: readNumber(rawAnalytics.lookbackDays) ?? DEFAULT_ANALYTICS.lookbackDays,
|
|
467
|
+
targetColumn,
|
|
468
|
+
analysisPrompt: readString(rawAnalytics.analysisPrompt) ?? DEFAULT_ANALYTICS.analysisPrompt
|
|
469
|
+
};
|
|
470
|
+
normalized.analytics = analytics;
|
|
471
|
+
}
|
|
419
472
|
const rawJobProviders = readObject(rawConfig.jobProviders);
|
|
420
473
|
if (rawJobProviders) {
|
|
421
474
|
const jobProviders = {};
|
|
@@ -475,6 +528,7 @@ function normalizeConfig(rawConfig) {
|
|
|
475
528
|
var init_config_normalize = __esm({
|
|
476
529
|
"../core/dist/config-normalize.js"() {
|
|
477
530
|
"use strict";
|
|
531
|
+
init_types2();
|
|
478
532
|
init_constants();
|
|
479
533
|
}
|
|
480
534
|
});
|
|
@@ -558,6 +612,11 @@ function buildEnvOverrideConfig(fileConfig) {
|
|
|
558
612
|
if (!isNaN(v) && v >= 0)
|
|
559
613
|
env.reviewerRetryDelay = v;
|
|
560
614
|
}
|
|
615
|
+
if (process.env.NW_REVIEWER_MAX_PRS_PER_RUN !== void 0) {
|
|
616
|
+
const v = parseInt(process.env.NW_REVIEWER_MAX_PRS_PER_RUN, 10);
|
|
617
|
+
if (!isNaN(v) && v >= 0)
|
|
618
|
+
env.reviewerMaxPrsPerRun = v;
|
|
619
|
+
}
|
|
561
620
|
if (process.env.NW_PROVIDER) {
|
|
562
621
|
const p = validateProvider(process.env.NW_PROVIDER);
|
|
563
622
|
if (p !== null)
|
|
@@ -693,6 +752,25 @@ function buildEnvOverrideConfig(fileConfig) {
|
|
|
693
752
|
if (!isNaN(v) && v > 0)
|
|
694
753
|
env.audit = { ...auditBase(), maxRuntime: v };
|
|
695
754
|
}
|
|
755
|
+
const analyticsBase = () => env.analytics ?? fileConfig?.analytics ?? DEFAULT_ANALYTICS;
|
|
756
|
+
if (process.env.NW_ANALYTICS_ENABLED) {
|
|
757
|
+
const v = parseBoolean(process.env.NW_ANALYTICS_ENABLED);
|
|
758
|
+
if (v !== null)
|
|
759
|
+
env.analytics = { ...analyticsBase(), enabled: v };
|
|
760
|
+
}
|
|
761
|
+
if (process.env.NW_ANALYTICS_SCHEDULE) {
|
|
762
|
+
env.analytics = { ...analyticsBase(), schedule: process.env.NW_ANALYTICS_SCHEDULE };
|
|
763
|
+
}
|
|
764
|
+
if (process.env.NW_ANALYTICS_MAX_RUNTIME) {
|
|
765
|
+
const v = parseInt(process.env.NW_ANALYTICS_MAX_RUNTIME, 10);
|
|
766
|
+
if (!isNaN(v) && v > 0)
|
|
767
|
+
env.analytics = { ...analyticsBase(), maxRuntime: v };
|
|
768
|
+
}
|
|
769
|
+
if (process.env.NW_ANALYTICS_LOOKBACK_DAYS) {
|
|
770
|
+
const v = parseInt(process.env.NW_ANALYTICS_LOOKBACK_DAYS, 10);
|
|
771
|
+
if (!isNaN(v) && v > 0)
|
|
772
|
+
env.analytics = { ...analyticsBase(), lookbackDays: v };
|
|
773
|
+
}
|
|
696
774
|
const jobProvidersEnv = {};
|
|
697
775
|
for (const jobType of VALID_JOB_TYPES) {
|
|
698
776
|
const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
|
|
@@ -767,6 +845,7 @@ function getDefaultConfig() {
|
|
|
767
845
|
maxRetries: DEFAULT_MAX_RETRIES,
|
|
768
846
|
reviewerMaxRetries: DEFAULT_REVIEWER_MAX_RETRIES,
|
|
769
847
|
reviewerRetryDelay: DEFAULT_REVIEWER_RETRY_DELAY,
|
|
848
|
+
reviewerMaxPrsPerRun: DEFAULT_REVIEWER_MAX_PRS_PER_RUN,
|
|
770
849
|
provider: DEFAULT_PROVIDER,
|
|
771
850
|
executorEnabled: DEFAULT_EXECUTOR_ENABLED,
|
|
772
851
|
reviewerEnabled: DEFAULT_REVIEWER_ENABLED,
|
|
@@ -784,6 +863,7 @@ function getDefaultConfig() {
|
|
|
784
863
|
claudeModel: DEFAULT_CLAUDE_MODEL,
|
|
785
864
|
qa: { ...DEFAULT_QA },
|
|
786
865
|
audit: { ...DEFAULT_AUDIT },
|
|
866
|
+
analytics: { ...DEFAULT_ANALYTICS },
|
|
787
867
|
jobProviders: { ...DEFAULT_JOB_PROVIDERS },
|
|
788
868
|
queue: { ...DEFAULT_QUEUE }
|
|
789
869
|
};
|
|
@@ -827,6 +907,16 @@ function sanitizeReviewerRetryDelay(value, fallback) {
|
|
|
827
907
|
return 300;
|
|
828
908
|
return n;
|
|
829
909
|
}
|
|
910
|
+
function sanitizeReviewerMaxPrsPerRun(value, fallback) {
|
|
911
|
+
if (!Number.isFinite(value))
|
|
912
|
+
return fallback;
|
|
913
|
+
const n = Math.floor(value);
|
|
914
|
+
if (n < 0)
|
|
915
|
+
return 0;
|
|
916
|
+
if (n > 100)
|
|
917
|
+
return 100;
|
|
918
|
+
return n;
|
|
919
|
+
}
|
|
830
920
|
function mergeConfigLayer(base, layer) {
|
|
831
921
|
for (const _key of Object.keys(layer)) {
|
|
832
922
|
const value = layer[_key];
|
|
@@ -840,7 +930,7 @@ function mergeConfigLayer(base, layer) {
|
|
|
840
930
|
...layerQueue,
|
|
841
931
|
providerBuckets: { ...baseQueue.providerBuckets, ...layerQueue.providerBuckets }
|
|
842
932
|
};
|
|
843
|
-
} else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit") {
|
|
933
|
+
} else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics") {
|
|
844
934
|
base[_key] = {
|
|
845
935
|
...base[_key],
|
|
846
936
|
...value
|
|
@@ -862,6 +952,7 @@ function mergeConfigs(base, fileConfig, envConfig) {
|
|
|
862
952
|
merged.maxRetries = sanitizeMaxRetries(merged.maxRetries, DEFAULT_MAX_RETRIES);
|
|
863
953
|
merged.reviewerMaxRetries = sanitizeReviewerMaxRetries(merged.reviewerMaxRetries, DEFAULT_REVIEWER_MAX_RETRIES);
|
|
864
954
|
merged.reviewerRetryDelay = sanitizeReviewerRetryDelay(merged.reviewerRetryDelay, DEFAULT_REVIEWER_RETRY_DELAY);
|
|
955
|
+
merged.reviewerMaxPrsPerRun = sanitizeReviewerMaxPrsPerRun(merged.reviewerMaxPrsPerRun, DEFAULT_REVIEWER_MAX_PRS_PER_RUN);
|
|
865
956
|
merged.primaryFallbackModel = merged.primaryFallbackModel ?? merged.claudeModel ?? DEFAULT_PRIMARY_FALLBACK_MODEL;
|
|
866
957
|
merged.secondaryFallbackModel = merged.secondaryFallbackModel ?? merged.primaryFallbackModel ?? DEFAULT_SECONDARY_FALLBACK_MODEL;
|
|
867
958
|
merged.claudeModel = merged.primaryFallbackModel;
|
|
@@ -924,15 +1015,6 @@ var init_config = __esm({
|
|
|
924
1015
|
}
|
|
925
1016
|
});
|
|
926
1017
|
|
|
927
|
-
// ../core/dist/board/types.js
|
|
928
|
-
var BOARD_COLUMNS;
|
|
929
|
-
var init_types2 = __esm({
|
|
930
|
-
"../core/dist/board/types.js"() {
|
|
931
|
-
"use strict";
|
|
932
|
-
BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
|
|
936
1018
|
// ../core/dist/storage/repositories/sqlite/execution-history.repository.js
|
|
937
1019
|
import Database from "better-sqlite3";
|
|
938
1020
|
import { inject, injectable } from "tsyringe";
|
|
@@ -3027,6 +3109,9 @@ function auditLockPath(projectDir) {
|
|
|
3027
3109
|
function plannerLockPath(projectDir) {
|
|
3028
3110
|
return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
|
|
3029
3111
|
}
|
|
3112
|
+
function analyticsLockPath(projectDir) {
|
|
3113
|
+
return `${LOCK_FILE_PREFIX}analytics-${projectRuntimeKey(projectDir)}.lock`;
|
|
3114
|
+
}
|
|
3030
3115
|
function isProcessRunning(pid) {
|
|
3031
3116
|
try {
|
|
3032
3117
|
process.kill(pid, 0);
|
|
@@ -3385,7 +3470,8 @@ function collectLogInfo(projectDir) {
|
|
|
3385
3470
|
{ name: "reviewer", fileName: "reviewer.log" },
|
|
3386
3471
|
{ name: "qa", fileName: `${QA_LOG_NAME}.log` },
|
|
3387
3472
|
{ name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
|
|
3388
|
-
{ name: "planner", fileName: `${PLANNER_LOG_NAME}.log` }
|
|
3473
|
+
{ name: "planner", fileName: `${PLANNER_LOG_NAME}.log` },
|
|
3474
|
+
{ name: "analytics", fileName: `${ANALYTICS_LOG_NAME}.log` }
|
|
3389
3475
|
];
|
|
3390
3476
|
return logEntries.map(({ name, fileName }) => {
|
|
3391
3477
|
const logPath = path6.join(projectDir, LOG_DIR, fileName);
|
|
@@ -3414,12 +3500,14 @@ async function fetchStatusSnapshot(projectDir, config) {
|
|
|
3414
3500
|
const qaLock = checkLockFile(qaLockPath(projectDir));
|
|
3415
3501
|
const auditLock = checkLockFile(auditLockPath(projectDir));
|
|
3416
3502
|
const plannerLock = checkLockFile(plannerLockPath(projectDir));
|
|
3503
|
+
const analyticsLock = checkLockFile(analyticsLockPath(projectDir));
|
|
3417
3504
|
const processes = [
|
|
3418
3505
|
{ name: "executor", running: executorLock.running, pid: executorLock.pid },
|
|
3419
3506
|
{ name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
|
|
3420
3507
|
{ name: "qa", running: qaLock.running, pid: qaLock.pid },
|
|
3421
3508
|
{ name: "audit", running: auditLock.running, pid: auditLock.pid },
|
|
3422
|
-
{ name: "planner", running: plannerLock.running, pid: plannerLock.pid }
|
|
3509
|
+
{ name: "planner", running: plannerLock.running, pid: plannerLock.pid },
|
|
3510
|
+
{ name: "analytics", running: analyticsLock.running, pid: analyticsLock.pid }
|
|
3423
3511
|
];
|
|
3424
3512
|
const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
|
|
3425
3513
|
const prs = await collectPrInfo(projectDir, config.branchPatterns);
|
|
@@ -3760,6 +3848,18 @@ import * as path8 from "path";
|
|
|
3760
3848
|
function isPlainObject(value) {
|
|
3761
3849
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3762
3850
|
}
|
|
3851
|
+
function cleanJobProviders(value) {
|
|
3852
|
+
if (!isPlainObject(value)) {
|
|
3853
|
+
return {};
|
|
3854
|
+
}
|
|
3855
|
+
const entries = [];
|
|
3856
|
+
for (const [key, provider] of Object.entries(value)) {
|
|
3857
|
+
if (typeof provider === "string" && provider.trim().length > 0) {
|
|
3858
|
+
entries.push([key, provider]);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
return Object.fromEntries(entries);
|
|
3862
|
+
}
|
|
3763
3863
|
function saveConfig(projectDir, changes) {
|
|
3764
3864
|
const configPath = path8.join(projectDir, CONFIG_FILE_NAME);
|
|
3765
3865
|
try {
|
|
@@ -3771,7 +3871,9 @@ function saveConfig(projectDir, changes) {
|
|
|
3771
3871
|
const merged = { ...existing };
|
|
3772
3872
|
for (const [key, value] of Object.entries(changes)) {
|
|
3773
3873
|
if (value !== void 0) {
|
|
3774
|
-
if (
|
|
3874
|
+
if (key === "jobProviders") {
|
|
3875
|
+
merged[key] = cleanJobProviders(value);
|
|
3876
|
+
} else if (PARTIAL_MERGE_KEYS.has(key) && isPlainObject(existing[key]) && isPlainObject(value)) {
|
|
3775
3877
|
merged[key] = { ...existing[key], ...value };
|
|
3776
3878
|
} else {
|
|
3777
3879
|
merged[key] = value;
|
|
@@ -3792,7 +3894,14 @@ var init_config_writer = __esm({
|
|
|
3792
3894
|
"../core/dist/utils/config-writer.js"() {
|
|
3793
3895
|
"use strict";
|
|
3794
3896
|
init_constants();
|
|
3795
|
-
PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set([
|
|
3897
|
+
PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set([
|
|
3898
|
+
"notifications",
|
|
3899
|
+
"qa",
|
|
3900
|
+
"audit",
|
|
3901
|
+
"roadmapScanner",
|
|
3902
|
+
"queue",
|
|
3903
|
+
"providerPresets"
|
|
3904
|
+
]);
|
|
3796
3905
|
}
|
|
3797
3906
|
});
|
|
3798
3907
|
|
|
@@ -5588,6 +5697,8 @@ function isJobTypeEnabled(config, jobType) {
|
|
|
5588
5697
|
return config.audit.enabled;
|
|
5589
5698
|
case "slicer":
|
|
5590
5699
|
return config.roadmapScanner.enabled;
|
|
5700
|
+
case "analytics":
|
|
5701
|
+
return config.analytics.enabled;
|
|
5591
5702
|
default:
|
|
5592
5703
|
return true;
|
|
5593
5704
|
}
|
|
@@ -5947,6 +6058,8 @@ function getLockPathForJob(projectPath, jobType) {
|
|
|
5947
6058
|
return auditLockPath(projectPath);
|
|
5948
6059
|
case "slicer":
|
|
5949
6060
|
return plannerLockPath(projectPath);
|
|
6061
|
+
case "analytics":
|
|
6062
|
+
return analyticsLockPath(projectPath);
|
|
5950
6063
|
}
|
|
5951
6064
|
}
|
|
5952
6065
|
function reconcileStaleRunningJobs(db) {
|
|
@@ -6126,7 +6239,10 @@ function fitsProviderCapacity(candidate, config, inFlightByBucket) {
|
|
|
6126
6239
|
}
|
|
6127
6240
|
const bucketConfig = config?.providerBuckets?.[bucketKey];
|
|
6128
6241
|
if (!bucketConfig) {
|
|
6129
|
-
logger.debug("Capacity check skipped: bucket not configured", {
|
|
6242
|
+
logger.debug("Capacity check skipped: bucket not configured", {
|
|
6243
|
+
id: candidate.id,
|
|
6244
|
+
bucket: bucketKey
|
|
6245
|
+
});
|
|
6130
6246
|
return true;
|
|
6131
6247
|
}
|
|
6132
6248
|
const inFlightCount = inFlightByBucket[bucketKey] ?? 0;
|
|
@@ -6157,7 +6273,10 @@ function dispatchNextJob(config) {
|
|
|
6157
6273
|
const runningCount = running?.count ?? 0;
|
|
6158
6274
|
logger.debug("Dispatch attempt", { mode, runningCount, maxConcurrency });
|
|
6159
6275
|
if (runningCount >= maxConcurrency) {
|
|
6160
|
-
logger.info("Dispatch skipped: global concurrency limit reached", {
|
|
6276
|
+
logger.info("Dispatch skipped: global concurrency limit reached", {
|
|
6277
|
+
runningCount,
|
|
6278
|
+
maxConcurrency
|
|
6279
|
+
});
|
|
6161
6280
|
return null;
|
|
6162
6281
|
}
|
|
6163
6282
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -6183,7 +6302,9 @@ function dispatchNextJob(config) {
|
|
|
6183
6302
|
logger.debug("Dispatch skipped: no pending jobs");
|
|
6184
6303
|
return null;
|
|
6185
6304
|
}
|
|
6186
|
-
logger.debug("Provider-aware dispatch: evaluating candidates", {
|
|
6305
|
+
logger.debug("Provider-aware dispatch: evaluating candidates", {
|
|
6306
|
+
candidateCount: candidates.length
|
|
6307
|
+
});
|
|
6187
6308
|
const inFlightByBucket = getInFlightCountByBucket(db);
|
|
6188
6309
|
for (const candidate of candidates) {
|
|
6189
6310
|
if (fitsProviderCapacity(candidate, config, inFlightByBucket)) {
|
|
@@ -6259,7 +6380,11 @@ function clearQueue(filter, force) {
|
|
|
6259
6380
|
} else {
|
|
6260
6381
|
result = db.prepare(`DELETE FROM job_queue WHERE status IN ${statuses}`).run();
|
|
6261
6382
|
}
|
|
6262
|
-
logger.info("Queue cleared", {
|
|
6383
|
+
logger.info("Queue cleared", {
|
|
6384
|
+
count: result.changes,
|
|
6385
|
+
filter: filter ?? "all",
|
|
6386
|
+
force: force ?? false
|
|
6387
|
+
});
|
|
6263
6388
|
return result.changes;
|
|
6264
6389
|
} finally {
|
|
6265
6390
|
db.close();
|
|
@@ -6392,6 +6517,192 @@ var init_job_queue = __esm({
|
|
|
6392
6517
|
}
|
|
6393
6518
|
});
|
|
6394
6519
|
|
|
6520
|
+
// ../core/dist/analytics/amplitude-client.js
|
|
6521
|
+
function buildAuthHeader(apiKey, secretKey) {
|
|
6522
|
+
return `Basic ${Buffer.from(`${apiKey}:${secretKey}`).toString("base64")}`;
|
|
6523
|
+
}
|
|
6524
|
+
function buildDateRange(lookbackDays) {
|
|
6525
|
+
const end = /* @__PURE__ */ new Date();
|
|
6526
|
+
const start = /* @__PURE__ */ new Date();
|
|
6527
|
+
start.setDate(start.getDate() - lookbackDays);
|
|
6528
|
+
const fmt = (d) => d.toISOString().slice(0, 10).replace(/-/g, "");
|
|
6529
|
+
return { start: fmt(start), end: fmt(end) };
|
|
6530
|
+
}
|
|
6531
|
+
async function amplitudeFetch(url, authHeader, label2) {
|
|
6532
|
+
logger2.debug(`Fetching ${label2}`, { url });
|
|
6533
|
+
const response = await fetch(url, {
|
|
6534
|
+
headers: { Authorization: authHeader }
|
|
6535
|
+
});
|
|
6536
|
+
if (!response.ok) {
|
|
6537
|
+
if (response.status === 401) {
|
|
6538
|
+
throw new Error(`Amplitude authentication failed (401). Check your API Key and Secret Key.`);
|
|
6539
|
+
}
|
|
6540
|
+
if (response.status === 429) {
|
|
6541
|
+
throw new Error(`Amplitude rate limit exceeded (429). Try again later.`);
|
|
6542
|
+
}
|
|
6543
|
+
throw new Error(`Amplitude API error: ${response.status} ${response.statusText}`);
|
|
6544
|
+
}
|
|
6545
|
+
return response.json();
|
|
6546
|
+
}
|
|
6547
|
+
async function fetchAmplitudeData(apiKey, secretKey, lookbackDays) {
|
|
6548
|
+
const authHeader = buildAuthHeader(apiKey, secretKey);
|
|
6549
|
+
const { start, end } = buildDateRange(lookbackDays);
|
|
6550
|
+
logger2.info("Fetching Amplitude data", { lookbackDays, start, end });
|
|
6551
|
+
const baseUrl = "https://amplitude.com/api/2";
|
|
6552
|
+
const allEventsParam = encodeURIComponent('{"event_type":"_all"}');
|
|
6553
|
+
const [activeUsers, eventSegmentation, retention, userSessions] = await Promise.allSettled([
|
|
6554
|
+
amplitudeFetch(`${baseUrl}/users/active?start=${start}&end=${end}`, authHeader, "active users"),
|
|
6555
|
+
amplitudeFetch(`${baseUrl}/events/segmentation?start=${start}&end=${end}&e=${allEventsParam}`, authHeader, "event segmentation"),
|
|
6556
|
+
amplitudeFetch(`${baseUrl}/retention?se=${allEventsParam}&re=${allEventsParam}&start=${start}&end=${end}`, authHeader, "retention"),
|
|
6557
|
+
amplitudeFetch(`${baseUrl}/sessions/average?start=${start}&end=${end}`, authHeader, "user sessions")
|
|
6558
|
+
]);
|
|
6559
|
+
const settled = [activeUsers, eventSegmentation, retention, userSessions];
|
|
6560
|
+
const labels = ["active users", "event segmentation", "retention", "user sessions"];
|
|
6561
|
+
if (settled.every((r) => r.status === "rejected")) {
|
|
6562
|
+
throw settled[0].reason;
|
|
6563
|
+
}
|
|
6564
|
+
const extract = (result, label2) => {
|
|
6565
|
+
if (result.status === "fulfilled")
|
|
6566
|
+
return result.value;
|
|
6567
|
+
logger2.warn(`Failed to fetch ${label2}`, { error: String(result.reason) });
|
|
6568
|
+
return null;
|
|
6569
|
+
};
|
|
6570
|
+
return {
|
|
6571
|
+
activeUsers: extract(activeUsers, labels[0]),
|
|
6572
|
+
eventSegmentation: extract(eventSegmentation, labels[1]),
|
|
6573
|
+
retention: extract(retention, labels[2]),
|
|
6574
|
+
userSessions: extract(userSessions, labels[3]),
|
|
6575
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6576
|
+
lookbackDays
|
|
6577
|
+
};
|
|
6578
|
+
}
|
|
6579
|
+
var logger2;
|
|
6580
|
+
var init_amplitude_client = __esm({
|
|
6581
|
+
"../core/dist/analytics/amplitude-client.js"() {
|
|
6582
|
+
"use strict";
|
|
6583
|
+
init_logger();
|
|
6584
|
+
logger2 = createLogger("amplitude-client");
|
|
6585
|
+
}
|
|
6586
|
+
});
|
|
6587
|
+
|
|
6588
|
+
// ../core/dist/analytics/analytics-runner.js
|
|
6589
|
+
import * as fs18 from "fs";
|
|
6590
|
+
import * as os7 from "os";
|
|
6591
|
+
import * as path18 from "path";
|
|
6592
|
+
function parseIssuesFromResponse(text) {
|
|
6593
|
+
const start = text.indexOf("[");
|
|
6594
|
+
const end = text.lastIndexOf("]");
|
|
6595
|
+
if (start === -1 || end === -1 || end <= start)
|
|
6596
|
+
return [];
|
|
6597
|
+
try {
|
|
6598
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
6599
|
+
if (!Array.isArray(parsed))
|
|
6600
|
+
return [];
|
|
6601
|
+
return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.title === "string" && typeof item.body === "string");
|
|
6602
|
+
} catch {
|
|
6603
|
+
logger3.warn("Failed to parse AI response as JSON");
|
|
6604
|
+
return [];
|
|
6605
|
+
}
|
|
6606
|
+
}
|
|
6607
|
+
async function runAnalytics(config, projectDir) {
|
|
6608
|
+
const apiKey = config.providerEnv?.AMPLITUDE_API_KEY;
|
|
6609
|
+
const secretKey = config.providerEnv?.AMPLITUDE_SECRET_KEY;
|
|
6610
|
+
if (!apiKey || !secretKey) {
|
|
6611
|
+
throw new Error("AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics");
|
|
6612
|
+
}
|
|
6613
|
+
logger3.info("Fetching Amplitude data", { lookbackDays: config.analytics.lookbackDays });
|
|
6614
|
+
const data = await fetchAmplitudeData(apiKey, secretKey, config.analytics.lookbackDays);
|
|
6615
|
+
const systemPrompt = config.analytics.analysisPrompt?.trim() || DEFAULT_ANALYTICS_PROMPT;
|
|
6616
|
+
const prompt2 = `${systemPrompt}
|
|
6617
|
+
|
|
6618
|
+
--- AMPLITUDE DATA ---
|
|
6619
|
+
${JSON.stringify(data, null, 2)}`;
|
|
6620
|
+
const tmpDir = fs18.mkdtempSync(path18.join(os7.tmpdir(), "nw-analytics-"));
|
|
6621
|
+
const promptFile = path18.join(tmpDir, "analytics-prompt.md");
|
|
6622
|
+
fs18.writeFileSync(promptFile, prompt2, "utf-8");
|
|
6623
|
+
try {
|
|
6624
|
+
const provider = resolveJobProvider(config, "analytics");
|
|
6625
|
+
const providerCmd = PROVIDER_COMMANDS[provider];
|
|
6626
|
+
let scriptContent;
|
|
6627
|
+
if (provider === "claude") {
|
|
6628
|
+
const modelId = CLAUDE_MODEL_IDS[config.claudeModel ?? "sonnet"];
|
|
6629
|
+
scriptContent = `#!/usr/bin/env bash
|
|
6630
|
+
set -euo pipefail
|
|
6631
|
+
${providerCmd} -p "$(cat ${promptFile})" --model ${modelId} --dangerously-skip-permissions 2>&1
|
|
6632
|
+
`;
|
|
6633
|
+
} else {
|
|
6634
|
+
scriptContent = `#!/usr/bin/env bash
|
|
6635
|
+
set -euo pipefail
|
|
6636
|
+
${providerCmd} exec --yolo "$(cat ${promptFile})" 2>&1
|
|
6637
|
+
`;
|
|
6638
|
+
}
|
|
6639
|
+
const scriptFile = path18.join(tmpDir, "run-analytics.sh");
|
|
6640
|
+
fs18.writeFileSync(scriptFile, scriptContent, { mode: 493 });
|
|
6641
|
+
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptFile, [], config.providerEnv ?? {});
|
|
6642
|
+
if (exitCode !== 0) {
|
|
6643
|
+
throw new Error(`AI provider exited with code ${exitCode}: ${stderr || stdout}`);
|
|
6644
|
+
}
|
|
6645
|
+
const fullOutput = `${stdout}
|
|
6646
|
+
${stderr}`;
|
|
6647
|
+
const issues = parseIssuesFromResponse(fullOutput);
|
|
6648
|
+
if (issues.length === 0) {
|
|
6649
|
+
logger3.info("No actionable insights found");
|
|
6650
|
+
return { issuesCreated: 0, summary: "No actionable insights found" };
|
|
6651
|
+
}
|
|
6652
|
+
const boardProvider = createBoardProvider(config.boardProvider, projectDir);
|
|
6653
|
+
const targetColumn = config.analytics.targetColumn;
|
|
6654
|
+
let created = 0;
|
|
6655
|
+
for (const issue of issues) {
|
|
6656
|
+
try {
|
|
6657
|
+
await boardProvider.createIssue({
|
|
6658
|
+
title: issue.title,
|
|
6659
|
+
body: issue.body,
|
|
6660
|
+
column: targetColumn,
|
|
6661
|
+
labels: issue.labels ?? ["analytics"]
|
|
6662
|
+
});
|
|
6663
|
+
created++;
|
|
6664
|
+
logger3.info("Created board issue", { title: issue.title, column: targetColumn });
|
|
6665
|
+
} catch (err) {
|
|
6666
|
+
logger3.error("Failed to create board issue", {
|
|
6667
|
+
title: issue.title,
|
|
6668
|
+
error: String(err)
|
|
6669
|
+
});
|
|
6670
|
+
}
|
|
6671
|
+
}
|
|
6672
|
+
return {
|
|
6673
|
+
issuesCreated: created,
|
|
6674
|
+
summary: `Created ${created} issue(s) from analytics insights`
|
|
6675
|
+
};
|
|
6676
|
+
} finally {
|
|
6677
|
+
try {
|
|
6678
|
+
fs18.rmSync(tmpDir, { recursive: true, force: true });
|
|
6679
|
+
} catch {
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
var logger3;
|
|
6684
|
+
var init_analytics_runner = __esm({
|
|
6685
|
+
"../core/dist/analytics/analytics-runner.js"() {
|
|
6686
|
+
"use strict";
|
|
6687
|
+
init_factory();
|
|
6688
|
+
init_config();
|
|
6689
|
+
init_constants();
|
|
6690
|
+
init_shell();
|
|
6691
|
+
init_logger();
|
|
6692
|
+
init_amplitude_client();
|
|
6693
|
+
logger3 = createLogger("analytics");
|
|
6694
|
+
}
|
|
6695
|
+
});
|
|
6696
|
+
|
|
6697
|
+
// ../core/dist/analytics/index.js
|
|
6698
|
+
var init_analytics = __esm({
|
|
6699
|
+
"../core/dist/analytics/index.js"() {
|
|
6700
|
+
"use strict";
|
|
6701
|
+
init_amplitude_client();
|
|
6702
|
+
init_analytics_runner();
|
|
6703
|
+
}
|
|
6704
|
+
});
|
|
6705
|
+
|
|
6395
6706
|
// ../core/dist/templates/prd-template.js
|
|
6396
6707
|
function renderDependsOn(deps) {
|
|
6397
6708
|
if (deps.length === 0) {
|
|
@@ -6568,6 +6879,7 @@ sequenceDiagram
|
|
|
6568
6879
|
// ../core/dist/index.js
|
|
6569
6880
|
var dist_exports = {};
|
|
6570
6881
|
__export(dist_exports, {
|
|
6882
|
+
ANALYTICS_LOG_NAME: () => ANALYTICS_LOG_NAME,
|
|
6571
6883
|
AUDIT_LOG_NAME: () => AUDIT_LOG_NAME,
|
|
6572
6884
|
BOARD_COLUMNS: () => BOARD_COLUMNS,
|
|
6573
6885
|
BUILT_IN_PRESETS: () => BUILT_IN_PRESETS,
|
|
@@ -6579,6 +6891,13 @@ __export(dist_exports, {
|
|
|
6579
6891
|
CONFIG_FILE_NAME: () => CONFIG_FILE_NAME,
|
|
6580
6892
|
CRONTAB_MARKER_PREFIX: () => CRONTAB_MARKER_PREFIX,
|
|
6581
6893
|
DATABASE_TOKEN: () => DATABASE_TOKEN,
|
|
6894
|
+
DEFAULT_ANALYTICS: () => DEFAULT_ANALYTICS,
|
|
6895
|
+
DEFAULT_ANALYTICS_ENABLED: () => DEFAULT_ANALYTICS_ENABLED,
|
|
6896
|
+
DEFAULT_ANALYTICS_LOOKBACK_DAYS: () => DEFAULT_ANALYTICS_LOOKBACK_DAYS,
|
|
6897
|
+
DEFAULT_ANALYTICS_MAX_RUNTIME: () => DEFAULT_ANALYTICS_MAX_RUNTIME,
|
|
6898
|
+
DEFAULT_ANALYTICS_PROMPT: () => DEFAULT_ANALYTICS_PROMPT,
|
|
6899
|
+
DEFAULT_ANALYTICS_SCHEDULE: () => DEFAULT_ANALYTICS_SCHEDULE,
|
|
6900
|
+
DEFAULT_ANALYTICS_TARGET_COLUMN: () => DEFAULT_ANALYTICS_TARGET_COLUMN,
|
|
6582
6901
|
DEFAULT_AUDIT: () => DEFAULT_AUDIT,
|
|
6583
6902
|
DEFAULT_AUDIT_ENABLED: () => DEFAULT_AUDIT_ENABLED,
|
|
6584
6903
|
DEFAULT_AUDIT_MAX_RUNTIME: () => DEFAULT_AUDIT_MAX_RUNTIME,
|
|
@@ -6620,6 +6939,7 @@ __export(dist_exports, {
|
|
|
6620
6939
|
DEFAULT_QUEUE_MODE: () => DEFAULT_QUEUE_MODE,
|
|
6621
6940
|
DEFAULT_QUEUE_PRIORITY: () => DEFAULT_QUEUE_PRIORITY,
|
|
6622
6941
|
DEFAULT_REVIEWER_ENABLED: () => DEFAULT_REVIEWER_ENABLED,
|
|
6942
|
+
DEFAULT_REVIEWER_MAX_PRS_PER_RUN: () => DEFAULT_REVIEWER_MAX_PRS_PER_RUN,
|
|
6623
6943
|
DEFAULT_REVIEWER_MAX_RETRIES: () => DEFAULT_REVIEWER_MAX_RETRIES,
|
|
6624
6944
|
DEFAULT_REVIEWER_MAX_RUNTIME: () => DEFAULT_REVIEWER_MAX_RUNTIME,
|
|
6625
6945
|
DEFAULT_REVIEWER_RETRY_DELAY: () => DEFAULT_REVIEWER_RETRY_DELAY,
|
|
@@ -6664,6 +6984,7 @@ __export(dist_exports, {
|
|
|
6664
6984
|
acquireLock: () => acquireLock,
|
|
6665
6985
|
addDelayToIsoString: () => addDelayToIsoString,
|
|
6666
6986
|
addEntry: () => addEntry,
|
|
6987
|
+
analyticsLockPath: () => analyticsLockPath,
|
|
6667
6988
|
auditLockPath: () => auditLockPath,
|
|
6668
6989
|
buildDescription: () => buildDescription,
|
|
6669
6990
|
calculateStringSimilarity: () => calculateStringSimilarity,
|
|
@@ -6714,6 +7035,7 @@ __export(dist_exports, {
|
|
|
6714
7035
|
extractPriority: () => extractPriority,
|
|
6715
7036
|
extractQaScreenshotUrls: () => extractQaScreenshotUrls,
|
|
6716
7037
|
extractSummary: () => extractSummary,
|
|
7038
|
+
fetchAmplitudeData: () => fetchAmplitudeData,
|
|
6717
7039
|
fetchLatestQaCommentBody: () => fetchLatestQaCommentBody,
|
|
6718
7040
|
fetchPrDetails: () => fetchPrDetails,
|
|
6719
7041
|
fetchPrDetailsByNumber: () => fetchPrDetailsByNumber,
|
|
@@ -6821,6 +7143,7 @@ __export(dist_exports, {
|
|
|
6821
7143
|
reviewerLockPath: () => reviewerLockPath,
|
|
6822
7144
|
rotateLog: () => rotateLog,
|
|
6823
7145
|
runAllChecks: () => runAllChecks,
|
|
7146
|
+
runAnalytics: () => runAnalytics,
|
|
6824
7147
|
runMigrations: () => runMigrations,
|
|
6825
7148
|
saveConfig: () => saveConfig,
|
|
6826
7149
|
saveHistory: () => saveHistory,
|
|
@@ -6889,6 +7212,7 @@ var init_dist = __esm({
|
|
|
6889
7212
|
init_webhook_validator();
|
|
6890
7213
|
init_worktree_manager();
|
|
6891
7214
|
init_job_queue();
|
|
7215
|
+
init_analytics();
|
|
6892
7216
|
init_prd_template();
|
|
6893
7217
|
init_slicer_prompt();
|
|
6894
7218
|
}
|
|
@@ -6899,37 +7223,37 @@ import "reflect-metadata";
|
|
|
6899
7223
|
import { Command as Command3 } from "commander";
|
|
6900
7224
|
import { existsSync as existsSync29, readFileSync as readFileSync18 } from "fs";
|
|
6901
7225
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
6902
|
-
import { dirname as dirname8, join as
|
|
7226
|
+
import { dirname as dirname8, join as join35 } from "path";
|
|
6903
7227
|
|
|
6904
7228
|
// src/commands/init.ts
|
|
6905
7229
|
init_dist();
|
|
6906
|
-
import
|
|
6907
|
-
import
|
|
7230
|
+
import fs19 from "fs";
|
|
7231
|
+
import path19 from "path";
|
|
6908
7232
|
import { execSync as execSync3 } from "child_process";
|
|
6909
7233
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6910
|
-
import { dirname as dirname4, join as
|
|
7234
|
+
import { dirname as dirname4, join as join17 } from "path";
|
|
6911
7235
|
import * as readline from "readline";
|
|
6912
7236
|
var __filename = fileURLToPath2(import.meta.url);
|
|
6913
7237
|
var __dirname2 = dirname4(__filename);
|
|
6914
7238
|
function findTemplatesDir(startDir) {
|
|
6915
7239
|
let d = startDir;
|
|
6916
7240
|
for (let i = 0; i < 8; i++) {
|
|
6917
|
-
const candidate =
|
|
6918
|
-
if (
|
|
7241
|
+
const candidate = join17(d, "templates");
|
|
7242
|
+
if (fs19.existsSync(candidate) && fs19.statSync(candidate).isDirectory()) {
|
|
6919
7243
|
return candidate;
|
|
6920
7244
|
}
|
|
6921
7245
|
d = dirname4(d);
|
|
6922
7246
|
}
|
|
6923
|
-
return
|
|
7247
|
+
return join17(startDir, "templates");
|
|
6924
7248
|
}
|
|
6925
7249
|
var TEMPLATES_DIR = findTemplatesDir(__dirname2);
|
|
6926
7250
|
function hasPlaywrightDependency(cwd) {
|
|
6927
|
-
const packageJsonPath =
|
|
6928
|
-
if (!
|
|
7251
|
+
const packageJsonPath = path19.join(cwd, "package.json");
|
|
7252
|
+
if (!fs19.existsSync(packageJsonPath)) {
|
|
6929
7253
|
return false;
|
|
6930
7254
|
}
|
|
6931
7255
|
try {
|
|
6932
|
-
const packageJson2 = JSON.parse(
|
|
7256
|
+
const packageJson2 = JSON.parse(fs19.readFileSync(packageJsonPath, "utf-8"));
|
|
6933
7257
|
return Boolean(
|
|
6934
7258
|
packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright
|
|
6935
7259
|
);
|
|
@@ -6941,7 +7265,7 @@ function detectPlaywright(cwd) {
|
|
|
6941
7265
|
if (hasPlaywrightDependency(cwd)) {
|
|
6942
7266
|
return true;
|
|
6943
7267
|
}
|
|
6944
|
-
if (
|
|
7268
|
+
if (fs19.existsSync(path19.join(cwd, "node_modules", ".bin", "playwright"))) {
|
|
6945
7269
|
return true;
|
|
6946
7270
|
}
|
|
6947
7271
|
try {
|
|
@@ -6957,10 +7281,10 @@ function detectPlaywright(cwd) {
|
|
|
6957
7281
|
}
|
|
6958
7282
|
}
|
|
6959
7283
|
function resolvePlaywrightInstallCommand(cwd) {
|
|
6960
|
-
if (
|
|
7284
|
+
if (fs19.existsSync(path19.join(cwd, "pnpm-lock.yaml"))) {
|
|
6961
7285
|
return "pnpm add -D @playwright/test";
|
|
6962
7286
|
}
|
|
6963
|
-
if (
|
|
7287
|
+
if (fs19.existsSync(path19.join(cwd, "yarn.lock"))) {
|
|
6964
7288
|
return "yarn add -D @playwright/test";
|
|
6965
7289
|
}
|
|
6966
7290
|
return "npm install -D @playwright/test";
|
|
@@ -7106,8 +7430,8 @@ function promptProviderSelection(providers) {
|
|
|
7106
7430
|
});
|
|
7107
7431
|
}
|
|
7108
7432
|
function ensureDir(dirPath) {
|
|
7109
|
-
if (!
|
|
7110
|
-
|
|
7433
|
+
if (!fs19.existsSync(dirPath)) {
|
|
7434
|
+
fs19.mkdirSync(dirPath, { recursive: true });
|
|
7111
7435
|
}
|
|
7112
7436
|
}
|
|
7113
7437
|
function buildInitConfig(params) {
|
|
@@ -7130,6 +7454,7 @@ function buildInitConfig(params) {
|
|
|
7130
7454
|
schedulingPriority: defaults.schedulingPriority,
|
|
7131
7455
|
maxRetries: defaults.maxRetries,
|
|
7132
7456
|
reviewerMaxRetries: defaults.reviewerMaxRetries,
|
|
7457
|
+
reviewerMaxPrsPerRun: defaults.reviewerMaxPrsPerRun,
|
|
7133
7458
|
reviewerRetryDelay: defaults.reviewerRetryDelay,
|
|
7134
7459
|
provider: params.provider,
|
|
7135
7460
|
providerLabel: "",
|
|
@@ -7153,6 +7478,7 @@ function buildInitConfig(params) {
|
|
|
7153
7478
|
branchPatterns: [...defaults.qa.branchPatterns]
|
|
7154
7479
|
},
|
|
7155
7480
|
audit: { ...defaults.audit },
|
|
7481
|
+
analytics: { ...defaults.analytics },
|
|
7156
7482
|
jobProviders: { ...defaults.jobProviders },
|
|
7157
7483
|
queue: {
|
|
7158
7484
|
...defaults.queue,
|
|
@@ -7162,30 +7488,30 @@ function buildInitConfig(params) {
|
|
|
7162
7488
|
}
|
|
7163
7489
|
function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
|
|
7164
7490
|
if (customTemplatesDir !== null) {
|
|
7165
|
-
const customPath =
|
|
7166
|
-
if (
|
|
7491
|
+
const customPath = join17(customTemplatesDir, templateName);
|
|
7492
|
+
if (fs19.existsSync(customPath)) {
|
|
7167
7493
|
return { path: customPath, source: "custom" };
|
|
7168
7494
|
}
|
|
7169
7495
|
}
|
|
7170
|
-
return { path:
|
|
7496
|
+
return { path: join17(bundledTemplatesDir, templateName), source: "bundled" };
|
|
7171
7497
|
}
|
|
7172
7498
|
function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
|
|
7173
|
-
if (
|
|
7499
|
+
if (fs19.existsSync(targetPath) && !force) {
|
|
7174
7500
|
console.log(` Skipped (exists): ${targetPath}`);
|
|
7175
7501
|
return { created: false, source: source ?? "bundled" };
|
|
7176
7502
|
}
|
|
7177
|
-
const templatePath = sourcePath ??
|
|
7503
|
+
const templatePath = sourcePath ?? join17(TEMPLATES_DIR, templateName);
|
|
7178
7504
|
const resolvedSource = source ?? "bundled";
|
|
7179
|
-
let content =
|
|
7505
|
+
let content = fs19.readFileSync(templatePath, "utf-8");
|
|
7180
7506
|
for (const [key, value] of Object.entries(replacements)) {
|
|
7181
7507
|
content = content.replaceAll(key, value);
|
|
7182
7508
|
}
|
|
7183
|
-
|
|
7509
|
+
fs19.writeFileSync(targetPath, content);
|
|
7184
7510
|
console.log(` Created: ${targetPath} (${resolvedSource})`);
|
|
7185
7511
|
return { created: true, source: resolvedSource };
|
|
7186
7512
|
}
|
|
7187
7513
|
function addToGitignore(cwd) {
|
|
7188
|
-
const gitignorePath =
|
|
7514
|
+
const gitignorePath = path19.join(cwd, ".gitignore");
|
|
7189
7515
|
const entries = [
|
|
7190
7516
|
{
|
|
7191
7517
|
pattern: "/logs/",
|
|
@@ -7199,13 +7525,13 @@ function addToGitignore(cwd) {
|
|
|
7199
7525
|
},
|
|
7200
7526
|
{ pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
|
|
7201
7527
|
];
|
|
7202
|
-
if (!
|
|
7528
|
+
if (!fs19.existsSync(gitignorePath)) {
|
|
7203
7529
|
const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
|
|
7204
|
-
|
|
7530
|
+
fs19.writeFileSync(gitignorePath, lines.join("\n"));
|
|
7205
7531
|
console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
|
|
7206
7532
|
return;
|
|
7207
7533
|
}
|
|
7208
|
-
const content =
|
|
7534
|
+
const content = fs19.readFileSync(gitignorePath, "utf-8");
|
|
7209
7535
|
const missing = entries.filter((e) => !e.check(content));
|
|
7210
7536
|
if (missing.length === 0) {
|
|
7211
7537
|
console.log(` Skipped (exists): Night Watch entries in .gitignore`);
|
|
@@ -7213,7 +7539,7 @@ function addToGitignore(cwd) {
|
|
|
7213
7539
|
}
|
|
7214
7540
|
const additions = missing.map((e) => e.pattern).join("\n");
|
|
7215
7541
|
const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
|
|
7216
|
-
|
|
7542
|
+
fs19.writeFileSync(gitignorePath, newContent);
|
|
7217
7543
|
console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
|
|
7218
7544
|
}
|
|
7219
7545
|
function initCommand(program2) {
|
|
@@ -7334,28 +7660,28 @@ function initCommand(program2) {
|
|
|
7334
7660
|
"${DEFAULT_BRANCH}": defaultBranch
|
|
7335
7661
|
};
|
|
7336
7662
|
step(6, totalSteps, "Creating PRD directory structure...");
|
|
7337
|
-
const prdDirPath =
|
|
7338
|
-
const doneDirPath =
|
|
7663
|
+
const prdDirPath = path19.join(cwd, prdDir);
|
|
7664
|
+
const doneDirPath = path19.join(prdDirPath, "done");
|
|
7339
7665
|
ensureDir(doneDirPath);
|
|
7340
7666
|
success(`Created ${prdDirPath}/`);
|
|
7341
7667
|
success(`Created ${doneDirPath}/`);
|
|
7342
7668
|
step(7, totalSteps, "Creating logs directory...");
|
|
7343
|
-
const logsPath =
|
|
7669
|
+
const logsPath = path19.join(cwd, LOG_DIR);
|
|
7344
7670
|
ensureDir(logsPath);
|
|
7345
7671
|
success(`Created ${logsPath}/`);
|
|
7346
7672
|
addToGitignore(cwd);
|
|
7347
7673
|
step(8, totalSteps, "Creating instructions directory...");
|
|
7348
|
-
const instructionsDir =
|
|
7674
|
+
const instructionsDir = path19.join(cwd, "instructions");
|
|
7349
7675
|
ensureDir(instructionsDir);
|
|
7350
7676
|
success(`Created ${instructionsDir}/`);
|
|
7351
7677
|
const existingConfig = loadConfig(cwd);
|
|
7352
|
-
const customTemplatesDirPath =
|
|
7353
|
-
const customTemplatesDir =
|
|
7678
|
+
const customTemplatesDirPath = path19.join(cwd, existingConfig.templatesDir);
|
|
7679
|
+
const customTemplatesDir = fs19.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
|
|
7354
7680
|
const templateSources = [];
|
|
7355
7681
|
const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7356
7682
|
const nwResult = processTemplate(
|
|
7357
7683
|
"executor.md",
|
|
7358
|
-
|
|
7684
|
+
path19.join(instructionsDir, "executor.md"),
|
|
7359
7685
|
replacements,
|
|
7360
7686
|
force,
|
|
7361
7687
|
nwResolution.path,
|
|
@@ -7369,7 +7695,7 @@ function initCommand(program2) {
|
|
|
7369
7695
|
);
|
|
7370
7696
|
const peResult = processTemplate(
|
|
7371
7697
|
"prd-executor.md",
|
|
7372
|
-
|
|
7698
|
+
path19.join(instructionsDir, "prd-executor.md"),
|
|
7373
7699
|
replacements,
|
|
7374
7700
|
force,
|
|
7375
7701
|
peResolution.path,
|
|
@@ -7379,7 +7705,7 @@ function initCommand(program2) {
|
|
|
7379
7705
|
const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7380
7706
|
const prResult = processTemplate(
|
|
7381
7707
|
"pr-reviewer.md",
|
|
7382
|
-
|
|
7708
|
+
path19.join(instructionsDir, "pr-reviewer.md"),
|
|
7383
7709
|
replacements,
|
|
7384
7710
|
force,
|
|
7385
7711
|
prResolution.path,
|
|
@@ -7389,7 +7715,7 @@ function initCommand(program2) {
|
|
|
7389
7715
|
const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7390
7716
|
const qaResult = processTemplate(
|
|
7391
7717
|
"qa.md",
|
|
7392
|
-
|
|
7718
|
+
path19.join(instructionsDir, "qa.md"),
|
|
7393
7719
|
replacements,
|
|
7394
7720
|
force,
|
|
7395
7721
|
qaResolution.path,
|
|
@@ -7399,7 +7725,7 @@ function initCommand(program2) {
|
|
|
7399
7725
|
const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7400
7726
|
const auditResult = processTemplate(
|
|
7401
7727
|
"audit.md",
|
|
7402
|
-
|
|
7728
|
+
path19.join(instructionsDir, "audit.md"),
|
|
7403
7729
|
replacements,
|
|
7404
7730
|
force,
|
|
7405
7731
|
auditResolution.path,
|
|
@@ -7407,8 +7733,8 @@ function initCommand(program2) {
|
|
|
7407
7733
|
);
|
|
7408
7734
|
templateSources.push({ name: "audit.md", source: auditResult.source });
|
|
7409
7735
|
step(9, totalSteps, "Creating configuration file...");
|
|
7410
|
-
const configPath =
|
|
7411
|
-
if (
|
|
7736
|
+
const configPath = path19.join(cwd, CONFIG_FILE_NAME);
|
|
7737
|
+
if (fs19.existsSync(configPath) && !force) {
|
|
7412
7738
|
console.log(` Skipped (exists): ${configPath}`);
|
|
7413
7739
|
} else {
|
|
7414
7740
|
const config = buildInitConfig({
|
|
@@ -7418,11 +7744,11 @@ function initCommand(program2) {
|
|
|
7418
7744
|
reviewerEnabled,
|
|
7419
7745
|
prdDir
|
|
7420
7746
|
});
|
|
7421
|
-
|
|
7747
|
+
fs19.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
7422
7748
|
success(`Created ${configPath}`);
|
|
7423
7749
|
}
|
|
7424
7750
|
step(10, totalSteps, "Setting up GitHub Project board...");
|
|
7425
|
-
const existingRaw = JSON.parse(
|
|
7751
|
+
const existingRaw = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
|
|
7426
7752
|
const existingBoard = existingRaw.boardProvider;
|
|
7427
7753
|
let boardSetupStatus = "Skipped";
|
|
7428
7754
|
if (existingBoard?.projectNumber && !force) {
|
|
@@ -7436,19 +7762,21 @@ function initCommand(program2) {
|
|
|
7436
7762
|
);
|
|
7437
7763
|
} else if (!ghAuthenticated) {
|
|
7438
7764
|
boardSetupStatus = "Skipped (gh auth required)";
|
|
7439
|
-
info(
|
|
7765
|
+
info(
|
|
7766
|
+
"GitHub CLI is not authenticated \u2014 run `gh auth login`, then `night-watch board setup`."
|
|
7767
|
+
);
|
|
7440
7768
|
} else {
|
|
7441
7769
|
try {
|
|
7442
7770
|
const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
|
|
7443
7771
|
const boardTitle = `${projectName} Night Watch`;
|
|
7444
7772
|
const board = await provider.setupBoard(boardTitle);
|
|
7445
|
-
const rawConfig = JSON.parse(
|
|
7773
|
+
const rawConfig = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
|
|
7446
7774
|
rawConfig.boardProvider = {
|
|
7447
7775
|
enabled: true,
|
|
7448
7776
|
provider: "github",
|
|
7449
7777
|
projectNumber: board.number
|
|
7450
7778
|
};
|
|
7451
|
-
|
|
7779
|
+
fs19.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
|
|
7452
7780
|
boardSetupStatus = `Created (#${board.number})`;
|
|
7453
7781
|
success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
|
|
7454
7782
|
} catch (boardErr) {
|
|
@@ -7572,8 +7900,8 @@ function getTelegramStatusWebhooks(config) {
|
|
|
7572
7900
|
}
|
|
7573
7901
|
|
|
7574
7902
|
// src/commands/run.ts
|
|
7575
|
-
import * as
|
|
7576
|
-
import * as
|
|
7903
|
+
import * as fs20 from "fs";
|
|
7904
|
+
import * as path20 from "path";
|
|
7577
7905
|
function resolveRunNotificationEvent(exitCode, scriptStatus) {
|
|
7578
7906
|
if (exitCode === 124) {
|
|
7579
7907
|
return "run_timeout";
|
|
@@ -7605,12 +7933,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
|
|
|
7605
7933
|
return scriptStatus === "skip_no_eligible_prd";
|
|
7606
7934
|
}
|
|
7607
7935
|
function getCrossProjectFallbackCandidates(currentProjectDir) {
|
|
7608
|
-
const current =
|
|
7936
|
+
const current = path20.resolve(currentProjectDir);
|
|
7609
7937
|
const { valid, invalid } = validateRegistry();
|
|
7610
7938
|
for (const entry of invalid) {
|
|
7611
7939
|
warn(`Skipping invalid registry entry: ${entry.path}`);
|
|
7612
7940
|
}
|
|
7613
|
-
return valid.filter((entry) =>
|
|
7941
|
+
return valid.filter((entry) => path20.resolve(entry.path) !== current);
|
|
7614
7942
|
}
|
|
7615
7943
|
async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
|
|
7616
7944
|
if (isRateLimitFallbackTriggered(scriptResult?.data)) {
|
|
@@ -7620,7 +7948,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
|
|
|
7620
7948
|
if (nonTelegramWebhooks.length > 0) {
|
|
7621
7949
|
const _rateLimitCtx = {
|
|
7622
7950
|
event: "rate_limit_fallback",
|
|
7623
|
-
projectName:
|
|
7951
|
+
projectName: path20.basename(projectDir),
|
|
7624
7952
|
exitCode,
|
|
7625
7953
|
provider: config.provider
|
|
7626
7954
|
};
|
|
@@ -7648,7 +7976,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
|
|
|
7648
7976
|
const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
|
|
7649
7977
|
const _ctx = {
|
|
7650
7978
|
event,
|
|
7651
|
-
projectName:
|
|
7979
|
+
projectName: path20.basename(projectDir),
|
|
7652
7980
|
exitCode,
|
|
7653
7981
|
provider: config.provider,
|
|
7654
7982
|
prdName: scriptResult?.data.prd,
|
|
@@ -7810,20 +8138,20 @@ function applyCliOverrides(config, options) {
|
|
|
7810
8138
|
return overridden;
|
|
7811
8139
|
}
|
|
7812
8140
|
function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
|
|
7813
|
-
const absolutePrdDir =
|
|
7814
|
-
const doneDir =
|
|
8141
|
+
const absolutePrdDir = path20.join(projectDir, prdDir);
|
|
8142
|
+
const doneDir = path20.join(absolutePrdDir, "done");
|
|
7815
8143
|
const pending = [];
|
|
7816
8144
|
const completed = [];
|
|
7817
|
-
if (
|
|
7818
|
-
const entries =
|
|
8145
|
+
if (fs20.existsSync(absolutePrdDir)) {
|
|
8146
|
+
const entries = fs20.readdirSync(absolutePrdDir, { withFileTypes: true });
|
|
7819
8147
|
for (const entry of entries) {
|
|
7820
8148
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7821
|
-
const claimPath =
|
|
8149
|
+
const claimPath = path20.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
|
|
7822
8150
|
let claimed = false;
|
|
7823
8151
|
let claimInfo = null;
|
|
7824
|
-
if (
|
|
8152
|
+
if (fs20.existsSync(claimPath)) {
|
|
7825
8153
|
try {
|
|
7826
|
-
const content =
|
|
8154
|
+
const content = fs20.readFileSync(claimPath, "utf-8");
|
|
7827
8155
|
const data = JSON.parse(content);
|
|
7828
8156
|
const age = Math.floor(Date.now() / 1e3) - data.timestamp;
|
|
7829
8157
|
if (age < maxRuntime) {
|
|
@@ -7837,8 +8165,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
|
|
|
7837
8165
|
}
|
|
7838
8166
|
}
|
|
7839
8167
|
}
|
|
7840
|
-
if (
|
|
7841
|
-
const entries =
|
|
8168
|
+
if (fs20.existsSync(doneDir)) {
|
|
8169
|
+
const entries = fs20.readdirSync(doneDir, { withFileTypes: true });
|
|
7842
8170
|
for (const entry of entries) {
|
|
7843
8171
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7844
8172
|
completed.push(entry.name);
|
|
@@ -7998,7 +8326,7 @@ ${stderr}`);
|
|
|
7998
8326
|
// src/commands/review.ts
|
|
7999
8327
|
init_dist();
|
|
8000
8328
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
8001
|
-
import * as
|
|
8329
|
+
import * as path21 from "path";
|
|
8002
8330
|
function shouldSendReviewNotification(scriptStatus) {
|
|
8003
8331
|
if (!scriptStatus) {
|
|
8004
8332
|
return true;
|
|
@@ -8033,6 +8361,7 @@ function buildEnvVars2(config, options) {
|
|
|
8033
8361
|
env.NW_REVIEWER_MAX_RUNTIME = String(config.reviewerMaxRuntime);
|
|
8034
8362
|
env.NW_REVIEWER_MAX_RETRIES = String(config.reviewerMaxRetries);
|
|
8035
8363
|
env.NW_REVIEWER_RETRY_DELAY = String(config.reviewerRetryDelay);
|
|
8364
|
+
env.NW_REVIEWER_MAX_PRS_PER_RUN = String(config.reviewerMaxPrsPerRun);
|
|
8036
8365
|
env.NW_MIN_REVIEW_SCORE = String(config.minReviewScore);
|
|
8037
8366
|
env.NW_BRANCH_PATTERNS = config.branchPatterns.join(",");
|
|
8038
8367
|
env.NW_PRD_DIR = config.prdDir;
|
|
@@ -8146,6 +8475,10 @@ function reviewCommand(program2) {
|
|
|
8146
8475
|
]);
|
|
8147
8476
|
configTable.push(["Max Retry Attempts", String(config.reviewerMaxRetries)]);
|
|
8148
8477
|
configTable.push(["Retry Delay", `${config.reviewerRetryDelay}s`]);
|
|
8478
|
+
configTable.push([
|
|
8479
|
+
"Max PRs Per Run",
|
|
8480
|
+
config.reviewerMaxPrsPerRun === 0 ? "Unlimited" : String(config.reviewerMaxPrsPerRun)
|
|
8481
|
+
]);
|
|
8149
8482
|
console.log(configTable.toString());
|
|
8150
8483
|
header("Open PRs Needing Work");
|
|
8151
8484
|
const openPrs = getOpenPrsNeedingWork(config.branchPatterns);
|
|
@@ -8233,7 +8566,7 @@ ${stderr}`);
|
|
|
8233
8566
|
const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
|
|
8234
8567
|
const _reviewCtx = {
|
|
8235
8568
|
event: "review_completed",
|
|
8236
|
-
projectName:
|
|
8569
|
+
projectName: path21.basename(projectDir),
|
|
8237
8570
|
exitCode,
|
|
8238
8571
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
8239
8572
|
prUrl: prDetails?.url,
|
|
@@ -8254,7 +8587,7 @@ ${stderr}`);
|
|
|
8254
8587
|
const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
|
|
8255
8588
|
const _mergeCtx = {
|
|
8256
8589
|
event: "pr_auto_merged",
|
|
8257
|
-
projectName:
|
|
8590
|
+
projectName: path21.basename(projectDir),
|
|
8258
8591
|
exitCode,
|
|
8259
8592
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
8260
8593
|
prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
|
|
@@ -8279,7 +8612,7 @@ ${stderr}`);
|
|
|
8279
8612
|
|
|
8280
8613
|
// src/commands/qa.ts
|
|
8281
8614
|
init_dist();
|
|
8282
|
-
import * as
|
|
8615
|
+
import * as path22 from "path";
|
|
8283
8616
|
function shouldSendQaNotification(scriptStatus) {
|
|
8284
8617
|
if (!scriptStatus) {
|
|
8285
8618
|
return true;
|
|
@@ -8413,7 +8746,7 @@ ${stderr}`);
|
|
|
8413
8746
|
const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
|
|
8414
8747
|
const _qaCtx = {
|
|
8415
8748
|
event: "qa_completed",
|
|
8416
|
-
projectName:
|
|
8749
|
+
projectName: path22.basename(projectDir),
|
|
8417
8750
|
exitCode,
|
|
8418
8751
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
8419
8752
|
prNumber: prDetails?.number ?? primaryQaPr,
|
|
@@ -8439,8 +8772,8 @@ ${stderr}`);
|
|
|
8439
8772
|
|
|
8440
8773
|
// src/commands/audit.ts
|
|
8441
8774
|
init_dist();
|
|
8442
|
-
import * as
|
|
8443
|
-
import * as
|
|
8775
|
+
import * as fs21 from "fs";
|
|
8776
|
+
import * as path23 from "path";
|
|
8444
8777
|
function buildEnvVars4(config, options) {
|
|
8445
8778
|
const env = buildBaseEnvVars(config, "audit", options.dryRun);
|
|
8446
8779
|
env.NW_AUDIT_MAX_RUNTIME = String(config.audit.maxRuntime);
|
|
@@ -8483,7 +8816,7 @@ function auditCommand(program2) {
|
|
|
8483
8816
|
configTable.push(["Provider", auditProvider]);
|
|
8484
8817
|
configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
|
|
8485
8818
|
configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
|
|
8486
|
-
configTable.push(["Report File",
|
|
8819
|
+
configTable.push(["Report File", path23.join(projectDir, "logs", "audit-report.md")]);
|
|
8487
8820
|
console.log(configTable.toString());
|
|
8488
8821
|
header("Provider Invocation");
|
|
8489
8822
|
const providerCmd = PROVIDER_COMMANDS[auditProvider];
|
|
@@ -8518,8 +8851,8 @@ ${stderr}`);
|
|
|
8518
8851
|
} else if (scriptResult?.status?.startsWith("skip_")) {
|
|
8519
8852
|
spinner.succeed("Code audit skipped");
|
|
8520
8853
|
} else {
|
|
8521
|
-
const reportPath =
|
|
8522
|
-
if (!
|
|
8854
|
+
const reportPath = path23.join(projectDir, "logs", "audit-report.md");
|
|
8855
|
+
if (!fs21.existsSync(reportPath)) {
|
|
8523
8856
|
spinner.fail("Code audit finished without a report file");
|
|
8524
8857
|
process.exit(1);
|
|
8525
8858
|
}
|
|
@@ -8530,9 +8863,9 @@ ${stderr}`);
|
|
|
8530
8863
|
const providerExit = scriptResult?.data?.provider_exit;
|
|
8531
8864
|
const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
|
|
8532
8865
|
spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
|
|
8533
|
-
const logPath =
|
|
8534
|
-
if (
|
|
8535
|
-
const logLines =
|
|
8866
|
+
const logPath = path23.join(projectDir, "logs", "audit.log");
|
|
8867
|
+
if (fs21.existsSync(logPath)) {
|
|
8868
|
+
const logLines = fs21.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
|
|
8536
8869
|
if (logLines.length > 0) {
|
|
8537
8870
|
process.stderr.write(logLines.join("\n") + "\n");
|
|
8538
8871
|
}
|
|
@@ -8546,19 +8879,80 @@ ${stderr}`);
|
|
|
8546
8879
|
});
|
|
8547
8880
|
}
|
|
8548
8881
|
|
|
8882
|
+
// src/commands/analytics.ts
|
|
8883
|
+
init_dist();
|
|
8884
|
+
function analyticsCommand(program2) {
|
|
8885
|
+
program2.command("analytics").description("Run Amplitude analytics job 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)").action(async (options) => {
|
|
8886
|
+
const projectDir = process.cwd();
|
|
8887
|
+
let config = loadConfig(projectDir);
|
|
8888
|
+
if (options.timeout) {
|
|
8889
|
+
const timeout = parseInt(options.timeout, 10);
|
|
8890
|
+
if (!isNaN(timeout)) {
|
|
8891
|
+
config = { ...config, analytics: { ...config.analytics, maxRuntime: timeout } };
|
|
8892
|
+
}
|
|
8893
|
+
}
|
|
8894
|
+
if (options.provider) {
|
|
8895
|
+
config = {
|
|
8896
|
+
...config,
|
|
8897
|
+
_cliProviderOverride: options.provider
|
|
8898
|
+
};
|
|
8899
|
+
}
|
|
8900
|
+
if (!config.analytics.enabled && !options.dryRun) {
|
|
8901
|
+
info("Analytics is disabled in config; skipping run.");
|
|
8902
|
+
process.exit(0);
|
|
8903
|
+
}
|
|
8904
|
+
const apiKey = config.providerEnv?.AMPLITUDE_API_KEY;
|
|
8905
|
+
const secretKey = config.providerEnv?.AMPLITUDE_SECRET_KEY;
|
|
8906
|
+
if (!apiKey || !secretKey) {
|
|
8907
|
+
info(
|
|
8908
|
+
"AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics."
|
|
8909
|
+
);
|
|
8910
|
+
process.exit(1);
|
|
8911
|
+
}
|
|
8912
|
+
if (options.dryRun) {
|
|
8913
|
+
header("Dry Run: Analytics Job");
|
|
8914
|
+
const analyticsProvider = resolveJobProvider(config, "analytics");
|
|
8915
|
+
header("Configuration");
|
|
8916
|
+
const configTable = createTable({ head: ["Setting", "Value"] });
|
|
8917
|
+
configTable.push(["Provider", analyticsProvider]);
|
|
8918
|
+
configTable.push(["Max Runtime", `${config.analytics.maxRuntime}s`]);
|
|
8919
|
+
configTable.push(["Lookback Days", String(config.analytics.lookbackDays)]);
|
|
8920
|
+
configTable.push(["Target Column", config.analytics.targetColumn]);
|
|
8921
|
+
configTable.push(["Amplitude API Key", apiKey ? "***" + apiKey.slice(-4) : "not set"]);
|
|
8922
|
+
console.log(configTable.toString());
|
|
8923
|
+
console.log();
|
|
8924
|
+
process.exit(0);
|
|
8925
|
+
}
|
|
8926
|
+
const spinner = createSpinner("Running analytics job...");
|
|
8927
|
+
spinner.start();
|
|
8928
|
+
try {
|
|
8929
|
+
await maybeApplyCronSchedulingDelay(config, "analytics", projectDir);
|
|
8930
|
+
const result = await runAnalytics(config, projectDir);
|
|
8931
|
+
if (result.issuesCreated > 0) {
|
|
8932
|
+
spinner.succeed(`Analytics complete \u2014 ${result.summary}`);
|
|
8933
|
+
} else {
|
|
8934
|
+
spinner.succeed("Analytics complete \u2014 no actionable insights found");
|
|
8935
|
+
}
|
|
8936
|
+
} catch (err) {
|
|
8937
|
+
spinner.fail(`Analytics failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8938
|
+
process.exit(1);
|
|
8939
|
+
}
|
|
8940
|
+
});
|
|
8941
|
+
}
|
|
8942
|
+
|
|
8549
8943
|
// src/commands/install.ts
|
|
8550
8944
|
init_dist();
|
|
8551
8945
|
import { execSync as execSync4 } from "child_process";
|
|
8552
|
-
import * as
|
|
8553
|
-
import * as
|
|
8946
|
+
import * as path24 from "path";
|
|
8947
|
+
import * as fs22 from "fs";
|
|
8554
8948
|
function shellQuote(value) {
|
|
8555
8949
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
8556
8950
|
}
|
|
8557
8951
|
function getNightWatchBinPath() {
|
|
8558
8952
|
try {
|
|
8559
8953
|
const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
|
|
8560
|
-
const binPath =
|
|
8561
|
-
if (
|
|
8954
|
+
const binPath = path24.join(npmBin, "night-watch");
|
|
8955
|
+
if (fs22.existsSync(binPath)) {
|
|
8562
8956
|
return binPath;
|
|
8563
8957
|
}
|
|
8564
8958
|
} catch {
|
|
@@ -8571,17 +8965,17 @@ function getNightWatchBinPath() {
|
|
|
8571
8965
|
}
|
|
8572
8966
|
function getNodeBinDir() {
|
|
8573
8967
|
if (process.execPath && process.execPath !== "node") {
|
|
8574
|
-
return
|
|
8968
|
+
return path24.dirname(process.execPath);
|
|
8575
8969
|
}
|
|
8576
8970
|
try {
|
|
8577
8971
|
const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
|
|
8578
|
-
return
|
|
8972
|
+
return path24.dirname(nodePath);
|
|
8579
8973
|
} catch {
|
|
8580
8974
|
return "";
|
|
8581
8975
|
}
|
|
8582
8976
|
}
|
|
8583
8977
|
function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
|
|
8584
|
-
const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ?
|
|
8978
|
+
const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path24.dirname(nightWatchBin) : "";
|
|
8585
8979
|
const pathParts = Array.from(
|
|
8586
8980
|
new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0))
|
|
8587
8981
|
);
|
|
@@ -8597,12 +8991,12 @@ function performInstall(projectDir, config, options) {
|
|
|
8597
8991
|
const nightWatchBin = getNightWatchBinPath();
|
|
8598
8992
|
const projectName = getProjectName(projectDir);
|
|
8599
8993
|
const marker = generateMarker(projectName);
|
|
8600
|
-
const logDir =
|
|
8601
|
-
if (!
|
|
8602
|
-
|
|
8994
|
+
const logDir = path24.join(projectDir, LOG_DIR);
|
|
8995
|
+
if (!fs22.existsSync(logDir)) {
|
|
8996
|
+
fs22.mkdirSync(logDir, { recursive: true });
|
|
8603
8997
|
}
|
|
8604
|
-
const executorLog =
|
|
8605
|
-
const reviewerLog =
|
|
8998
|
+
const executorLog = path24.join(logDir, "executor.log");
|
|
8999
|
+
const reviewerLog = path24.join(logDir, "reviewer.log");
|
|
8606
9000
|
if (!options?.force) {
|
|
8607
9001
|
const existingEntries2 = Array.from(
|
|
8608
9002
|
/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
|
|
@@ -8639,7 +9033,7 @@ function performInstall(projectDir, config, options) {
|
|
|
8639
9033
|
const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
|
|
8640
9034
|
if (installSlicer) {
|
|
8641
9035
|
const slicerSchedule = config.roadmapScanner.slicerSchedule;
|
|
8642
|
-
const slicerLog =
|
|
9036
|
+
const slicerLog = path24.join(logDir, "slicer.log");
|
|
8643
9037
|
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
8644
9038
|
entries.push(slicerEntry);
|
|
8645
9039
|
}
|
|
@@ -8647,7 +9041,7 @@ function performInstall(projectDir, config, options) {
|
|
|
8647
9041
|
const installQa = disableQa ? false : config.qa.enabled;
|
|
8648
9042
|
if (installQa) {
|
|
8649
9043
|
const qaSchedule = config.qa.schedule;
|
|
8650
|
-
const qaLog =
|
|
9044
|
+
const qaLog = path24.join(logDir, "qa.log");
|
|
8651
9045
|
const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
|
|
8652
9046
|
entries.push(qaEntry);
|
|
8653
9047
|
}
|
|
@@ -8655,10 +9049,18 @@ function performInstall(projectDir, config, options) {
|
|
|
8655
9049
|
const installAudit = disableAudit ? false : config.audit.enabled;
|
|
8656
9050
|
if (installAudit) {
|
|
8657
9051
|
const auditSchedule = config.audit.schedule;
|
|
8658
|
-
const auditLog =
|
|
9052
|
+
const auditLog = path24.join(logDir, "audit.log");
|
|
8659
9053
|
const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
|
|
8660
9054
|
entries.push(auditEntry);
|
|
8661
9055
|
}
|
|
9056
|
+
const disableAnalytics = options?.noAnalytics === true || options?.analytics === false;
|
|
9057
|
+
const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
|
|
9058
|
+
if (installAnalytics) {
|
|
9059
|
+
const analyticsSchedule = config.analytics.schedule;
|
|
9060
|
+
const analyticsLog = path24.join(logDir, "analytics.log");
|
|
9061
|
+
const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
|
|
9062
|
+
entries.push(analyticsEntry);
|
|
9063
|
+
}
|
|
8662
9064
|
const existingEntries = new Set(
|
|
8663
9065
|
Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]))
|
|
8664
9066
|
);
|
|
@@ -8677,7 +9079,7 @@ function performInstall(projectDir, config, options) {
|
|
|
8677
9079
|
}
|
|
8678
9080
|
}
|
|
8679
9081
|
function installCommand(program2) {
|
|
8680
|
-
program2.command("install").description("Add crontab entries for automated execution").option("-s, --schedule <cron>", "Cron schedule for PRD executor").option("--reviewer-schedule <cron>", "Cron schedule for reviewer").option("--no-reviewer", "Skip installing reviewer cron").option("--no-slicer", "Skip installing slicer cron").option("--no-qa", "Skip installing QA cron").option("--no-audit", "Skip installing audit cron").option("-f, --force", "Replace existing cron entries for this project").action(async (options) => {
|
|
9082
|
+
program2.command("install").description("Add crontab entries for automated execution").option("-s, --schedule <cron>", "Cron schedule for PRD executor").option("--reviewer-schedule <cron>", "Cron schedule for reviewer").option("--no-reviewer", "Skip installing reviewer cron").option("--no-slicer", "Skip installing slicer cron").option("--no-qa", "Skip installing QA cron").option("--no-audit", "Skip installing audit cron").option("--no-analytics", "Skip installing analytics cron").option("-f, --force", "Replace existing cron entries for this project").action(async (options) => {
|
|
8681
9083
|
try {
|
|
8682
9084
|
const projectDir = process.cwd();
|
|
8683
9085
|
const config = loadConfig(projectDir);
|
|
@@ -8686,12 +9088,12 @@ function installCommand(program2) {
|
|
|
8686
9088
|
const nightWatchBin = getNightWatchBinPath();
|
|
8687
9089
|
const projectName = getProjectName(projectDir);
|
|
8688
9090
|
const marker = generateMarker(projectName);
|
|
8689
|
-
const logDir =
|
|
8690
|
-
if (!
|
|
8691
|
-
|
|
9091
|
+
const logDir = path24.join(projectDir, LOG_DIR);
|
|
9092
|
+
if (!fs22.existsSync(logDir)) {
|
|
9093
|
+
fs22.mkdirSync(logDir, { recursive: true });
|
|
8692
9094
|
}
|
|
8693
|
-
const executorLog =
|
|
8694
|
-
const reviewerLog =
|
|
9095
|
+
const executorLog = path24.join(logDir, "executor.log");
|
|
9096
|
+
const reviewerLog = path24.join(logDir, "reviewer.log");
|
|
8695
9097
|
const existingEntries = Array.from(
|
|
8696
9098
|
/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
|
|
8697
9099
|
);
|
|
@@ -8727,7 +9129,7 @@ function installCommand(program2) {
|
|
|
8727
9129
|
const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
|
|
8728
9130
|
let slicerLog;
|
|
8729
9131
|
if (installSlicer) {
|
|
8730
|
-
slicerLog =
|
|
9132
|
+
slicerLog = path24.join(logDir, "slicer.log");
|
|
8731
9133
|
const slicerSchedule = config.roadmapScanner.slicerSchedule;
|
|
8732
9134
|
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
8733
9135
|
entries.push(slicerEntry);
|
|
@@ -8736,7 +9138,7 @@ function installCommand(program2) {
|
|
|
8736
9138
|
const installQa = disableQa ? false : config.qa.enabled;
|
|
8737
9139
|
let qaLog;
|
|
8738
9140
|
if (installQa) {
|
|
8739
|
-
qaLog =
|
|
9141
|
+
qaLog = path24.join(logDir, "qa.log");
|
|
8740
9142
|
const qaSchedule = config.qa.schedule;
|
|
8741
9143
|
const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
|
|
8742
9144
|
entries.push(qaEntry);
|
|
@@ -8745,11 +9147,20 @@ function installCommand(program2) {
|
|
|
8745
9147
|
const installAudit = disableAudit ? false : config.audit.enabled;
|
|
8746
9148
|
let auditLog;
|
|
8747
9149
|
if (installAudit) {
|
|
8748
|
-
auditLog =
|
|
9150
|
+
auditLog = path24.join(logDir, "audit.log");
|
|
8749
9151
|
const auditSchedule = config.audit.schedule;
|
|
8750
9152
|
const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
|
|
8751
9153
|
entries.push(auditEntry);
|
|
8752
9154
|
}
|
|
9155
|
+
const disableAnalytics = options.noAnalytics === true || options.analytics === false;
|
|
9156
|
+
const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
|
|
9157
|
+
let analyticsLog;
|
|
9158
|
+
if (installAnalytics) {
|
|
9159
|
+
analyticsLog = path24.join(logDir, "analytics.log");
|
|
9160
|
+
const analyticsSchedule = config.analytics.schedule;
|
|
9161
|
+
const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
|
|
9162
|
+
entries.push(analyticsEntry);
|
|
9163
|
+
}
|
|
8753
9164
|
const existingEntrySet = new Set(existingEntries);
|
|
8754
9165
|
const currentCrontab = readCrontab();
|
|
8755
9166
|
const baseCrontab = options.force ? currentCrontab.filter((line) => !existingEntrySet.has(line) && !line.includes(marker)) : currentCrontab;
|
|
@@ -8776,6 +9187,9 @@ function installCommand(program2) {
|
|
|
8776
9187
|
if (installAudit && auditLog) {
|
|
8777
9188
|
dim(` Audit: ${auditLog}`);
|
|
8778
9189
|
}
|
|
9190
|
+
if (installAnalytics && analyticsLog) {
|
|
9191
|
+
dim(` Analytics: ${analyticsLog}`);
|
|
9192
|
+
}
|
|
8779
9193
|
console.log();
|
|
8780
9194
|
dim("To uninstall, run: night-watch uninstall");
|
|
8781
9195
|
dim("To check status, run: night-watch status");
|
|
@@ -8790,8 +9204,8 @@ function installCommand(program2) {
|
|
|
8790
9204
|
|
|
8791
9205
|
// src/commands/uninstall.ts
|
|
8792
9206
|
init_dist();
|
|
8793
|
-
import * as
|
|
8794
|
-
import * as
|
|
9207
|
+
import * as path25 from "path";
|
|
9208
|
+
import * as fs23 from "fs";
|
|
8795
9209
|
function performUninstall(projectDir, options) {
|
|
8796
9210
|
try {
|
|
8797
9211
|
const projectName = getProjectName(projectDir);
|
|
@@ -8806,19 +9220,19 @@ function performUninstall(projectDir, options) {
|
|
|
8806
9220
|
const removedCount = removeEntriesForProject(projectDir, marker);
|
|
8807
9221
|
unregisterProject(projectDir);
|
|
8808
9222
|
if (!options?.keepLogs) {
|
|
8809
|
-
const logDir =
|
|
8810
|
-
if (
|
|
9223
|
+
const logDir = path25.join(projectDir, "logs");
|
|
9224
|
+
if (fs23.existsSync(logDir)) {
|
|
8811
9225
|
const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
|
|
8812
9226
|
logFiles.forEach((logFile) => {
|
|
8813
|
-
const logPath =
|
|
8814
|
-
if (
|
|
8815
|
-
|
|
9227
|
+
const logPath = path25.join(logDir, logFile);
|
|
9228
|
+
if (fs23.existsSync(logPath)) {
|
|
9229
|
+
fs23.unlinkSync(logPath);
|
|
8816
9230
|
}
|
|
8817
9231
|
});
|
|
8818
9232
|
try {
|
|
8819
|
-
const remainingFiles =
|
|
9233
|
+
const remainingFiles = fs23.readdirSync(logDir);
|
|
8820
9234
|
if (remainingFiles.length === 0) {
|
|
8821
|
-
|
|
9235
|
+
fs23.rmdirSync(logDir);
|
|
8822
9236
|
}
|
|
8823
9237
|
} catch {
|
|
8824
9238
|
}
|
|
@@ -8851,21 +9265,21 @@ function uninstallCommand(program2) {
|
|
|
8851
9265
|
existingEntries.forEach((entry) => dim(` ${entry}`));
|
|
8852
9266
|
const removedCount = removeEntriesForProject(projectDir, marker);
|
|
8853
9267
|
if (!options.keepLogs) {
|
|
8854
|
-
const logDir =
|
|
8855
|
-
if (
|
|
9268
|
+
const logDir = path25.join(projectDir, "logs");
|
|
9269
|
+
if (fs23.existsSync(logDir)) {
|
|
8856
9270
|
const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
|
|
8857
9271
|
let logsRemoved = 0;
|
|
8858
9272
|
logFiles.forEach((logFile) => {
|
|
8859
|
-
const logPath =
|
|
8860
|
-
if (
|
|
8861
|
-
|
|
9273
|
+
const logPath = path25.join(logDir, logFile);
|
|
9274
|
+
if (fs23.existsSync(logPath)) {
|
|
9275
|
+
fs23.unlinkSync(logPath);
|
|
8862
9276
|
logsRemoved++;
|
|
8863
9277
|
}
|
|
8864
9278
|
});
|
|
8865
9279
|
try {
|
|
8866
|
-
const remainingFiles =
|
|
9280
|
+
const remainingFiles = fs23.readdirSync(logDir);
|
|
8867
9281
|
if (remainingFiles.length === 0) {
|
|
8868
|
-
|
|
9282
|
+
fs23.rmdirSync(logDir);
|
|
8869
9283
|
}
|
|
8870
9284
|
} catch {
|
|
8871
9285
|
}
|
|
@@ -9101,14 +9515,14 @@ function statusCommand(program2) {
|
|
|
9101
9515
|
// src/commands/logs.ts
|
|
9102
9516
|
init_dist();
|
|
9103
9517
|
import { spawn as spawn3 } from "child_process";
|
|
9104
|
-
import * as
|
|
9105
|
-
import * as
|
|
9518
|
+
import * as path26 from "path";
|
|
9519
|
+
import * as fs24 from "fs";
|
|
9106
9520
|
function getLastLines(filePath, lineCount) {
|
|
9107
|
-
if (!
|
|
9521
|
+
if (!fs24.existsSync(filePath)) {
|
|
9108
9522
|
return `Log file not found: ${filePath}`;
|
|
9109
9523
|
}
|
|
9110
9524
|
try {
|
|
9111
|
-
const content =
|
|
9525
|
+
const content = fs24.readFileSync(filePath, "utf-8");
|
|
9112
9526
|
const lines = content.trim().split("\n");
|
|
9113
9527
|
return lines.slice(-lineCount).join("\n");
|
|
9114
9528
|
} catch (error2) {
|
|
@@ -9116,7 +9530,7 @@ function getLastLines(filePath, lineCount) {
|
|
|
9116
9530
|
}
|
|
9117
9531
|
}
|
|
9118
9532
|
function followLog(filePath) {
|
|
9119
|
-
if (!
|
|
9533
|
+
if (!fs24.existsSync(filePath)) {
|
|
9120
9534
|
console.log(`Log file not found: ${filePath}`);
|
|
9121
9535
|
console.log("The log file will be created when the first execution runs.");
|
|
9122
9536
|
return;
|
|
@@ -9136,13 +9550,13 @@ function logsCommand(program2) {
|
|
|
9136
9550
|
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) => {
|
|
9137
9551
|
try {
|
|
9138
9552
|
const projectDir = process.cwd();
|
|
9139
|
-
const logDir =
|
|
9553
|
+
const logDir = path26.join(projectDir, LOG_DIR);
|
|
9140
9554
|
const lineCount = parseInt(options.lines || "50", 10);
|
|
9141
|
-
const executorLog =
|
|
9142
|
-
const reviewerLog =
|
|
9143
|
-
const qaLog =
|
|
9144
|
-
const auditLog =
|
|
9145
|
-
const plannerLog =
|
|
9555
|
+
const executorLog = path26.join(logDir, EXECUTOR_LOG_FILE);
|
|
9556
|
+
const reviewerLog = path26.join(logDir, REVIEWER_LOG_FILE);
|
|
9557
|
+
const qaLog = path26.join(logDir, `${QA_LOG_NAME}.log`);
|
|
9558
|
+
const auditLog = path26.join(logDir, `${AUDIT_LOG_NAME}.log`);
|
|
9559
|
+
const plannerLog = path26.join(logDir, `${PLANNER_LOG_NAME}.log`);
|
|
9146
9560
|
const logType = options.type?.toLowerCase() || "all";
|
|
9147
9561
|
const showExecutor = logType === "all" || logType === "run" || logType === "executor";
|
|
9148
9562
|
const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
|
|
@@ -9206,15 +9620,15 @@ function logsCommand(program2) {
|
|
|
9206
9620
|
|
|
9207
9621
|
// src/commands/prd.ts
|
|
9208
9622
|
init_dist();
|
|
9209
|
-
import * as
|
|
9210
|
-
import * as
|
|
9623
|
+
import * as fs25 from "fs";
|
|
9624
|
+
import * as path27 from "path";
|
|
9211
9625
|
import * as readline2 from "readline";
|
|
9212
9626
|
function slugify2(name) {
|
|
9213
9627
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
9214
9628
|
}
|
|
9215
9629
|
function getNextPrdNumber2(prdDir) {
|
|
9216
|
-
if (!
|
|
9217
|
-
const files =
|
|
9630
|
+
if (!fs25.existsSync(prdDir)) return 1;
|
|
9631
|
+
const files = fs25.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
|
|
9218
9632
|
const numbers = files.map((f) => {
|
|
9219
9633
|
const match = f.match(/^(\d+)-/);
|
|
9220
9634
|
return match ? parseInt(match[1], 10) : 0;
|
|
@@ -9235,10 +9649,10 @@ function parseDependencies(content) {
|
|
|
9235
9649
|
}
|
|
9236
9650
|
function isClaimActive(claimPath, maxRuntime) {
|
|
9237
9651
|
try {
|
|
9238
|
-
if (!
|
|
9652
|
+
if (!fs25.existsSync(claimPath)) {
|
|
9239
9653
|
return { active: false };
|
|
9240
9654
|
}
|
|
9241
|
-
const content =
|
|
9655
|
+
const content = fs25.readFileSync(claimPath, "utf-8");
|
|
9242
9656
|
const claim = JSON.parse(content);
|
|
9243
9657
|
const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
|
|
9244
9658
|
if (age < maxRuntime) {
|
|
@@ -9254,9 +9668,9 @@ function prdCommand(program2) {
|
|
|
9254
9668
|
prd.command("create").description("Generate a new PRD markdown file from template").argument("<name>", "PRD name (used for title and filename)").option("-i, --interactive", "Prompt for complexity, dependencies, and phase count", false).option("-t, --template <path>", "Path to a custom template file").option("--deps <files>", "Comma-separated dependency filenames").option("--phases <count>", "Number of execution phases", "3").option("--no-number", "Skip auto-numbering prefix").action(async (name, options) => {
|
|
9255
9669
|
const projectDir = process.cwd();
|
|
9256
9670
|
const config = loadConfig(projectDir);
|
|
9257
|
-
const prdDir =
|
|
9258
|
-
if (!
|
|
9259
|
-
|
|
9671
|
+
const prdDir = path27.join(projectDir, config.prdDir);
|
|
9672
|
+
if (!fs25.existsSync(prdDir)) {
|
|
9673
|
+
fs25.mkdirSync(prdDir, { recursive: true });
|
|
9260
9674
|
}
|
|
9261
9675
|
let complexityScore = 5;
|
|
9262
9676
|
let dependsOn = [];
|
|
@@ -9315,20 +9729,20 @@ function prdCommand(program2) {
|
|
|
9315
9729
|
} else {
|
|
9316
9730
|
filename = `${slug}.md`;
|
|
9317
9731
|
}
|
|
9318
|
-
const filePath =
|
|
9319
|
-
if (
|
|
9732
|
+
const filePath = path27.join(prdDir, filename);
|
|
9733
|
+
if (fs25.existsSync(filePath)) {
|
|
9320
9734
|
error(`File already exists: ${filePath}`);
|
|
9321
9735
|
dim("Use a different name or remove the existing file.");
|
|
9322
9736
|
process.exit(1);
|
|
9323
9737
|
}
|
|
9324
9738
|
let customTemplate;
|
|
9325
9739
|
if (options.template) {
|
|
9326
|
-
const templatePath =
|
|
9327
|
-
if (!
|
|
9740
|
+
const templatePath = path27.resolve(options.template);
|
|
9741
|
+
if (!fs25.existsSync(templatePath)) {
|
|
9328
9742
|
error(`Template file not found: ${templatePath}`);
|
|
9329
9743
|
process.exit(1);
|
|
9330
9744
|
}
|
|
9331
|
-
customTemplate =
|
|
9745
|
+
customTemplate = fs25.readFileSync(templatePath, "utf-8");
|
|
9332
9746
|
}
|
|
9333
9747
|
const vars = {
|
|
9334
9748
|
title: name,
|
|
@@ -9339,7 +9753,7 @@ function prdCommand(program2) {
|
|
|
9339
9753
|
phaseCount
|
|
9340
9754
|
};
|
|
9341
9755
|
const content = renderPrdTemplate(vars, customTemplate);
|
|
9342
|
-
|
|
9756
|
+
fs25.writeFileSync(filePath, content, "utf-8");
|
|
9343
9757
|
header("PRD Created");
|
|
9344
9758
|
success(`Created: ${filePath}`);
|
|
9345
9759
|
info(`Title: ${name}`);
|
|
@@ -9351,15 +9765,15 @@ function prdCommand(program2) {
|
|
|
9351
9765
|
prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
|
|
9352
9766
|
const projectDir = process.cwd();
|
|
9353
9767
|
const config = loadConfig(projectDir);
|
|
9354
|
-
const absolutePrdDir =
|
|
9355
|
-
const doneDir =
|
|
9768
|
+
const absolutePrdDir = path27.join(projectDir, config.prdDir);
|
|
9769
|
+
const doneDir = path27.join(absolutePrdDir, "done");
|
|
9356
9770
|
const pending = [];
|
|
9357
|
-
if (
|
|
9358
|
-
const files =
|
|
9771
|
+
if (fs25.existsSync(absolutePrdDir)) {
|
|
9772
|
+
const files = fs25.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
|
|
9359
9773
|
for (const file of files) {
|
|
9360
|
-
const content =
|
|
9774
|
+
const content = fs25.readFileSync(path27.join(absolutePrdDir, file), "utf-8");
|
|
9361
9775
|
const deps = parseDependencies(content);
|
|
9362
|
-
const claimPath =
|
|
9776
|
+
const claimPath = path27.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
|
|
9363
9777
|
const claimStatus = isClaimActive(claimPath, config.maxRuntime);
|
|
9364
9778
|
pending.push({
|
|
9365
9779
|
name: file,
|
|
@@ -9370,10 +9784,10 @@ function prdCommand(program2) {
|
|
|
9370
9784
|
}
|
|
9371
9785
|
}
|
|
9372
9786
|
const done = [];
|
|
9373
|
-
if (
|
|
9374
|
-
const files =
|
|
9787
|
+
if (fs25.existsSync(doneDir)) {
|
|
9788
|
+
const files = fs25.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
|
|
9375
9789
|
for (const file of files) {
|
|
9376
|
-
const content =
|
|
9790
|
+
const content = fs25.readFileSync(path27.join(doneDir, file), "utf-8");
|
|
9377
9791
|
const deps = parseDependencies(content);
|
|
9378
9792
|
done.push({ name: file, dependencies: deps });
|
|
9379
9793
|
}
|
|
@@ -9412,7 +9826,7 @@ import blessed6 from "blessed";
|
|
|
9412
9826
|
// src/commands/dashboard/tab-status.ts
|
|
9413
9827
|
init_dist();
|
|
9414
9828
|
import blessed from "blessed";
|
|
9415
|
-
import * as
|
|
9829
|
+
import * as fs26 from "fs";
|
|
9416
9830
|
function sortPrdsByPriority(prds, priority) {
|
|
9417
9831
|
if (priority.length === 0) return prds;
|
|
9418
9832
|
const priorityMap = /* @__PURE__ */ new Map();
|
|
@@ -9508,7 +9922,7 @@ function renderLogPane(projectDir, logs) {
|
|
|
9508
9922
|
let newestMtime = 0;
|
|
9509
9923
|
for (const log of existingLogs) {
|
|
9510
9924
|
try {
|
|
9511
|
-
const stat =
|
|
9925
|
+
const stat = fs26.statSync(log.path);
|
|
9512
9926
|
if (stat.mtimeMs > newestMtime) {
|
|
9513
9927
|
newestMtime = stat.mtimeMs;
|
|
9514
9928
|
newestLog = log;
|
|
@@ -11163,8 +11577,8 @@ function createActionsTab() {
|
|
|
11163
11577
|
// src/commands/dashboard/tab-logs.ts
|
|
11164
11578
|
init_dist();
|
|
11165
11579
|
import blessed5 from "blessed";
|
|
11166
|
-
import * as
|
|
11167
|
-
import * as
|
|
11580
|
+
import * as fs27 from "fs";
|
|
11581
|
+
import * as path28 from "path";
|
|
11168
11582
|
var LOG_NAMES = ["executor", "reviewer"];
|
|
11169
11583
|
var LOG_LINES = 200;
|
|
11170
11584
|
function createLogsTab() {
|
|
@@ -11205,7 +11619,7 @@ function createLogsTab() {
|
|
|
11205
11619
|
let activeKeyHandlers = [];
|
|
11206
11620
|
let activeCtx = null;
|
|
11207
11621
|
function getLogPath(projectDir, logName) {
|
|
11208
|
-
return
|
|
11622
|
+
return path28.join(projectDir, "logs", `${logName}.log`);
|
|
11209
11623
|
}
|
|
11210
11624
|
function updateSelector() {
|
|
11211
11625
|
const tabs = LOG_NAMES.map((name, idx) => {
|
|
@@ -11219,7 +11633,7 @@ function createLogsTab() {
|
|
|
11219
11633
|
function loadLog(ctx) {
|
|
11220
11634
|
const logName = LOG_NAMES[selectedLogIndex];
|
|
11221
11635
|
const logPath = getLogPath(ctx.projectDir, logName);
|
|
11222
|
-
if (!
|
|
11636
|
+
if (!fs27.existsSync(logPath)) {
|
|
11223
11637
|
logContent.setContent(
|
|
11224
11638
|
`{yellow-fg}No ${logName}.log file found{/yellow-fg}
|
|
11225
11639
|
|
|
@@ -11229,7 +11643,7 @@ Log will appear here once the ${logName} runs.`
|
|
|
11229
11643
|
return;
|
|
11230
11644
|
}
|
|
11231
11645
|
try {
|
|
11232
|
-
const stat =
|
|
11646
|
+
const stat = fs27.statSync(logPath);
|
|
11233
11647
|
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
11234
11648
|
logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
|
|
11235
11649
|
} catch {
|
|
@@ -11735,12 +12149,12 @@ function doctorCommand(program2) {
|
|
|
11735
12149
|
|
|
11736
12150
|
// src/commands/serve.ts
|
|
11737
12151
|
init_dist();
|
|
11738
|
-
import * as
|
|
12152
|
+
import * as fs32 from "fs";
|
|
11739
12153
|
|
|
11740
12154
|
// ../server/dist/index.js
|
|
11741
12155
|
init_dist();
|
|
11742
|
-
import * as
|
|
11743
|
-
import * as
|
|
12156
|
+
import * as fs31 from "fs";
|
|
12157
|
+
import * as path34 from "path";
|
|
11744
12158
|
import { dirname as dirname7 } from "path";
|
|
11745
12159
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
11746
12160
|
import cors from "cors";
|
|
@@ -11825,8 +12239,8 @@ function setupGracefulShutdown(server, beforeClose) {
|
|
|
11825
12239
|
|
|
11826
12240
|
// ../server/dist/middleware/project-resolver.middleware.js
|
|
11827
12241
|
init_dist();
|
|
11828
|
-
import * as
|
|
11829
|
-
import * as
|
|
12242
|
+
import * as fs28 from "fs";
|
|
12243
|
+
import * as path29 from "path";
|
|
11830
12244
|
function resolveProject(req, res, next) {
|
|
11831
12245
|
const projectId = req.params.projectId;
|
|
11832
12246
|
const decodedId = decodeURIComponent(projectId).replace(/~/g, "/");
|
|
@@ -11836,7 +12250,7 @@ function resolveProject(req, res, next) {
|
|
|
11836
12250
|
res.status(404).json({ error: `Project not found: ${decodedId}` });
|
|
11837
12251
|
return;
|
|
11838
12252
|
}
|
|
11839
|
-
if (!
|
|
12253
|
+
if (!fs28.existsSync(entry.path) || !fs28.existsSync(path29.join(entry.path, CONFIG_FILE_NAME))) {
|
|
11840
12254
|
res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
|
|
11841
12255
|
return;
|
|
11842
12256
|
}
|
|
@@ -11881,8 +12295,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
|
|
|
11881
12295
|
|
|
11882
12296
|
// ../server/dist/routes/action.routes.js
|
|
11883
12297
|
init_dist();
|
|
11884
|
-
import * as
|
|
11885
|
-
import * as
|
|
12298
|
+
import * as fs29 from "fs";
|
|
12299
|
+
import * as path30 from "path";
|
|
11886
12300
|
import { execSync as execSync5, spawn as spawn5 } from "child_process";
|
|
11887
12301
|
import { Router } from "express";
|
|
11888
12302
|
|
|
@@ -11920,17 +12334,17 @@ function getBoardProvider(config, projectDir) {
|
|
|
11920
12334
|
function cleanOrphanedClaims(dir) {
|
|
11921
12335
|
let entries;
|
|
11922
12336
|
try {
|
|
11923
|
-
entries =
|
|
12337
|
+
entries = fs29.readdirSync(dir, { withFileTypes: true });
|
|
11924
12338
|
} catch {
|
|
11925
12339
|
return;
|
|
11926
12340
|
}
|
|
11927
12341
|
for (const entry of entries) {
|
|
11928
|
-
const fullPath =
|
|
12342
|
+
const fullPath = path30.join(dir, entry.name);
|
|
11929
12343
|
if (entry.isDirectory() && entry.name !== "done") {
|
|
11930
12344
|
cleanOrphanedClaims(fullPath);
|
|
11931
12345
|
} else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
|
|
11932
12346
|
try {
|
|
11933
|
-
|
|
12347
|
+
fs29.unlinkSync(fullPath);
|
|
11934
12348
|
} catch {
|
|
11935
12349
|
}
|
|
11936
12350
|
}
|
|
@@ -12027,6 +12441,9 @@ function createActionRouteHandlers(ctx) {
|
|
|
12027
12441
|
router.post(`/${p}audit`, (req, res) => {
|
|
12028
12442
|
spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
|
|
12029
12443
|
});
|
|
12444
|
+
router.post(`/${p}analytics`, (req, res) => {
|
|
12445
|
+
spawnAction2(ctx.getProjectDir(req), ["analytics"], req, res);
|
|
12446
|
+
});
|
|
12030
12447
|
router.post(`/${p}planner`, (req, res) => {
|
|
12031
12448
|
spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
|
|
12032
12449
|
});
|
|
@@ -12084,19 +12501,19 @@ function createActionRouteHandlers(ctx) {
|
|
|
12084
12501
|
res.status(400).json({ error: "Invalid PRD name" });
|
|
12085
12502
|
return;
|
|
12086
12503
|
}
|
|
12087
|
-
const prdDir =
|
|
12504
|
+
const prdDir = path30.join(projectDir, config.prdDir);
|
|
12088
12505
|
const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
|
|
12089
|
-
const pendingPath =
|
|
12090
|
-
const donePath =
|
|
12091
|
-
if (
|
|
12506
|
+
const pendingPath = path30.join(prdDir, normalized);
|
|
12507
|
+
const donePath = path30.join(prdDir, "done", normalized);
|
|
12508
|
+
if (fs29.existsSync(pendingPath)) {
|
|
12092
12509
|
res.json({ message: `"${normalized}" is already pending` });
|
|
12093
12510
|
return;
|
|
12094
12511
|
}
|
|
12095
|
-
if (!
|
|
12512
|
+
if (!fs29.existsSync(donePath)) {
|
|
12096
12513
|
res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
|
|
12097
12514
|
return;
|
|
12098
12515
|
}
|
|
12099
|
-
|
|
12516
|
+
fs29.renameSync(donePath, pendingPath);
|
|
12100
12517
|
res.json({ message: `Moved "${normalized}" back to pending` });
|
|
12101
12518
|
} catch (error2) {
|
|
12102
12519
|
res.status(500).json({
|
|
@@ -12114,11 +12531,11 @@ function createActionRouteHandlers(ctx) {
|
|
|
12114
12531
|
res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
|
|
12115
12532
|
return;
|
|
12116
12533
|
}
|
|
12117
|
-
if (
|
|
12118
|
-
|
|
12534
|
+
if (fs29.existsSync(lockPath)) {
|
|
12535
|
+
fs29.unlinkSync(lockPath);
|
|
12119
12536
|
}
|
|
12120
|
-
const prdDir =
|
|
12121
|
-
if (
|
|
12537
|
+
const prdDir = path30.join(projectDir, config.prdDir);
|
|
12538
|
+
if (fs29.existsSync(prdDir)) {
|
|
12122
12539
|
cleanOrphanedClaims(prdDir);
|
|
12123
12540
|
}
|
|
12124
12541
|
broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
|
|
@@ -12314,6 +12731,7 @@ function createBoardRouteHandlers(ctx) {
|
|
|
12314
12731
|
return;
|
|
12315
12732
|
}
|
|
12316
12733
|
await provider.closeIssue(issueNumber);
|
|
12734
|
+
await provider.moveIssue(issueNumber, "Done");
|
|
12317
12735
|
invalidateBoardCache(projectDir);
|
|
12318
12736
|
res.json({ closed: true });
|
|
12319
12737
|
} catch (error2) {
|
|
@@ -12408,6 +12826,9 @@ function validateConfigChanges(changes, currentConfig) {
|
|
|
12408
12826
|
if (changes.reviewerRetryDelay !== void 0 && (typeof changes.reviewerRetryDelay !== "number" || !Number.isInteger(changes.reviewerRetryDelay) || changes.reviewerRetryDelay < 0 || changes.reviewerRetryDelay > 300)) {
|
|
12409
12827
|
return "reviewerRetryDelay must be an integer between 0 and 300";
|
|
12410
12828
|
}
|
|
12829
|
+
if (changes.reviewerMaxPrsPerRun !== void 0 && (typeof changes.reviewerMaxPrsPerRun !== "number" || !Number.isInteger(changes.reviewerMaxPrsPerRun) || changes.reviewerMaxPrsPerRun < 0 || changes.reviewerMaxPrsPerRun > 100)) {
|
|
12830
|
+
return "reviewerMaxPrsPerRun must be an integer between 0 and 100";
|
|
12831
|
+
}
|
|
12411
12832
|
if (changes.branchPatterns !== void 0 && (!Array.isArray(changes.branchPatterns) || !changes.branchPatterns.every((p) => typeof p === "string"))) {
|
|
12412
12833
|
return "branchPatterns must be an array of strings";
|
|
12413
12834
|
}
|
|
@@ -12617,6 +13038,34 @@ function validateConfigChanges(changes, currentConfig) {
|
|
|
12617
13038
|
return "audit.maxRuntime must be a number >= 60";
|
|
12618
13039
|
}
|
|
12619
13040
|
}
|
|
13041
|
+
if (changes.analytics !== void 0) {
|
|
13042
|
+
if (typeof changes.analytics !== "object" || changes.analytics === null) {
|
|
13043
|
+
return "analytics must be an object";
|
|
13044
|
+
}
|
|
13045
|
+
const analytics = changes.analytics;
|
|
13046
|
+
if (analytics.enabled !== void 0 && typeof analytics.enabled !== "boolean") {
|
|
13047
|
+
return "analytics.enabled must be a boolean";
|
|
13048
|
+
}
|
|
13049
|
+
const analyticsScheduleError = validateCronField("analytics.schedule", analytics.schedule);
|
|
13050
|
+
if (analyticsScheduleError) {
|
|
13051
|
+
return analyticsScheduleError;
|
|
13052
|
+
}
|
|
13053
|
+
if (analytics.maxRuntime !== void 0 && (typeof analytics.maxRuntime !== "number" || analytics.maxRuntime < 60)) {
|
|
13054
|
+
return "analytics.maxRuntime must be a number >= 60";
|
|
13055
|
+
}
|
|
13056
|
+
if (analytics.lookbackDays !== void 0 && (typeof analytics.lookbackDays !== "number" || analytics.lookbackDays < 1 || analytics.lookbackDays > 90)) {
|
|
13057
|
+
return "analytics.lookbackDays must be a number between 1 and 90";
|
|
13058
|
+
}
|
|
13059
|
+
if (analytics.targetColumn !== void 0) {
|
|
13060
|
+
const validColumns = ["Draft", "Ready", "In Progress", "Review", "Done"];
|
|
13061
|
+
if (!validColumns.includes(analytics.targetColumn)) {
|
|
13062
|
+
return `analytics.targetColumn must be one of: ${validColumns.join(", ")}`;
|
|
13063
|
+
}
|
|
13064
|
+
}
|
|
13065
|
+
if (analytics.analysisPrompt !== void 0 && typeof analytics.analysisPrompt !== "string") {
|
|
13066
|
+
return "analytics.analysisPrompt must be a string";
|
|
13067
|
+
}
|
|
13068
|
+
}
|
|
12620
13069
|
if (changes.queue !== void 0) {
|
|
12621
13070
|
if (typeof changes.queue !== "object" || changes.queue === null) {
|
|
12622
13071
|
return "queue must be an object";
|
|
@@ -12635,7 +13084,14 @@ function validateConfigChanges(changes, currentConfig) {
|
|
|
12635
13084
|
if (typeof queue.priority !== "object" || queue.priority === null) {
|
|
12636
13085
|
return "queue.priority must be an object";
|
|
12637
13086
|
}
|
|
12638
|
-
const validQueueJobs = [
|
|
13087
|
+
const validQueueJobs = [
|
|
13088
|
+
"executor",
|
|
13089
|
+
"reviewer",
|
|
13090
|
+
"qa",
|
|
13091
|
+
"audit",
|
|
13092
|
+
"slicer",
|
|
13093
|
+
"analytics"
|
|
13094
|
+
];
|
|
12639
13095
|
for (const [jobType, value] of Object.entries(queue.priority)) {
|
|
12640
13096
|
if (!validQueueJobs.includes(jobType)) {
|
|
12641
13097
|
return `queue.priority contains invalid job type: ${jobType}`;
|
|
@@ -12771,8 +13227,8 @@ function createProjectConfigRoutes() {
|
|
|
12771
13227
|
|
|
12772
13228
|
// ../server/dist/routes/doctor.routes.js
|
|
12773
13229
|
init_dist();
|
|
12774
|
-
import * as
|
|
12775
|
-
import * as
|
|
13230
|
+
import * as fs30 from "fs";
|
|
13231
|
+
import * as path31 from "path";
|
|
12776
13232
|
import { execSync as execSync6 } from "child_process";
|
|
12777
13233
|
import { Router as Router4 } from "express";
|
|
12778
13234
|
function runDoctorChecks(projectDir, config) {
|
|
@@ -12805,7 +13261,7 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12805
13261
|
});
|
|
12806
13262
|
}
|
|
12807
13263
|
try {
|
|
12808
|
-
const projectName =
|
|
13264
|
+
const projectName = path31.basename(projectDir);
|
|
12809
13265
|
const marker = generateMarker(projectName);
|
|
12810
13266
|
const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
|
|
12811
13267
|
if (crontabEntries.length > 0) {
|
|
@@ -12828,8 +13284,8 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12828
13284
|
detail: "Failed to check crontab"
|
|
12829
13285
|
});
|
|
12830
13286
|
}
|
|
12831
|
-
const configPath =
|
|
12832
|
-
if (
|
|
13287
|
+
const configPath = path31.join(projectDir, CONFIG_FILE_NAME);
|
|
13288
|
+
if (fs30.existsSync(configPath)) {
|
|
12833
13289
|
checks.push({ name: "config", status: "pass", detail: "Config file exists" });
|
|
12834
13290
|
} else {
|
|
12835
13291
|
checks.push({
|
|
@@ -12838,9 +13294,9 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12838
13294
|
detail: "Config file not found (using defaults)"
|
|
12839
13295
|
});
|
|
12840
13296
|
}
|
|
12841
|
-
const prdDir =
|
|
12842
|
-
if (
|
|
12843
|
-
const prds =
|
|
13297
|
+
const prdDir = path31.join(projectDir, config.prdDir);
|
|
13298
|
+
if (fs30.existsSync(prdDir)) {
|
|
13299
|
+
const prds = fs30.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
|
|
12844
13300
|
checks.push({
|
|
12845
13301
|
name: "prdDir",
|
|
12846
13302
|
status: "pass",
|
|
@@ -12883,7 +13339,7 @@ function createProjectDoctorRoutes() {
|
|
|
12883
13339
|
|
|
12884
13340
|
// ../server/dist/routes/log.routes.js
|
|
12885
13341
|
init_dist();
|
|
12886
|
-
import * as
|
|
13342
|
+
import * as path32 from "path";
|
|
12887
13343
|
import { Router as Router5 } from "express";
|
|
12888
13344
|
function createLogRoutes(deps) {
|
|
12889
13345
|
const { projectDir } = deps;
|
|
@@ -12891,7 +13347,7 @@ function createLogRoutes(deps) {
|
|
|
12891
13347
|
router.get("/:name", (req, res) => {
|
|
12892
13348
|
try {
|
|
12893
13349
|
const { name } = req.params;
|
|
12894
|
-
const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
|
|
13350
|
+
const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
|
|
12895
13351
|
if (!validNames.includes(name)) {
|
|
12896
13352
|
res.status(400).json({
|
|
12897
13353
|
error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
|
|
@@ -12902,7 +13358,7 @@ function createLogRoutes(deps) {
|
|
|
12902
13358
|
const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
|
|
12903
13359
|
const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
|
|
12904
13360
|
const fileName = LOG_FILE_NAMES[name] || name;
|
|
12905
|
-
const logPath =
|
|
13361
|
+
const logPath = path32.join(projectDir, LOG_DIR, `${fileName}.log`);
|
|
12906
13362
|
const logLines = getLastLogLines(logPath, linesToRead);
|
|
12907
13363
|
res.json({ name, lines: logLines });
|
|
12908
13364
|
} catch (error2) {
|
|
@@ -12917,7 +13373,7 @@ function createProjectLogRoutes() {
|
|
|
12917
13373
|
try {
|
|
12918
13374
|
const projectDir = req.projectDir;
|
|
12919
13375
|
const { name } = req.params;
|
|
12920
|
-
const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
|
|
13376
|
+
const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
|
|
12921
13377
|
if (!validNames.includes(name)) {
|
|
12922
13378
|
res.status(400).json({
|
|
12923
13379
|
error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
|
|
@@ -12928,7 +13384,7 @@ function createProjectLogRoutes() {
|
|
|
12928
13384
|
const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
|
|
12929
13385
|
const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
|
|
12930
13386
|
const fileName = LOG_FILE_NAMES[name] || name;
|
|
12931
|
-
const logPath =
|
|
13387
|
+
const logPath = path32.join(projectDir, LOG_DIR, `${fileName}.log`);
|
|
12932
13388
|
const logLines = getLastLogLines(logPath, linesToRead);
|
|
12933
13389
|
res.json({ name, lines: logLines });
|
|
12934
13390
|
} catch (error2) {
|
|
@@ -12963,7 +13419,7 @@ function createProjectPrdRoutes() {
|
|
|
12963
13419
|
|
|
12964
13420
|
// ../server/dist/routes/roadmap.routes.js
|
|
12965
13421
|
init_dist();
|
|
12966
|
-
import * as
|
|
13422
|
+
import * as path33 from "path";
|
|
12967
13423
|
import { Router as Router7 } from "express";
|
|
12968
13424
|
function createRoadmapRouteHandlers(ctx) {
|
|
12969
13425
|
const router = Router7({ mergeParams: true });
|
|
@@ -12973,7 +13429,7 @@ function createRoadmapRouteHandlers(ctx) {
|
|
|
12973
13429
|
const config = ctx.getConfig(req);
|
|
12974
13430
|
const projectDir = ctx.getProjectDir(req);
|
|
12975
13431
|
const status = getRoadmapStatus(projectDir, config);
|
|
12976
|
-
const prdDir =
|
|
13432
|
+
const prdDir = path33.join(projectDir, config.prdDir);
|
|
12977
13433
|
const state = loadRoadmapState(prdDir);
|
|
12978
13434
|
res.json({
|
|
12979
13435
|
...status,
|
|
@@ -13097,11 +13553,13 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
|
|
|
13097
13553
|
const qaPlan = getSchedulingPlan(projectDir, config, "qa");
|
|
13098
13554
|
const auditPlan = getSchedulingPlan(projectDir, config, "audit");
|
|
13099
13555
|
const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
|
|
13556
|
+
const analyticsPlan = getSchedulingPlan(projectDir, config, "analytics");
|
|
13100
13557
|
const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
|
|
13101
13558
|
const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
|
|
13102
13559
|
const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
|
|
13103
13560
|
const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
|
|
13104
13561
|
const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
|
|
13562
|
+
const analyticsInstalled = installed && config.analytics.enabled && hasScheduledCommand(entries, "analytics");
|
|
13105
13563
|
return {
|
|
13106
13564
|
executor: {
|
|
13107
13565
|
schedule: config.cronSchedule,
|
|
@@ -13143,6 +13601,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
|
|
|
13143
13601
|
manualDelayMinutes: plannerPlan.manualDelayMinutes,
|
|
13144
13602
|
balancedDelayMinutes: plannerPlan.balancedDelayMinutes
|
|
13145
13603
|
},
|
|
13604
|
+
analytics: {
|
|
13605
|
+
schedule: config.analytics.schedule,
|
|
13606
|
+
installed: analyticsInstalled,
|
|
13607
|
+
nextRun: analyticsInstalled ? addDelayToIsoString(computeNextRun(config.analytics.schedule), analyticsPlan.totalDelayMinutes) : null,
|
|
13608
|
+
delayMinutes: analyticsPlan.totalDelayMinutes,
|
|
13609
|
+
manualDelayMinutes: analyticsPlan.manualDelayMinutes,
|
|
13610
|
+
balancedDelayMinutes: analyticsPlan.balancedDelayMinutes
|
|
13611
|
+
},
|
|
13146
13612
|
paused: !installed,
|
|
13147
13613
|
schedulingPriority: config.schedulingPriority,
|
|
13148
13614
|
entries
|
|
@@ -13297,14 +13763,14 @@ function createQueueRoutes(deps) {
|
|
|
13297
13763
|
var __filename2 = fileURLToPath3(import.meta.url);
|
|
13298
13764
|
var __dirname3 = dirname7(__filename2);
|
|
13299
13765
|
function resolveWebDistPath() {
|
|
13300
|
-
const bundled =
|
|
13301
|
-
if (
|
|
13766
|
+
const bundled = path34.join(__dirname3, "web");
|
|
13767
|
+
if (fs31.existsSync(path34.join(bundled, "index.html")))
|
|
13302
13768
|
return bundled;
|
|
13303
13769
|
let d = __dirname3;
|
|
13304
13770
|
for (let i = 0; i < 8; i++) {
|
|
13305
|
-
if (
|
|
13306
|
-
const dev =
|
|
13307
|
-
if (
|
|
13771
|
+
if (fs31.existsSync(path34.join(d, "turbo.json"))) {
|
|
13772
|
+
const dev = path34.join(d, "web/dist");
|
|
13773
|
+
if (fs31.existsSync(path34.join(dev, "index.html")))
|
|
13308
13774
|
return dev;
|
|
13309
13775
|
break;
|
|
13310
13776
|
}
|
|
@@ -13314,7 +13780,7 @@ function resolveWebDistPath() {
|
|
|
13314
13780
|
}
|
|
13315
13781
|
function setupStaticFiles(app) {
|
|
13316
13782
|
const webDistPath = resolveWebDistPath();
|
|
13317
|
-
if (
|
|
13783
|
+
if (fs31.existsSync(webDistPath)) {
|
|
13318
13784
|
app.use(express.static(webDistPath));
|
|
13319
13785
|
}
|
|
13320
13786
|
app.use((req, res, next) => {
|
|
@@ -13322,8 +13788,8 @@ function setupStaticFiles(app) {
|
|
|
13322
13788
|
next();
|
|
13323
13789
|
return;
|
|
13324
13790
|
}
|
|
13325
|
-
const indexPath =
|
|
13326
|
-
if (
|
|
13791
|
+
const indexPath = path34.resolve(webDistPath, "index.html");
|
|
13792
|
+
if (fs31.existsSync(indexPath)) {
|
|
13327
13793
|
res.sendFile(indexPath, (err) => {
|
|
13328
13794
|
if (err)
|
|
13329
13795
|
next();
|
|
@@ -13439,7 +13905,7 @@ function createGlobalApp() {
|
|
|
13439
13905
|
return app;
|
|
13440
13906
|
}
|
|
13441
13907
|
function bootContainer() {
|
|
13442
|
-
initContainer(
|
|
13908
|
+
initContainer(path34.dirname(getDbPath()));
|
|
13443
13909
|
}
|
|
13444
13910
|
function startServer(projectDir, port) {
|
|
13445
13911
|
bootContainer();
|
|
@@ -13492,8 +13958,8 @@ function isProcessRunning2(pid) {
|
|
|
13492
13958
|
}
|
|
13493
13959
|
function readPid(lockPath) {
|
|
13494
13960
|
try {
|
|
13495
|
-
if (!
|
|
13496
|
-
const raw =
|
|
13961
|
+
if (!fs32.existsSync(lockPath)) return null;
|
|
13962
|
+
const raw = fs32.readFileSync(lockPath, "utf-8").trim();
|
|
13497
13963
|
const pid = parseInt(raw, 10);
|
|
13498
13964
|
return Number.isFinite(pid) ? pid : null;
|
|
13499
13965
|
} catch {
|
|
@@ -13505,10 +13971,10 @@ function acquireServeLock(mode, port) {
|
|
|
13505
13971
|
let stalePidCleaned;
|
|
13506
13972
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
13507
13973
|
try {
|
|
13508
|
-
const fd =
|
|
13509
|
-
|
|
13974
|
+
const fd = fs32.openSync(lockPath, "wx");
|
|
13975
|
+
fs32.writeFileSync(fd, `${process.pid}
|
|
13510
13976
|
`);
|
|
13511
|
-
|
|
13977
|
+
fs32.closeSync(fd);
|
|
13512
13978
|
return { acquired: true, lockPath, stalePidCleaned };
|
|
13513
13979
|
} catch (error2) {
|
|
13514
13980
|
const err = error2;
|
|
@@ -13529,7 +13995,7 @@ function acquireServeLock(mode, port) {
|
|
|
13529
13995
|
};
|
|
13530
13996
|
}
|
|
13531
13997
|
try {
|
|
13532
|
-
|
|
13998
|
+
fs32.unlinkSync(lockPath);
|
|
13533
13999
|
if (existingPid) {
|
|
13534
14000
|
stalePidCleaned = existingPid;
|
|
13535
14001
|
}
|
|
@@ -13552,10 +14018,10 @@ function acquireServeLock(mode, port) {
|
|
|
13552
14018
|
}
|
|
13553
14019
|
function releaseServeLock(lockPath) {
|
|
13554
14020
|
try {
|
|
13555
|
-
if (!
|
|
14021
|
+
if (!fs32.existsSync(lockPath)) return;
|
|
13556
14022
|
const lockPid = readPid(lockPath);
|
|
13557
14023
|
if (lockPid !== null && lockPid !== process.pid) return;
|
|
13558
|
-
|
|
14024
|
+
fs32.unlinkSync(lockPath);
|
|
13559
14025
|
} catch {
|
|
13560
14026
|
}
|
|
13561
14027
|
}
|
|
@@ -13651,14 +14117,14 @@ function historyCommand(program2) {
|
|
|
13651
14117
|
// src/commands/update.ts
|
|
13652
14118
|
init_dist();
|
|
13653
14119
|
import { spawnSync } from "child_process";
|
|
13654
|
-
import * as
|
|
13655
|
-
import * as
|
|
14120
|
+
import * as fs33 from "fs";
|
|
14121
|
+
import * as path35 from "path";
|
|
13656
14122
|
var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
|
|
13657
14123
|
function parseProjectDirs(projects, cwd) {
|
|
13658
14124
|
if (!projects || projects.trim().length === 0) {
|
|
13659
14125
|
return [cwd];
|
|
13660
14126
|
}
|
|
13661
|
-
const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) =>
|
|
14127
|
+
const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path35.resolve(cwd, entry));
|
|
13662
14128
|
return Array.from(new Set(dirs));
|
|
13663
14129
|
}
|
|
13664
14130
|
function shouldInstallGlobal(options) {
|
|
@@ -13700,7 +14166,7 @@ function updateCommand(program2) {
|
|
|
13700
14166
|
}
|
|
13701
14167
|
const nightWatchBin = resolveNightWatchBin();
|
|
13702
14168
|
for (const projectDir of projectDirs) {
|
|
13703
|
-
if (!
|
|
14169
|
+
if (!fs33.existsSync(projectDir) || !fs33.statSync(projectDir).isDirectory()) {
|
|
13704
14170
|
warn(`Skipping invalid project directory: ${projectDir}`);
|
|
13705
14171
|
continue;
|
|
13706
14172
|
}
|
|
@@ -13744,8 +14210,8 @@ function prdStateCommand(program2) {
|
|
|
13744
14210
|
|
|
13745
14211
|
// src/commands/retry.ts
|
|
13746
14212
|
init_dist();
|
|
13747
|
-
import * as
|
|
13748
|
-
import * as
|
|
14213
|
+
import * as fs34 from "fs";
|
|
14214
|
+
import * as path36 from "path";
|
|
13749
14215
|
function normalizePrdName(name) {
|
|
13750
14216
|
if (!name.endsWith(".md")) {
|
|
13751
14217
|
return `${name}.md`;
|
|
@@ -13753,26 +14219,26 @@ function normalizePrdName(name) {
|
|
|
13753
14219
|
return name;
|
|
13754
14220
|
}
|
|
13755
14221
|
function getDonePrds(doneDir) {
|
|
13756
|
-
if (!
|
|
14222
|
+
if (!fs34.existsSync(doneDir)) {
|
|
13757
14223
|
return [];
|
|
13758
14224
|
}
|
|
13759
|
-
return
|
|
14225
|
+
return fs34.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
|
|
13760
14226
|
}
|
|
13761
14227
|
function retryCommand(program2) {
|
|
13762
14228
|
program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
|
|
13763
14229
|
const projectDir = process.cwd();
|
|
13764
14230
|
const config = loadConfig(projectDir);
|
|
13765
|
-
const prdDir =
|
|
13766
|
-
const doneDir =
|
|
14231
|
+
const prdDir = path36.join(projectDir, config.prdDir);
|
|
14232
|
+
const doneDir = path36.join(prdDir, "done");
|
|
13767
14233
|
const normalizedPrdName = normalizePrdName(prdName);
|
|
13768
|
-
const pendingPath =
|
|
13769
|
-
if (
|
|
14234
|
+
const pendingPath = path36.join(prdDir, normalizedPrdName);
|
|
14235
|
+
if (fs34.existsSync(pendingPath)) {
|
|
13770
14236
|
info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
|
|
13771
14237
|
return;
|
|
13772
14238
|
}
|
|
13773
|
-
const donePath =
|
|
13774
|
-
if (
|
|
13775
|
-
|
|
14239
|
+
const donePath = path36.join(doneDir, normalizedPrdName);
|
|
14240
|
+
if (fs34.existsSync(donePath)) {
|
|
14241
|
+
fs34.renameSync(donePath, pendingPath);
|
|
13776
14242
|
success(`Moved "${normalizedPrdName}" back to pending.`);
|
|
13777
14243
|
dim(`From: ${donePath}`);
|
|
13778
14244
|
dim(`To: ${pendingPath}`);
|
|
@@ -14024,7 +14490,7 @@ function prdsCommand(program2) {
|
|
|
14024
14490
|
|
|
14025
14491
|
// src/commands/cancel.ts
|
|
14026
14492
|
init_dist();
|
|
14027
|
-
import * as
|
|
14493
|
+
import * as fs35 from "fs";
|
|
14028
14494
|
import * as readline3 from "readline";
|
|
14029
14495
|
function getLockFilePaths2(projectDir) {
|
|
14030
14496
|
const runtimeKey = projectRuntimeKey(projectDir);
|
|
@@ -14071,7 +14537,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
14071
14537
|
const pid = lockStatus.pid;
|
|
14072
14538
|
if (!lockStatus.running) {
|
|
14073
14539
|
try {
|
|
14074
|
-
|
|
14540
|
+
fs35.unlinkSync(lockPath);
|
|
14075
14541
|
return {
|
|
14076
14542
|
success: true,
|
|
14077
14543
|
message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
|
|
@@ -14109,7 +14575,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
14109
14575
|
await sleep2(3e3);
|
|
14110
14576
|
if (!isProcessRunning3(pid)) {
|
|
14111
14577
|
try {
|
|
14112
|
-
|
|
14578
|
+
fs35.unlinkSync(lockPath);
|
|
14113
14579
|
} catch {
|
|
14114
14580
|
}
|
|
14115
14581
|
return {
|
|
@@ -14144,7 +14610,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
14144
14610
|
await sleep2(500);
|
|
14145
14611
|
if (!isProcessRunning3(pid)) {
|
|
14146
14612
|
try {
|
|
14147
|
-
|
|
14613
|
+
fs35.unlinkSync(lockPath);
|
|
14148
14614
|
} catch {
|
|
14149
14615
|
}
|
|
14150
14616
|
return {
|
|
@@ -14205,31 +14671,31 @@ function cancelCommand(program2) {
|
|
|
14205
14671
|
|
|
14206
14672
|
// src/commands/slice.ts
|
|
14207
14673
|
init_dist();
|
|
14208
|
-
import * as
|
|
14209
|
-
import * as
|
|
14674
|
+
import * as fs36 from "fs";
|
|
14675
|
+
import * as path37 from "path";
|
|
14210
14676
|
function plannerLockPath2(projectDir) {
|
|
14211
14677
|
return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
|
|
14212
14678
|
}
|
|
14213
14679
|
function acquirePlannerLock(projectDir) {
|
|
14214
14680
|
const lockFile = plannerLockPath2(projectDir);
|
|
14215
|
-
if (
|
|
14216
|
-
const pidRaw =
|
|
14681
|
+
if (fs36.existsSync(lockFile)) {
|
|
14682
|
+
const pidRaw = fs36.readFileSync(lockFile, "utf-8").trim();
|
|
14217
14683
|
const pid = parseInt(pidRaw, 10);
|
|
14218
14684
|
if (!Number.isNaN(pid) && isProcessRunning(pid)) {
|
|
14219
14685
|
return { acquired: false, lockFile, pid };
|
|
14220
14686
|
}
|
|
14221
14687
|
try {
|
|
14222
|
-
|
|
14688
|
+
fs36.unlinkSync(lockFile);
|
|
14223
14689
|
} catch {
|
|
14224
14690
|
}
|
|
14225
14691
|
}
|
|
14226
|
-
|
|
14692
|
+
fs36.writeFileSync(lockFile, String(process.pid));
|
|
14227
14693
|
return { acquired: true, lockFile };
|
|
14228
14694
|
}
|
|
14229
14695
|
function releasePlannerLock(lockFile) {
|
|
14230
14696
|
try {
|
|
14231
|
-
if (
|
|
14232
|
-
|
|
14697
|
+
if (fs36.existsSync(lockFile)) {
|
|
14698
|
+
fs36.unlinkSync(lockFile);
|
|
14233
14699
|
}
|
|
14234
14700
|
} catch {
|
|
14235
14701
|
}
|
|
@@ -14238,12 +14704,12 @@ function resolvePlannerIssueColumn(config) {
|
|
|
14238
14704
|
return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
|
|
14239
14705
|
}
|
|
14240
14706
|
function buildPlannerIssueBody(projectDir, config, result) {
|
|
14241
|
-
const relativePrdPath =
|
|
14242
|
-
const absolutePrdPath =
|
|
14707
|
+
const relativePrdPath = path37.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
|
|
14708
|
+
const absolutePrdPath = path37.join(projectDir, config.prdDir, result.file ?? "");
|
|
14243
14709
|
const sourceItem = result.item;
|
|
14244
14710
|
let prdContent;
|
|
14245
14711
|
try {
|
|
14246
|
-
prdContent =
|
|
14712
|
+
prdContent = fs36.readFileSync(absolutePrdPath, "utf-8");
|
|
14247
14713
|
} catch {
|
|
14248
14714
|
prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
|
|
14249
14715
|
}
|
|
@@ -14419,7 +14885,7 @@ function sliceCommand(program2) {
|
|
|
14419
14885
|
if (!options.dryRun) {
|
|
14420
14886
|
await sendNotifications(config, {
|
|
14421
14887
|
event: "run_started",
|
|
14422
|
-
projectName:
|
|
14888
|
+
projectName: path37.basename(projectDir),
|
|
14423
14889
|
exitCode: 0,
|
|
14424
14890
|
provider: config.provider
|
|
14425
14891
|
});
|
|
@@ -14454,7 +14920,7 @@ function sliceCommand(program2) {
|
|
|
14454
14920
|
if (!options.dryRun && result.sliced) {
|
|
14455
14921
|
await sendNotifications(config, {
|
|
14456
14922
|
event: "run_succeeded",
|
|
14457
|
-
projectName:
|
|
14923
|
+
projectName: path37.basename(projectDir),
|
|
14458
14924
|
exitCode,
|
|
14459
14925
|
provider: config.provider,
|
|
14460
14926
|
prTitle: result.item?.title
|
|
@@ -14462,7 +14928,7 @@ function sliceCommand(program2) {
|
|
|
14462
14928
|
} else if (!options.dryRun && !nothingPending) {
|
|
14463
14929
|
await sendNotifications(config, {
|
|
14464
14930
|
event: "run_failed",
|
|
14465
|
-
projectName:
|
|
14931
|
+
projectName: path37.basename(projectDir),
|
|
14466
14932
|
exitCode,
|
|
14467
14933
|
provider: config.provider
|
|
14468
14934
|
});
|
|
@@ -14478,21 +14944,21 @@ function sliceCommand(program2) {
|
|
|
14478
14944
|
|
|
14479
14945
|
// src/commands/state.ts
|
|
14480
14946
|
init_dist();
|
|
14481
|
-
import * as
|
|
14482
|
-
import * as
|
|
14947
|
+
import * as os8 from "os";
|
|
14948
|
+
import * as path38 from "path";
|
|
14483
14949
|
import chalk5 from "chalk";
|
|
14484
14950
|
import { Command } from "commander";
|
|
14485
14951
|
function createStateCommand() {
|
|
14486
14952
|
const state = new Command("state");
|
|
14487
14953
|
state.description("Manage Night Watch state");
|
|
14488
14954
|
state.command("migrate").description("Migrate legacy JSON state files to SQLite").option("--dry-run", "Show what would be migrated without making changes").action((opts) => {
|
|
14489
|
-
const nightWatchHome = process.env.NIGHT_WATCH_HOME ||
|
|
14955
|
+
const nightWatchHome = process.env.NIGHT_WATCH_HOME || path38.join(os8.homedir(), GLOBAL_CONFIG_DIR);
|
|
14490
14956
|
if (opts.dryRun) {
|
|
14491
14957
|
console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
|
|
14492
14958
|
console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
|
|
14493
|
-
console.log(` ${
|
|
14494
|
-
console.log(` ${
|
|
14495
|
-
console.log(` ${
|
|
14959
|
+
console.log(` ${path38.join(nightWatchHome, "projects.json")}`);
|
|
14960
|
+
console.log(` ${path38.join(nightWatchHome, "history.json")}`);
|
|
14961
|
+
console.log(` ${path38.join(nightWatchHome, "prd-states.json")}`);
|
|
14496
14962
|
console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
|
|
14497
14963
|
console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
|
|
14498
14964
|
return;
|
|
@@ -14530,8 +14996,8 @@ function createStateCommand() {
|
|
|
14530
14996
|
init_dist();
|
|
14531
14997
|
init_dist();
|
|
14532
14998
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
14533
|
-
import * as
|
|
14534
|
-
import * as
|
|
14999
|
+
import * as fs37 from "fs";
|
|
15000
|
+
import * as path39 from "path";
|
|
14535
15001
|
import * as readline4 from "readline";
|
|
14536
15002
|
import chalk6 from "chalk";
|
|
14537
15003
|
async function run(fn) {
|
|
@@ -14553,7 +15019,7 @@ function getProvider(config, cwd) {
|
|
|
14553
15019
|
return createBoardProvider(bp, cwd);
|
|
14554
15020
|
}
|
|
14555
15021
|
function defaultBoardTitle(cwd) {
|
|
14556
|
-
return `${
|
|
15022
|
+
return `${path39.basename(cwd)} Night Watch`;
|
|
14557
15023
|
}
|
|
14558
15024
|
async function ensureBoardConfigured(config, cwd, provider, options) {
|
|
14559
15025
|
if (config.boardProvider?.projectNumber) {
|
|
@@ -14752,11 +15218,11 @@ function boardCommand(program2) {
|
|
|
14752
15218
|
let body = options.body ?? "";
|
|
14753
15219
|
if (options.bodyFile) {
|
|
14754
15220
|
const filePath = options.bodyFile;
|
|
14755
|
-
if (!
|
|
15221
|
+
if (!fs37.existsSync(filePath)) {
|
|
14756
15222
|
console.error(`File not found: ${filePath}`);
|
|
14757
15223
|
process.exit(1);
|
|
14758
15224
|
}
|
|
14759
|
-
body =
|
|
15225
|
+
body = fs37.readFileSync(filePath, "utf-8");
|
|
14760
15226
|
}
|
|
14761
15227
|
const labels = [];
|
|
14762
15228
|
if (options.label) {
|
|
@@ -14978,12 +15444,12 @@ function boardCommand(program2) {
|
|
|
14978
15444
|
const config = loadConfig(cwd);
|
|
14979
15445
|
const provider = getProvider(config, cwd);
|
|
14980
15446
|
await ensureBoardConfigured(config, cwd, provider);
|
|
14981
|
-
const roadmapPath = options.roadmap ??
|
|
14982
|
-
if (!
|
|
15447
|
+
const roadmapPath = options.roadmap ?? path39.join(cwd, "ROADMAP.md");
|
|
15448
|
+
if (!fs37.existsSync(roadmapPath)) {
|
|
14983
15449
|
console.error(`Roadmap file not found: ${roadmapPath}`);
|
|
14984
15450
|
process.exit(1);
|
|
14985
15451
|
}
|
|
14986
|
-
const roadmapContent =
|
|
15452
|
+
const roadmapContent = fs37.readFileSync(roadmapPath, "utf-8");
|
|
14987
15453
|
const items = parseRoadmap(roadmapContent);
|
|
14988
15454
|
const uncheckedItems = getUncheckedItems(items);
|
|
14989
15455
|
if (uncheckedItems.length === 0) {
|
|
@@ -15107,11 +15573,11 @@ function boardCommand(program2) {
|
|
|
15107
15573
|
// src/commands/queue.ts
|
|
15108
15574
|
init_dist();
|
|
15109
15575
|
init_dist();
|
|
15110
|
-
import * as
|
|
15576
|
+
import * as path40 from "path";
|
|
15111
15577
|
import { spawn as spawn6 } from "child_process";
|
|
15112
15578
|
import chalk7 from "chalk";
|
|
15113
15579
|
import { Command as Command2 } from "commander";
|
|
15114
|
-
var
|
|
15580
|
+
var logger4 = createLogger("queue");
|
|
15115
15581
|
var VALID_JOB_TYPES2 = ["executor", "reviewer", "qa", "audit", "slicer"];
|
|
15116
15582
|
function formatTimestamp(unixTs) {
|
|
15117
15583
|
if (unixTs === null) return "-";
|
|
@@ -15227,7 +15693,7 @@ function createQueueCommand() {
|
|
|
15227
15693
|
process.exit(1);
|
|
15228
15694
|
}
|
|
15229
15695
|
}
|
|
15230
|
-
const projectName =
|
|
15696
|
+
const projectName = path40.basename(projectDir);
|
|
15231
15697
|
const queueConfig = loadConfig(projectDir).queue;
|
|
15232
15698
|
const id = enqueueJob(projectDir, projectName, jobType, envVars, queueConfig);
|
|
15233
15699
|
console.log(chalk7.green(`Enqueued ${jobType} for ${projectName} (ID: ${id})`));
|
|
@@ -15235,13 +15701,13 @@ function createQueueCommand() {
|
|
|
15235
15701
|
queue.command("dispatch").description("Dispatch the next pending job (used by cron scripts)").option("--log <file>", "Log file to write dispatch output").action((_opts) => {
|
|
15236
15702
|
const entry = dispatchNextJob(loadConfig(process.cwd()).queue);
|
|
15237
15703
|
if (!entry) {
|
|
15238
|
-
|
|
15704
|
+
logger4.info("No pending jobs to dispatch");
|
|
15239
15705
|
return;
|
|
15240
15706
|
}
|
|
15241
|
-
|
|
15707
|
+
logger4.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
|
|
15242
15708
|
const scriptName = getScriptNameForJobType(entry.jobType);
|
|
15243
15709
|
if (!scriptName) {
|
|
15244
|
-
|
|
15710
|
+
logger4.error(`Unknown job type: ${entry.jobType}`);
|
|
15245
15711
|
return;
|
|
15246
15712
|
}
|
|
15247
15713
|
let projectEnv;
|
|
@@ -15260,7 +15726,7 @@ function createQueueCommand() {
|
|
|
15260
15726
|
NW_QUEUE_ENTRY_ID: String(entry.id)
|
|
15261
15727
|
};
|
|
15262
15728
|
const scriptPath = getScriptPath(scriptName);
|
|
15263
|
-
|
|
15729
|
+
logger4.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
|
|
15264
15730
|
try {
|
|
15265
15731
|
const child = spawn6("bash", [scriptPath, entry.projectPath], {
|
|
15266
15732
|
detached: true,
|
|
@@ -15269,11 +15735,11 @@ function createQueueCommand() {
|
|
|
15269
15735
|
cwd: entry.projectPath
|
|
15270
15736
|
});
|
|
15271
15737
|
child.unref();
|
|
15272
|
-
|
|
15738
|
+
logger4.info(`Spawned PID: ${child.pid}`);
|
|
15273
15739
|
markJobRunning(entry.id);
|
|
15274
15740
|
} catch (error2) {
|
|
15275
15741
|
updateJobStatus(entry.id, "pending");
|
|
15276
|
-
|
|
15742
|
+
logger4.error(
|
|
15277
15743
|
`Failed to dispatch ${entry.jobType} for ${entry.projectName}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
15278
15744
|
);
|
|
15279
15745
|
process.exit(1);
|
|
@@ -15387,13 +15853,13 @@ var __dirname4 = dirname8(__filename3);
|
|
|
15387
15853
|
function findPackageRoot(dir) {
|
|
15388
15854
|
let d = dir;
|
|
15389
15855
|
for (let i = 0; i < 5; i++) {
|
|
15390
|
-
if (existsSync29(
|
|
15856
|
+
if (existsSync29(join35(d, "package.json"))) return d;
|
|
15391
15857
|
d = dirname8(d);
|
|
15392
15858
|
}
|
|
15393
15859
|
return dir;
|
|
15394
15860
|
}
|
|
15395
15861
|
var packageRoot = findPackageRoot(__dirname4);
|
|
15396
|
-
var packageJson = JSON.parse(readFileSync18(
|
|
15862
|
+
var packageJson = JSON.parse(readFileSync18(join35(packageRoot, "package.json"), "utf-8"));
|
|
15397
15863
|
var program = new Command3();
|
|
15398
15864
|
program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
|
|
15399
15865
|
initCommand(program);
|
|
@@ -15401,6 +15867,7 @@ runCommand(program);
|
|
|
15401
15867
|
reviewCommand(program);
|
|
15402
15868
|
qaCommand(program);
|
|
15403
15869
|
auditCommand(program);
|
|
15870
|
+
analyticsCommand(program);
|
|
15404
15871
|
installCommand(program);
|
|
15405
15872
|
uninstallCommand(program);
|
|
15406
15873
|
statusCommand(program);
|