@jonit-dev/night-watch-cli 1.8.1 → 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 +701 -286
- 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 +1 -0
- 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/web/assets/index-B_l_3wnA.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-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_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, 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";
|
|
@@ -109,10 +109,36 @@ var init_constants = __esm({
|
|
|
109
109
|
schedule: DEFAULT_AUDIT_SCHEDULE,
|
|
110
110
|
maxRuntime: DEFAULT_AUDIT_MAX_RUNTIME
|
|
111
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
|
+
};
|
|
112
130
|
AUDIT_LOG_NAME = "audit";
|
|
113
131
|
PLANNER_LOG_NAME = "slicer";
|
|
132
|
+
ANALYTICS_LOG_NAME = "analytics";
|
|
114
133
|
VALID_PROVIDERS = ["claude", "codex"];
|
|
115
|
-
VALID_JOB_TYPES = [
|
|
134
|
+
VALID_JOB_TYPES = [
|
|
135
|
+
"executor",
|
|
136
|
+
"reviewer",
|
|
137
|
+
"qa",
|
|
138
|
+
"audit",
|
|
139
|
+
"slicer",
|
|
140
|
+
"analytics"
|
|
141
|
+
];
|
|
116
142
|
DEFAULT_JOB_PROVIDERS = {};
|
|
117
143
|
BUILT_IN_PRESETS = {
|
|
118
144
|
claude: {
|
|
@@ -191,7 +217,8 @@ var init_constants = __esm({
|
|
|
191
217
|
reviewer: REVIEWER_LOG_NAME,
|
|
192
218
|
qa: QA_LOG_NAME,
|
|
193
219
|
audit: AUDIT_LOG_NAME,
|
|
194
|
-
planner: PLANNER_LOG_NAME
|
|
220
|
+
planner: PLANNER_LOG_NAME,
|
|
221
|
+
analytics: ANALYTICS_LOG_NAME
|
|
195
222
|
};
|
|
196
223
|
GLOBAL_CONFIG_DIR = ".night-watch";
|
|
197
224
|
REGISTRY_FILE_NAME = "projects.json";
|
|
@@ -208,7 +235,8 @@ var init_constants = __esm({
|
|
|
208
235
|
reviewer: 40,
|
|
209
236
|
slicer: 30,
|
|
210
237
|
qa: 20,
|
|
211
|
-
audit: 10
|
|
238
|
+
audit: 10,
|
|
239
|
+
analytics: 10
|
|
212
240
|
};
|
|
213
241
|
DEFAULT_QUEUE = {
|
|
214
242
|
enabled: DEFAULT_QUEUE_ENABLED,
|
|
@@ -223,6 +251,15 @@ var init_constants = __esm({
|
|
|
223
251
|
}
|
|
224
252
|
});
|
|
225
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
|
+
|
|
226
263
|
// ../core/dist/config-normalize.js
|
|
227
264
|
function validateProvider(value) {
|
|
228
265
|
const trimmed = value.trim();
|
|
@@ -418,6 +455,20 @@ function normalizeConfig(rawConfig) {
|
|
|
418
455
|
};
|
|
419
456
|
normalized.audit = audit;
|
|
420
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
|
+
}
|
|
421
472
|
const rawJobProviders = readObject(rawConfig.jobProviders);
|
|
422
473
|
if (rawJobProviders) {
|
|
423
474
|
const jobProviders = {};
|
|
@@ -477,6 +528,7 @@ function normalizeConfig(rawConfig) {
|
|
|
477
528
|
var init_config_normalize = __esm({
|
|
478
529
|
"../core/dist/config-normalize.js"() {
|
|
479
530
|
"use strict";
|
|
531
|
+
init_types2();
|
|
480
532
|
init_constants();
|
|
481
533
|
}
|
|
482
534
|
});
|
|
@@ -700,6 +752,25 @@ function buildEnvOverrideConfig(fileConfig) {
|
|
|
700
752
|
if (!isNaN(v) && v > 0)
|
|
701
753
|
env.audit = { ...auditBase(), maxRuntime: v };
|
|
702
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
|
+
}
|
|
703
774
|
const jobProvidersEnv = {};
|
|
704
775
|
for (const jobType of VALID_JOB_TYPES) {
|
|
705
776
|
const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
|
|
@@ -792,6 +863,7 @@ function getDefaultConfig() {
|
|
|
792
863
|
claudeModel: DEFAULT_CLAUDE_MODEL,
|
|
793
864
|
qa: { ...DEFAULT_QA },
|
|
794
865
|
audit: { ...DEFAULT_AUDIT },
|
|
866
|
+
analytics: { ...DEFAULT_ANALYTICS },
|
|
795
867
|
jobProviders: { ...DEFAULT_JOB_PROVIDERS },
|
|
796
868
|
queue: { ...DEFAULT_QUEUE }
|
|
797
869
|
};
|
|
@@ -858,7 +930,7 @@ function mergeConfigLayer(base, layer) {
|
|
|
858
930
|
...layerQueue,
|
|
859
931
|
providerBuckets: { ...baseQueue.providerBuckets, ...layerQueue.providerBuckets }
|
|
860
932
|
};
|
|
861
|
-
} else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit") {
|
|
933
|
+
} else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics") {
|
|
862
934
|
base[_key] = {
|
|
863
935
|
...base[_key],
|
|
864
936
|
...value
|
|
@@ -943,15 +1015,6 @@ var init_config = __esm({
|
|
|
943
1015
|
}
|
|
944
1016
|
});
|
|
945
1017
|
|
|
946
|
-
// ../core/dist/board/types.js
|
|
947
|
-
var BOARD_COLUMNS;
|
|
948
|
-
var init_types2 = __esm({
|
|
949
|
-
"../core/dist/board/types.js"() {
|
|
950
|
-
"use strict";
|
|
951
|
-
BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
|
|
955
1018
|
// ../core/dist/storage/repositories/sqlite/execution-history.repository.js
|
|
956
1019
|
import Database from "better-sqlite3";
|
|
957
1020
|
import { inject, injectable } from "tsyringe";
|
|
@@ -3046,6 +3109,9 @@ function auditLockPath(projectDir) {
|
|
|
3046
3109
|
function plannerLockPath(projectDir) {
|
|
3047
3110
|
return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
|
|
3048
3111
|
}
|
|
3112
|
+
function analyticsLockPath(projectDir) {
|
|
3113
|
+
return `${LOCK_FILE_PREFIX}analytics-${projectRuntimeKey(projectDir)}.lock`;
|
|
3114
|
+
}
|
|
3049
3115
|
function isProcessRunning(pid) {
|
|
3050
3116
|
try {
|
|
3051
3117
|
process.kill(pid, 0);
|
|
@@ -3404,7 +3470,8 @@ function collectLogInfo(projectDir) {
|
|
|
3404
3470
|
{ name: "reviewer", fileName: "reviewer.log" },
|
|
3405
3471
|
{ name: "qa", fileName: `${QA_LOG_NAME}.log` },
|
|
3406
3472
|
{ name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
|
|
3407
|
-
{ name: "planner", fileName: `${PLANNER_LOG_NAME}.log` }
|
|
3473
|
+
{ name: "planner", fileName: `${PLANNER_LOG_NAME}.log` },
|
|
3474
|
+
{ name: "analytics", fileName: `${ANALYTICS_LOG_NAME}.log` }
|
|
3408
3475
|
];
|
|
3409
3476
|
return logEntries.map(({ name, fileName }) => {
|
|
3410
3477
|
const logPath = path6.join(projectDir, LOG_DIR, fileName);
|
|
@@ -3433,12 +3500,14 @@ async function fetchStatusSnapshot(projectDir, config) {
|
|
|
3433
3500
|
const qaLock = checkLockFile(qaLockPath(projectDir));
|
|
3434
3501
|
const auditLock = checkLockFile(auditLockPath(projectDir));
|
|
3435
3502
|
const plannerLock = checkLockFile(plannerLockPath(projectDir));
|
|
3503
|
+
const analyticsLock = checkLockFile(analyticsLockPath(projectDir));
|
|
3436
3504
|
const processes = [
|
|
3437
3505
|
{ name: "executor", running: executorLock.running, pid: executorLock.pid },
|
|
3438
3506
|
{ name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
|
|
3439
3507
|
{ name: "qa", running: qaLock.running, pid: qaLock.pid },
|
|
3440
3508
|
{ name: "audit", running: auditLock.running, pid: auditLock.pid },
|
|
3441
|
-
{ 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 }
|
|
3442
3511
|
];
|
|
3443
3512
|
const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
|
|
3444
3513
|
const prs = await collectPrInfo(projectDir, config.branchPatterns);
|
|
@@ -5628,6 +5697,8 @@ function isJobTypeEnabled(config, jobType) {
|
|
|
5628
5697
|
return config.audit.enabled;
|
|
5629
5698
|
case "slicer":
|
|
5630
5699
|
return config.roadmapScanner.enabled;
|
|
5700
|
+
case "analytics":
|
|
5701
|
+
return config.analytics.enabled;
|
|
5631
5702
|
default:
|
|
5632
5703
|
return true;
|
|
5633
5704
|
}
|
|
@@ -5987,6 +6058,8 @@ function getLockPathForJob(projectPath, jobType) {
|
|
|
5987
6058
|
return auditLockPath(projectPath);
|
|
5988
6059
|
case "slicer":
|
|
5989
6060
|
return plannerLockPath(projectPath);
|
|
6061
|
+
case "analytics":
|
|
6062
|
+
return analyticsLockPath(projectPath);
|
|
5990
6063
|
}
|
|
5991
6064
|
}
|
|
5992
6065
|
function reconcileStaleRunningJobs(db) {
|
|
@@ -6166,7 +6239,10 @@ function fitsProviderCapacity(candidate, config, inFlightByBucket) {
|
|
|
6166
6239
|
}
|
|
6167
6240
|
const bucketConfig = config?.providerBuckets?.[bucketKey];
|
|
6168
6241
|
if (!bucketConfig) {
|
|
6169
|
-
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
|
+
});
|
|
6170
6246
|
return true;
|
|
6171
6247
|
}
|
|
6172
6248
|
const inFlightCount = inFlightByBucket[bucketKey] ?? 0;
|
|
@@ -6197,7 +6273,10 @@ function dispatchNextJob(config) {
|
|
|
6197
6273
|
const runningCount = running?.count ?? 0;
|
|
6198
6274
|
logger.debug("Dispatch attempt", { mode, runningCount, maxConcurrency });
|
|
6199
6275
|
if (runningCount >= maxConcurrency) {
|
|
6200
|
-
logger.info("Dispatch skipped: global concurrency limit reached", {
|
|
6276
|
+
logger.info("Dispatch skipped: global concurrency limit reached", {
|
|
6277
|
+
runningCount,
|
|
6278
|
+
maxConcurrency
|
|
6279
|
+
});
|
|
6201
6280
|
return null;
|
|
6202
6281
|
}
|
|
6203
6282
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -6223,7 +6302,9 @@ function dispatchNextJob(config) {
|
|
|
6223
6302
|
logger.debug("Dispatch skipped: no pending jobs");
|
|
6224
6303
|
return null;
|
|
6225
6304
|
}
|
|
6226
|
-
logger.debug("Provider-aware dispatch: evaluating candidates", {
|
|
6305
|
+
logger.debug("Provider-aware dispatch: evaluating candidates", {
|
|
6306
|
+
candidateCount: candidates.length
|
|
6307
|
+
});
|
|
6227
6308
|
const inFlightByBucket = getInFlightCountByBucket(db);
|
|
6228
6309
|
for (const candidate of candidates) {
|
|
6229
6310
|
if (fitsProviderCapacity(candidate, config, inFlightByBucket)) {
|
|
@@ -6299,7 +6380,11 @@ function clearQueue(filter, force) {
|
|
|
6299
6380
|
} else {
|
|
6300
6381
|
result = db.prepare(`DELETE FROM job_queue WHERE status IN ${statuses}`).run();
|
|
6301
6382
|
}
|
|
6302
|
-
logger.info("Queue cleared", {
|
|
6383
|
+
logger.info("Queue cleared", {
|
|
6384
|
+
count: result.changes,
|
|
6385
|
+
filter: filter ?? "all",
|
|
6386
|
+
force: force ?? false
|
|
6387
|
+
});
|
|
6303
6388
|
return result.changes;
|
|
6304
6389
|
} finally {
|
|
6305
6390
|
db.close();
|
|
@@ -6432,6 +6517,192 @@ var init_job_queue = __esm({
|
|
|
6432
6517
|
}
|
|
6433
6518
|
});
|
|
6434
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
|
+
|
|
6435
6706
|
// ../core/dist/templates/prd-template.js
|
|
6436
6707
|
function renderDependsOn(deps) {
|
|
6437
6708
|
if (deps.length === 0) {
|
|
@@ -6608,6 +6879,7 @@ sequenceDiagram
|
|
|
6608
6879
|
// ../core/dist/index.js
|
|
6609
6880
|
var dist_exports = {};
|
|
6610
6881
|
__export(dist_exports, {
|
|
6882
|
+
ANALYTICS_LOG_NAME: () => ANALYTICS_LOG_NAME,
|
|
6611
6883
|
AUDIT_LOG_NAME: () => AUDIT_LOG_NAME,
|
|
6612
6884
|
BOARD_COLUMNS: () => BOARD_COLUMNS,
|
|
6613
6885
|
BUILT_IN_PRESETS: () => BUILT_IN_PRESETS,
|
|
@@ -6619,6 +6891,13 @@ __export(dist_exports, {
|
|
|
6619
6891
|
CONFIG_FILE_NAME: () => CONFIG_FILE_NAME,
|
|
6620
6892
|
CRONTAB_MARKER_PREFIX: () => CRONTAB_MARKER_PREFIX,
|
|
6621
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,
|
|
6622
6901
|
DEFAULT_AUDIT: () => DEFAULT_AUDIT,
|
|
6623
6902
|
DEFAULT_AUDIT_ENABLED: () => DEFAULT_AUDIT_ENABLED,
|
|
6624
6903
|
DEFAULT_AUDIT_MAX_RUNTIME: () => DEFAULT_AUDIT_MAX_RUNTIME,
|
|
@@ -6705,6 +6984,7 @@ __export(dist_exports, {
|
|
|
6705
6984
|
acquireLock: () => acquireLock,
|
|
6706
6985
|
addDelayToIsoString: () => addDelayToIsoString,
|
|
6707
6986
|
addEntry: () => addEntry,
|
|
6987
|
+
analyticsLockPath: () => analyticsLockPath,
|
|
6708
6988
|
auditLockPath: () => auditLockPath,
|
|
6709
6989
|
buildDescription: () => buildDescription,
|
|
6710
6990
|
calculateStringSimilarity: () => calculateStringSimilarity,
|
|
@@ -6755,6 +7035,7 @@ __export(dist_exports, {
|
|
|
6755
7035
|
extractPriority: () => extractPriority,
|
|
6756
7036
|
extractQaScreenshotUrls: () => extractQaScreenshotUrls,
|
|
6757
7037
|
extractSummary: () => extractSummary,
|
|
7038
|
+
fetchAmplitudeData: () => fetchAmplitudeData,
|
|
6758
7039
|
fetchLatestQaCommentBody: () => fetchLatestQaCommentBody,
|
|
6759
7040
|
fetchPrDetails: () => fetchPrDetails,
|
|
6760
7041
|
fetchPrDetailsByNumber: () => fetchPrDetailsByNumber,
|
|
@@ -6862,6 +7143,7 @@ __export(dist_exports, {
|
|
|
6862
7143
|
reviewerLockPath: () => reviewerLockPath,
|
|
6863
7144
|
rotateLog: () => rotateLog,
|
|
6864
7145
|
runAllChecks: () => runAllChecks,
|
|
7146
|
+
runAnalytics: () => runAnalytics,
|
|
6865
7147
|
runMigrations: () => runMigrations,
|
|
6866
7148
|
saveConfig: () => saveConfig,
|
|
6867
7149
|
saveHistory: () => saveHistory,
|
|
@@ -6930,6 +7212,7 @@ var init_dist = __esm({
|
|
|
6930
7212
|
init_webhook_validator();
|
|
6931
7213
|
init_worktree_manager();
|
|
6932
7214
|
init_job_queue();
|
|
7215
|
+
init_analytics();
|
|
6933
7216
|
init_prd_template();
|
|
6934
7217
|
init_slicer_prompt();
|
|
6935
7218
|
}
|
|
@@ -6940,37 +7223,37 @@ import "reflect-metadata";
|
|
|
6940
7223
|
import { Command as Command3 } from "commander";
|
|
6941
7224
|
import { existsSync as existsSync29, readFileSync as readFileSync18 } from "fs";
|
|
6942
7225
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
6943
|
-
import { dirname as dirname8, join as
|
|
7226
|
+
import { dirname as dirname8, join as join35 } from "path";
|
|
6944
7227
|
|
|
6945
7228
|
// src/commands/init.ts
|
|
6946
7229
|
init_dist();
|
|
6947
|
-
import
|
|
6948
|
-
import
|
|
7230
|
+
import fs19 from "fs";
|
|
7231
|
+
import path19 from "path";
|
|
6949
7232
|
import { execSync as execSync3 } from "child_process";
|
|
6950
7233
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6951
|
-
import { dirname as dirname4, join as
|
|
7234
|
+
import { dirname as dirname4, join as join17 } from "path";
|
|
6952
7235
|
import * as readline from "readline";
|
|
6953
7236
|
var __filename = fileURLToPath2(import.meta.url);
|
|
6954
7237
|
var __dirname2 = dirname4(__filename);
|
|
6955
7238
|
function findTemplatesDir(startDir) {
|
|
6956
7239
|
let d = startDir;
|
|
6957
7240
|
for (let i = 0; i < 8; i++) {
|
|
6958
|
-
const candidate =
|
|
6959
|
-
if (
|
|
7241
|
+
const candidate = join17(d, "templates");
|
|
7242
|
+
if (fs19.existsSync(candidate) && fs19.statSync(candidate).isDirectory()) {
|
|
6960
7243
|
return candidate;
|
|
6961
7244
|
}
|
|
6962
7245
|
d = dirname4(d);
|
|
6963
7246
|
}
|
|
6964
|
-
return
|
|
7247
|
+
return join17(startDir, "templates");
|
|
6965
7248
|
}
|
|
6966
7249
|
var TEMPLATES_DIR = findTemplatesDir(__dirname2);
|
|
6967
7250
|
function hasPlaywrightDependency(cwd) {
|
|
6968
|
-
const packageJsonPath =
|
|
6969
|
-
if (!
|
|
7251
|
+
const packageJsonPath = path19.join(cwd, "package.json");
|
|
7252
|
+
if (!fs19.existsSync(packageJsonPath)) {
|
|
6970
7253
|
return false;
|
|
6971
7254
|
}
|
|
6972
7255
|
try {
|
|
6973
|
-
const packageJson2 = JSON.parse(
|
|
7256
|
+
const packageJson2 = JSON.parse(fs19.readFileSync(packageJsonPath, "utf-8"));
|
|
6974
7257
|
return Boolean(
|
|
6975
7258
|
packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright
|
|
6976
7259
|
);
|
|
@@ -6982,7 +7265,7 @@ function detectPlaywright(cwd) {
|
|
|
6982
7265
|
if (hasPlaywrightDependency(cwd)) {
|
|
6983
7266
|
return true;
|
|
6984
7267
|
}
|
|
6985
|
-
if (
|
|
7268
|
+
if (fs19.existsSync(path19.join(cwd, "node_modules", ".bin", "playwright"))) {
|
|
6986
7269
|
return true;
|
|
6987
7270
|
}
|
|
6988
7271
|
try {
|
|
@@ -6998,10 +7281,10 @@ function detectPlaywright(cwd) {
|
|
|
6998
7281
|
}
|
|
6999
7282
|
}
|
|
7000
7283
|
function resolvePlaywrightInstallCommand(cwd) {
|
|
7001
|
-
if (
|
|
7284
|
+
if (fs19.existsSync(path19.join(cwd, "pnpm-lock.yaml"))) {
|
|
7002
7285
|
return "pnpm add -D @playwright/test";
|
|
7003
7286
|
}
|
|
7004
|
-
if (
|
|
7287
|
+
if (fs19.existsSync(path19.join(cwd, "yarn.lock"))) {
|
|
7005
7288
|
return "yarn add -D @playwright/test";
|
|
7006
7289
|
}
|
|
7007
7290
|
return "npm install -D @playwright/test";
|
|
@@ -7147,8 +7430,8 @@ function promptProviderSelection(providers) {
|
|
|
7147
7430
|
});
|
|
7148
7431
|
}
|
|
7149
7432
|
function ensureDir(dirPath) {
|
|
7150
|
-
if (!
|
|
7151
|
-
|
|
7433
|
+
if (!fs19.existsSync(dirPath)) {
|
|
7434
|
+
fs19.mkdirSync(dirPath, { recursive: true });
|
|
7152
7435
|
}
|
|
7153
7436
|
}
|
|
7154
7437
|
function buildInitConfig(params) {
|
|
@@ -7195,6 +7478,7 @@ function buildInitConfig(params) {
|
|
|
7195
7478
|
branchPatterns: [...defaults.qa.branchPatterns]
|
|
7196
7479
|
},
|
|
7197
7480
|
audit: { ...defaults.audit },
|
|
7481
|
+
analytics: { ...defaults.analytics },
|
|
7198
7482
|
jobProviders: { ...defaults.jobProviders },
|
|
7199
7483
|
queue: {
|
|
7200
7484
|
...defaults.queue,
|
|
@@ -7204,30 +7488,30 @@ function buildInitConfig(params) {
|
|
|
7204
7488
|
}
|
|
7205
7489
|
function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
|
|
7206
7490
|
if (customTemplatesDir !== null) {
|
|
7207
|
-
const customPath =
|
|
7208
|
-
if (
|
|
7491
|
+
const customPath = join17(customTemplatesDir, templateName);
|
|
7492
|
+
if (fs19.existsSync(customPath)) {
|
|
7209
7493
|
return { path: customPath, source: "custom" };
|
|
7210
7494
|
}
|
|
7211
7495
|
}
|
|
7212
|
-
return { path:
|
|
7496
|
+
return { path: join17(bundledTemplatesDir, templateName), source: "bundled" };
|
|
7213
7497
|
}
|
|
7214
7498
|
function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
|
|
7215
|
-
if (
|
|
7499
|
+
if (fs19.existsSync(targetPath) && !force) {
|
|
7216
7500
|
console.log(` Skipped (exists): ${targetPath}`);
|
|
7217
7501
|
return { created: false, source: source ?? "bundled" };
|
|
7218
7502
|
}
|
|
7219
|
-
const templatePath = sourcePath ??
|
|
7503
|
+
const templatePath = sourcePath ?? join17(TEMPLATES_DIR, templateName);
|
|
7220
7504
|
const resolvedSource = source ?? "bundled";
|
|
7221
|
-
let content =
|
|
7505
|
+
let content = fs19.readFileSync(templatePath, "utf-8");
|
|
7222
7506
|
for (const [key, value] of Object.entries(replacements)) {
|
|
7223
7507
|
content = content.replaceAll(key, value);
|
|
7224
7508
|
}
|
|
7225
|
-
|
|
7509
|
+
fs19.writeFileSync(targetPath, content);
|
|
7226
7510
|
console.log(` Created: ${targetPath} (${resolvedSource})`);
|
|
7227
7511
|
return { created: true, source: resolvedSource };
|
|
7228
7512
|
}
|
|
7229
7513
|
function addToGitignore(cwd) {
|
|
7230
|
-
const gitignorePath =
|
|
7514
|
+
const gitignorePath = path19.join(cwd, ".gitignore");
|
|
7231
7515
|
const entries = [
|
|
7232
7516
|
{
|
|
7233
7517
|
pattern: "/logs/",
|
|
@@ -7241,13 +7525,13 @@ function addToGitignore(cwd) {
|
|
|
7241
7525
|
},
|
|
7242
7526
|
{ pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
|
|
7243
7527
|
];
|
|
7244
|
-
if (!
|
|
7528
|
+
if (!fs19.existsSync(gitignorePath)) {
|
|
7245
7529
|
const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
|
|
7246
|
-
|
|
7530
|
+
fs19.writeFileSync(gitignorePath, lines.join("\n"));
|
|
7247
7531
|
console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
|
|
7248
7532
|
return;
|
|
7249
7533
|
}
|
|
7250
|
-
const content =
|
|
7534
|
+
const content = fs19.readFileSync(gitignorePath, "utf-8");
|
|
7251
7535
|
const missing = entries.filter((e) => !e.check(content));
|
|
7252
7536
|
if (missing.length === 0) {
|
|
7253
7537
|
console.log(` Skipped (exists): Night Watch entries in .gitignore`);
|
|
@@ -7255,7 +7539,7 @@ function addToGitignore(cwd) {
|
|
|
7255
7539
|
}
|
|
7256
7540
|
const additions = missing.map((e) => e.pattern).join("\n");
|
|
7257
7541
|
const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
|
|
7258
|
-
|
|
7542
|
+
fs19.writeFileSync(gitignorePath, newContent);
|
|
7259
7543
|
console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
|
|
7260
7544
|
}
|
|
7261
7545
|
function initCommand(program2) {
|
|
@@ -7376,28 +7660,28 @@ function initCommand(program2) {
|
|
|
7376
7660
|
"${DEFAULT_BRANCH}": defaultBranch
|
|
7377
7661
|
};
|
|
7378
7662
|
step(6, totalSteps, "Creating PRD directory structure...");
|
|
7379
|
-
const prdDirPath =
|
|
7380
|
-
const doneDirPath =
|
|
7663
|
+
const prdDirPath = path19.join(cwd, prdDir);
|
|
7664
|
+
const doneDirPath = path19.join(prdDirPath, "done");
|
|
7381
7665
|
ensureDir(doneDirPath);
|
|
7382
7666
|
success(`Created ${prdDirPath}/`);
|
|
7383
7667
|
success(`Created ${doneDirPath}/`);
|
|
7384
7668
|
step(7, totalSteps, "Creating logs directory...");
|
|
7385
|
-
const logsPath =
|
|
7669
|
+
const logsPath = path19.join(cwd, LOG_DIR);
|
|
7386
7670
|
ensureDir(logsPath);
|
|
7387
7671
|
success(`Created ${logsPath}/`);
|
|
7388
7672
|
addToGitignore(cwd);
|
|
7389
7673
|
step(8, totalSteps, "Creating instructions directory...");
|
|
7390
|
-
const instructionsDir =
|
|
7674
|
+
const instructionsDir = path19.join(cwd, "instructions");
|
|
7391
7675
|
ensureDir(instructionsDir);
|
|
7392
7676
|
success(`Created ${instructionsDir}/`);
|
|
7393
7677
|
const existingConfig = loadConfig(cwd);
|
|
7394
|
-
const customTemplatesDirPath =
|
|
7395
|
-
const customTemplatesDir =
|
|
7678
|
+
const customTemplatesDirPath = path19.join(cwd, existingConfig.templatesDir);
|
|
7679
|
+
const customTemplatesDir = fs19.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
|
|
7396
7680
|
const templateSources = [];
|
|
7397
7681
|
const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7398
7682
|
const nwResult = processTemplate(
|
|
7399
7683
|
"executor.md",
|
|
7400
|
-
|
|
7684
|
+
path19.join(instructionsDir, "executor.md"),
|
|
7401
7685
|
replacements,
|
|
7402
7686
|
force,
|
|
7403
7687
|
nwResolution.path,
|
|
@@ -7411,7 +7695,7 @@ function initCommand(program2) {
|
|
|
7411
7695
|
);
|
|
7412
7696
|
const peResult = processTemplate(
|
|
7413
7697
|
"prd-executor.md",
|
|
7414
|
-
|
|
7698
|
+
path19.join(instructionsDir, "prd-executor.md"),
|
|
7415
7699
|
replacements,
|
|
7416
7700
|
force,
|
|
7417
7701
|
peResolution.path,
|
|
@@ -7421,7 +7705,7 @@ function initCommand(program2) {
|
|
|
7421
7705
|
const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7422
7706
|
const prResult = processTemplate(
|
|
7423
7707
|
"pr-reviewer.md",
|
|
7424
|
-
|
|
7708
|
+
path19.join(instructionsDir, "pr-reviewer.md"),
|
|
7425
7709
|
replacements,
|
|
7426
7710
|
force,
|
|
7427
7711
|
prResolution.path,
|
|
@@ -7431,7 +7715,7 @@ function initCommand(program2) {
|
|
|
7431
7715
|
const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7432
7716
|
const qaResult = processTemplate(
|
|
7433
7717
|
"qa.md",
|
|
7434
|
-
|
|
7718
|
+
path19.join(instructionsDir, "qa.md"),
|
|
7435
7719
|
replacements,
|
|
7436
7720
|
force,
|
|
7437
7721
|
qaResolution.path,
|
|
@@ -7441,7 +7725,7 @@ function initCommand(program2) {
|
|
|
7441
7725
|
const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7442
7726
|
const auditResult = processTemplate(
|
|
7443
7727
|
"audit.md",
|
|
7444
|
-
|
|
7728
|
+
path19.join(instructionsDir, "audit.md"),
|
|
7445
7729
|
replacements,
|
|
7446
7730
|
force,
|
|
7447
7731
|
auditResolution.path,
|
|
@@ -7449,8 +7733,8 @@ function initCommand(program2) {
|
|
|
7449
7733
|
);
|
|
7450
7734
|
templateSources.push({ name: "audit.md", source: auditResult.source });
|
|
7451
7735
|
step(9, totalSteps, "Creating configuration file...");
|
|
7452
|
-
const configPath =
|
|
7453
|
-
if (
|
|
7736
|
+
const configPath = path19.join(cwd, CONFIG_FILE_NAME);
|
|
7737
|
+
if (fs19.existsSync(configPath) && !force) {
|
|
7454
7738
|
console.log(` Skipped (exists): ${configPath}`);
|
|
7455
7739
|
} else {
|
|
7456
7740
|
const config = buildInitConfig({
|
|
@@ -7460,11 +7744,11 @@ function initCommand(program2) {
|
|
|
7460
7744
|
reviewerEnabled,
|
|
7461
7745
|
prdDir
|
|
7462
7746
|
});
|
|
7463
|
-
|
|
7747
|
+
fs19.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
7464
7748
|
success(`Created ${configPath}`);
|
|
7465
7749
|
}
|
|
7466
7750
|
step(10, totalSteps, "Setting up GitHub Project board...");
|
|
7467
|
-
const existingRaw = JSON.parse(
|
|
7751
|
+
const existingRaw = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
|
|
7468
7752
|
const existingBoard = existingRaw.boardProvider;
|
|
7469
7753
|
let boardSetupStatus = "Skipped";
|
|
7470
7754
|
if (existingBoard?.projectNumber && !force) {
|
|
@@ -7486,13 +7770,13 @@ function initCommand(program2) {
|
|
|
7486
7770
|
const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
|
|
7487
7771
|
const boardTitle = `${projectName} Night Watch`;
|
|
7488
7772
|
const board = await provider.setupBoard(boardTitle);
|
|
7489
|
-
const rawConfig = JSON.parse(
|
|
7773
|
+
const rawConfig = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
|
|
7490
7774
|
rawConfig.boardProvider = {
|
|
7491
7775
|
enabled: true,
|
|
7492
7776
|
provider: "github",
|
|
7493
7777
|
projectNumber: board.number
|
|
7494
7778
|
};
|
|
7495
|
-
|
|
7779
|
+
fs19.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
|
|
7496
7780
|
boardSetupStatus = `Created (#${board.number})`;
|
|
7497
7781
|
success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
|
|
7498
7782
|
} catch (boardErr) {
|
|
@@ -7616,8 +7900,8 @@ function getTelegramStatusWebhooks(config) {
|
|
|
7616
7900
|
}
|
|
7617
7901
|
|
|
7618
7902
|
// src/commands/run.ts
|
|
7619
|
-
import * as
|
|
7620
|
-
import * as
|
|
7903
|
+
import * as fs20 from "fs";
|
|
7904
|
+
import * as path20 from "path";
|
|
7621
7905
|
function resolveRunNotificationEvent(exitCode, scriptStatus) {
|
|
7622
7906
|
if (exitCode === 124) {
|
|
7623
7907
|
return "run_timeout";
|
|
@@ -7649,12 +7933,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
|
|
|
7649
7933
|
return scriptStatus === "skip_no_eligible_prd";
|
|
7650
7934
|
}
|
|
7651
7935
|
function getCrossProjectFallbackCandidates(currentProjectDir) {
|
|
7652
|
-
const current =
|
|
7936
|
+
const current = path20.resolve(currentProjectDir);
|
|
7653
7937
|
const { valid, invalid } = validateRegistry();
|
|
7654
7938
|
for (const entry of invalid) {
|
|
7655
7939
|
warn(`Skipping invalid registry entry: ${entry.path}`);
|
|
7656
7940
|
}
|
|
7657
|
-
return valid.filter((entry) =>
|
|
7941
|
+
return valid.filter((entry) => path20.resolve(entry.path) !== current);
|
|
7658
7942
|
}
|
|
7659
7943
|
async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
|
|
7660
7944
|
if (isRateLimitFallbackTriggered(scriptResult?.data)) {
|
|
@@ -7664,7 +7948,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
|
|
|
7664
7948
|
if (nonTelegramWebhooks.length > 0) {
|
|
7665
7949
|
const _rateLimitCtx = {
|
|
7666
7950
|
event: "rate_limit_fallback",
|
|
7667
|
-
projectName:
|
|
7951
|
+
projectName: path20.basename(projectDir),
|
|
7668
7952
|
exitCode,
|
|
7669
7953
|
provider: config.provider
|
|
7670
7954
|
};
|
|
@@ -7692,7 +7976,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
|
|
|
7692
7976
|
const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
|
|
7693
7977
|
const _ctx = {
|
|
7694
7978
|
event,
|
|
7695
|
-
projectName:
|
|
7979
|
+
projectName: path20.basename(projectDir),
|
|
7696
7980
|
exitCode,
|
|
7697
7981
|
provider: config.provider,
|
|
7698
7982
|
prdName: scriptResult?.data.prd,
|
|
@@ -7854,20 +8138,20 @@ function applyCliOverrides(config, options) {
|
|
|
7854
8138
|
return overridden;
|
|
7855
8139
|
}
|
|
7856
8140
|
function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
|
|
7857
|
-
const absolutePrdDir =
|
|
7858
|
-
const doneDir =
|
|
8141
|
+
const absolutePrdDir = path20.join(projectDir, prdDir);
|
|
8142
|
+
const doneDir = path20.join(absolutePrdDir, "done");
|
|
7859
8143
|
const pending = [];
|
|
7860
8144
|
const completed = [];
|
|
7861
|
-
if (
|
|
7862
|
-
const entries =
|
|
8145
|
+
if (fs20.existsSync(absolutePrdDir)) {
|
|
8146
|
+
const entries = fs20.readdirSync(absolutePrdDir, { withFileTypes: true });
|
|
7863
8147
|
for (const entry of entries) {
|
|
7864
8148
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7865
|
-
const claimPath =
|
|
8149
|
+
const claimPath = path20.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
|
|
7866
8150
|
let claimed = false;
|
|
7867
8151
|
let claimInfo = null;
|
|
7868
|
-
if (
|
|
8152
|
+
if (fs20.existsSync(claimPath)) {
|
|
7869
8153
|
try {
|
|
7870
|
-
const content =
|
|
8154
|
+
const content = fs20.readFileSync(claimPath, "utf-8");
|
|
7871
8155
|
const data = JSON.parse(content);
|
|
7872
8156
|
const age = Math.floor(Date.now() / 1e3) - data.timestamp;
|
|
7873
8157
|
if (age < maxRuntime) {
|
|
@@ -7881,8 +8165,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
|
|
|
7881
8165
|
}
|
|
7882
8166
|
}
|
|
7883
8167
|
}
|
|
7884
|
-
if (
|
|
7885
|
-
const entries =
|
|
8168
|
+
if (fs20.existsSync(doneDir)) {
|
|
8169
|
+
const entries = fs20.readdirSync(doneDir, { withFileTypes: true });
|
|
7886
8170
|
for (const entry of entries) {
|
|
7887
8171
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7888
8172
|
completed.push(entry.name);
|
|
@@ -8042,7 +8326,7 @@ ${stderr}`);
|
|
|
8042
8326
|
// src/commands/review.ts
|
|
8043
8327
|
init_dist();
|
|
8044
8328
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
8045
|
-
import * as
|
|
8329
|
+
import * as path21 from "path";
|
|
8046
8330
|
function shouldSendReviewNotification(scriptStatus) {
|
|
8047
8331
|
if (!scriptStatus) {
|
|
8048
8332
|
return true;
|
|
@@ -8282,7 +8566,7 @@ ${stderr}`);
|
|
|
8282
8566
|
const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
|
|
8283
8567
|
const _reviewCtx = {
|
|
8284
8568
|
event: "review_completed",
|
|
8285
|
-
projectName:
|
|
8569
|
+
projectName: path21.basename(projectDir),
|
|
8286
8570
|
exitCode,
|
|
8287
8571
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
8288
8572
|
prUrl: prDetails?.url,
|
|
@@ -8303,7 +8587,7 @@ ${stderr}`);
|
|
|
8303
8587
|
const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
|
|
8304
8588
|
const _mergeCtx = {
|
|
8305
8589
|
event: "pr_auto_merged",
|
|
8306
|
-
projectName:
|
|
8590
|
+
projectName: path21.basename(projectDir),
|
|
8307
8591
|
exitCode,
|
|
8308
8592
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
8309
8593
|
prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
|
|
@@ -8328,7 +8612,7 @@ ${stderr}`);
|
|
|
8328
8612
|
|
|
8329
8613
|
// src/commands/qa.ts
|
|
8330
8614
|
init_dist();
|
|
8331
|
-
import * as
|
|
8615
|
+
import * as path22 from "path";
|
|
8332
8616
|
function shouldSendQaNotification(scriptStatus) {
|
|
8333
8617
|
if (!scriptStatus) {
|
|
8334
8618
|
return true;
|
|
@@ -8462,7 +8746,7 @@ ${stderr}`);
|
|
|
8462
8746
|
const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
|
|
8463
8747
|
const _qaCtx = {
|
|
8464
8748
|
event: "qa_completed",
|
|
8465
|
-
projectName:
|
|
8749
|
+
projectName: path22.basename(projectDir),
|
|
8466
8750
|
exitCode,
|
|
8467
8751
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
8468
8752
|
prNumber: prDetails?.number ?? primaryQaPr,
|
|
@@ -8488,8 +8772,8 @@ ${stderr}`);
|
|
|
8488
8772
|
|
|
8489
8773
|
// src/commands/audit.ts
|
|
8490
8774
|
init_dist();
|
|
8491
|
-
import * as
|
|
8492
|
-
import * as
|
|
8775
|
+
import * as fs21 from "fs";
|
|
8776
|
+
import * as path23 from "path";
|
|
8493
8777
|
function buildEnvVars4(config, options) {
|
|
8494
8778
|
const env = buildBaseEnvVars(config, "audit", options.dryRun);
|
|
8495
8779
|
env.NW_AUDIT_MAX_RUNTIME = String(config.audit.maxRuntime);
|
|
@@ -8532,7 +8816,7 @@ function auditCommand(program2) {
|
|
|
8532
8816
|
configTable.push(["Provider", auditProvider]);
|
|
8533
8817
|
configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
|
|
8534
8818
|
configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
|
|
8535
|
-
configTable.push(["Report File",
|
|
8819
|
+
configTable.push(["Report File", path23.join(projectDir, "logs", "audit-report.md")]);
|
|
8536
8820
|
console.log(configTable.toString());
|
|
8537
8821
|
header("Provider Invocation");
|
|
8538
8822
|
const providerCmd = PROVIDER_COMMANDS[auditProvider];
|
|
@@ -8567,8 +8851,8 @@ ${stderr}`);
|
|
|
8567
8851
|
} else if (scriptResult?.status?.startsWith("skip_")) {
|
|
8568
8852
|
spinner.succeed("Code audit skipped");
|
|
8569
8853
|
} else {
|
|
8570
|
-
const reportPath =
|
|
8571
|
-
if (!
|
|
8854
|
+
const reportPath = path23.join(projectDir, "logs", "audit-report.md");
|
|
8855
|
+
if (!fs21.existsSync(reportPath)) {
|
|
8572
8856
|
spinner.fail("Code audit finished without a report file");
|
|
8573
8857
|
process.exit(1);
|
|
8574
8858
|
}
|
|
@@ -8579,9 +8863,9 @@ ${stderr}`);
|
|
|
8579
8863
|
const providerExit = scriptResult?.data?.provider_exit;
|
|
8580
8864
|
const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
|
|
8581
8865
|
spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
|
|
8582
|
-
const logPath =
|
|
8583
|
-
if (
|
|
8584
|
-
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);
|
|
8585
8869
|
if (logLines.length > 0) {
|
|
8586
8870
|
process.stderr.write(logLines.join("\n") + "\n");
|
|
8587
8871
|
}
|
|
@@ -8595,19 +8879,80 @@ ${stderr}`);
|
|
|
8595
8879
|
});
|
|
8596
8880
|
}
|
|
8597
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
|
+
|
|
8598
8943
|
// src/commands/install.ts
|
|
8599
8944
|
init_dist();
|
|
8600
8945
|
import { execSync as execSync4 } from "child_process";
|
|
8601
|
-
import * as
|
|
8602
|
-
import * as
|
|
8946
|
+
import * as path24 from "path";
|
|
8947
|
+
import * as fs22 from "fs";
|
|
8603
8948
|
function shellQuote(value) {
|
|
8604
8949
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
8605
8950
|
}
|
|
8606
8951
|
function getNightWatchBinPath() {
|
|
8607
8952
|
try {
|
|
8608
8953
|
const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
|
|
8609
|
-
const binPath =
|
|
8610
|
-
if (
|
|
8954
|
+
const binPath = path24.join(npmBin, "night-watch");
|
|
8955
|
+
if (fs22.existsSync(binPath)) {
|
|
8611
8956
|
return binPath;
|
|
8612
8957
|
}
|
|
8613
8958
|
} catch {
|
|
@@ -8620,17 +8965,17 @@ function getNightWatchBinPath() {
|
|
|
8620
8965
|
}
|
|
8621
8966
|
function getNodeBinDir() {
|
|
8622
8967
|
if (process.execPath && process.execPath !== "node") {
|
|
8623
|
-
return
|
|
8968
|
+
return path24.dirname(process.execPath);
|
|
8624
8969
|
}
|
|
8625
8970
|
try {
|
|
8626
8971
|
const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
|
|
8627
|
-
return
|
|
8972
|
+
return path24.dirname(nodePath);
|
|
8628
8973
|
} catch {
|
|
8629
8974
|
return "";
|
|
8630
8975
|
}
|
|
8631
8976
|
}
|
|
8632
8977
|
function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
|
|
8633
|
-
const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ?
|
|
8978
|
+
const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path24.dirname(nightWatchBin) : "";
|
|
8634
8979
|
const pathParts = Array.from(
|
|
8635
8980
|
new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0))
|
|
8636
8981
|
);
|
|
@@ -8646,12 +8991,12 @@ function performInstall(projectDir, config, options) {
|
|
|
8646
8991
|
const nightWatchBin = getNightWatchBinPath();
|
|
8647
8992
|
const projectName = getProjectName(projectDir);
|
|
8648
8993
|
const marker = generateMarker(projectName);
|
|
8649
|
-
const logDir =
|
|
8650
|
-
if (!
|
|
8651
|
-
|
|
8994
|
+
const logDir = path24.join(projectDir, LOG_DIR);
|
|
8995
|
+
if (!fs22.existsSync(logDir)) {
|
|
8996
|
+
fs22.mkdirSync(logDir, { recursive: true });
|
|
8652
8997
|
}
|
|
8653
|
-
const executorLog =
|
|
8654
|
-
const reviewerLog =
|
|
8998
|
+
const executorLog = path24.join(logDir, "executor.log");
|
|
8999
|
+
const reviewerLog = path24.join(logDir, "reviewer.log");
|
|
8655
9000
|
if (!options?.force) {
|
|
8656
9001
|
const existingEntries2 = Array.from(
|
|
8657
9002
|
/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
|
|
@@ -8688,7 +9033,7 @@ function performInstall(projectDir, config, options) {
|
|
|
8688
9033
|
const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
|
|
8689
9034
|
if (installSlicer) {
|
|
8690
9035
|
const slicerSchedule = config.roadmapScanner.slicerSchedule;
|
|
8691
|
-
const slicerLog =
|
|
9036
|
+
const slicerLog = path24.join(logDir, "slicer.log");
|
|
8692
9037
|
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
8693
9038
|
entries.push(slicerEntry);
|
|
8694
9039
|
}
|
|
@@ -8696,7 +9041,7 @@ function performInstall(projectDir, config, options) {
|
|
|
8696
9041
|
const installQa = disableQa ? false : config.qa.enabled;
|
|
8697
9042
|
if (installQa) {
|
|
8698
9043
|
const qaSchedule = config.qa.schedule;
|
|
8699
|
-
const qaLog =
|
|
9044
|
+
const qaLog = path24.join(logDir, "qa.log");
|
|
8700
9045
|
const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
|
|
8701
9046
|
entries.push(qaEntry);
|
|
8702
9047
|
}
|
|
@@ -8704,10 +9049,18 @@ function performInstall(projectDir, config, options) {
|
|
|
8704
9049
|
const installAudit = disableAudit ? false : config.audit.enabled;
|
|
8705
9050
|
if (installAudit) {
|
|
8706
9051
|
const auditSchedule = config.audit.schedule;
|
|
8707
|
-
const auditLog =
|
|
9052
|
+
const auditLog = path24.join(logDir, "audit.log");
|
|
8708
9053
|
const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
|
|
8709
9054
|
entries.push(auditEntry);
|
|
8710
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
|
+
}
|
|
8711
9064
|
const existingEntries = new Set(
|
|
8712
9065
|
Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]))
|
|
8713
9066
|
);
|
|
@@ -8726,7 +9079,7 @@ function performInstall(projectDir, config, options) {
|
|
|
8726
9079
|
}
|
|
8727
9080
|
}
|
|
8728
9081
|
function installCommand(program2) {
|
|
8729
|
-
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) => {
|
|
8730
9083
|
try {
|
|
8731
9084
|
const projectDir = process.cwd();
|
|
8732
9085
|
const config = loadConfig(projectDir);
|
|
@@ -8735,12 +9088,12 @@ function installCommand(program2) {
|
|
|
8735
9088
|
const nightWatchBin = getNightWatchBinPath();
|
|
8736
9089
|
const projectName = getProjectName(projectDir);
|
|
8737
9090
|
const marker = generateMarker(projectName);
|
|
8738
|
-
const logDir =
|
|
8739
|
-
if (!
|
|
8740
|
-
|
|
9091
|
+
const logDir = path24.join(projectDir, LOG_DIR);
|
|
9092
|
+
if (!fs22.existsSync(logDir)) {
|
|
9093
|
+
fs22.mkdirSync(logDir, { recursive: true });
|
|
8741
9094
|
}
|
|
8742
|
-
const executorLog =
|
|
8743
|
-
const reviewerLog =
|
|
9095
|
+
const executorLog = path24.join(logDir, "executor.log");
|
|
9096
|
+
const reviewerLog = path24.join(logDir, "reviewer.log");
|
|
8744
9097
|
const existingEntries = Array.from(
|
|
8745
9098
|
/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
|
|
8746
9099
|
);
|
|
@@ -8776,7 +9129,7 @@ function installCommand(program2) {
|
|
|
8776
9129
|
const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
|
|
8777
9130
|
let slicerLog;
|
|
8778
9131
|
if (installSlicer) {
|
|
8779
|
-
slicerLog =
|
|
9132
|
+
slicerLog = path24.join(logDir, "slicer.log");
|
|
8780
9133
|
const slicerSchedule = config.roadmapScanner.slicerSchedule;
|
|
8781
9134
|
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
8782
9135
|
entries.push(slicerEntry);
|
|
@@ -8785,7 +9138,7 @@ function installCommand(program2) {
|
|
|
8785
9138
|
const installQa = disableQa ? false : config.qa.enabled;
|
|
8786
9139
|
let qaLog;
|
|
8787
9140
|
if (installQa) {
|
|
8788
|
-
qaLog =
|
|
9141
|
+
qaLog = path24.join(logDir, "qa.log");
|
|
8789
9142
|
const qaSchedule = config.qa.schedule;
|
|
8790
9143
|
const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
|
|
8791
9144
|
entries.push(qaEntry);
|
|
@@ -8794,11 +9147,20 @@ function installCommand(program2) {
|
|
|
8794
9147
|
const installAudit = disableAudit ? false : config.audit.enabled;
|
|
8795
9148
|
let auditLog;
|
|
8796
9149
|
if (installAudit) {
|
|
8797
|
-
auditLog =
|
|
9150
|
+
auditLog = path24.join(logDir, "audit.log");
|
|
8798
9151
|
const auditSchedule = config.audit.schedule;
|
|
8799
9152
|
const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
|
|
8800
9153
|
entries.push(auditEntry);
|
|
8801
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
|
+
}
|
|
8802
9164
|
const existingEntrySet = new Set(existingEntries);
|
|
8803
9165
|
const currentCrontab = readCrontab();
|
|
8804
9166
|
const baseCrontab = options.force ? currentCrontab.filter((line) => !existingEntrySet.has(line) && !line.includes(marker)) : currentCrontab;
|
|
@@ -8825,6 +9187,9 @@ function installCommand(program2) {
|
|
|
8825
9187
|
if (installAudit && auditLog) {
|
|
8826
9188
|
dim(` Audit: ${auditLog}`);
|
|
8827
9189
|
}
|
|
9190
|
+
if (installAnalytics && analyticsLog) {
|
|
9191
|
+
dim(` Analytics: ${analyticsLog}`);
|
|
9192
|
+
}
|
|
8828
9193
|
console.log();
|
|
8829
9194
|
dim("To uninstall, run: night-watch uninstall");
|
|
8830
9195
|
dim("To check status, run: night-watch status");
|
|
@@ -8839,8 +9204,8 @@ function installCommand(program2) {
|
|
|
8839
9204
|
|
|
8840
9205
|
// src/commands/uninstall.ts
|
|
8841
9206
|
init_dist();
|
|
8842
|
-
import * as
|
|
8843
|
-
import * as
|
|
9207
|
+
import * as path25 from "path";
|
|
9208
|
+
import * as fs23 from "fs";
|
|
8844
9209
|
function performUninstall(projectDir, options) {
|
|
8845
9210
|
try {
|
|
8846
9211
|
const projectName = getProjectName(projectDir);
|
|
@@ -8855,19 +9220,19 @@ function performUninstall(projectDir, options) {
|
|
|
8855
9220
|
const removedCount = removeEntriesForProject(projectDir, marker);
|
|
8856
9221
|
unregisterProject(projectDir);
|
|
8857
9222
|
if (!options?.keepLogs) {
|
|
8858
|
-
const logDir =
|
|
8859
|
-
if (
|
|
9223
|
+
const logDir = path25.join(projectDir, "logs");
|
|
9224
|
+
if (fs23.existsSync(logDir)) {
|
|
8860
9225
|
const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
|
|
8861
9226
|
logFiles.forEach((logFile) => {
|
|
8862
|
-
const logPath =
|
|
8863
|
-
if (
|
|
8864
|
-
|
|
9227
|
+
const logPath = path25.join(logDir, logFile);
|
|
9228
|
+
if (fs23.existsSync(logPath)) {
|
|
9229
|
+
fs23.unlinkSync(logPath);
|
|
8865
9230
|
}
|
|
8866
9231
|
});
|
|
8867
9232
|
try {
|
|
8868
|
-
const remainingFiles =
|
|
9233
|
+
const remainingFiles = fs23.readdirSync(logDir);
|
|
8869
9234
|
if (remainingFiles.length === 0) {
|
|
8870
|
-
|
|
9235
|
+
fs23.rmdirSync(logDir);
|
|
8871
9236
|
}
|
|
8872
9237
|
} catch {
|
|
8873
9238
|
}
|
|
@@ -8900,21 +9265,21 @@ function uninstallCommand(program2) {
|
|
|
8900
9265
|
existingEntries.forEach((entry) => dim(` ${entry}`));
|
|
8901
9266
|
const removedCount = removeEntriesForProject(projectDir, marker);
|
|
8902
9267
|
if (!options.keepLogs) {
|
|
8903
|
-
const logDir =
|
|
8904
|
-
if (
|
|
9268
|
+
const logDir = path25.join(projectDir, "logs");
|
|
9269
|
+
if (fs23.existsSync(logDir)) {
|
|
8905
9270
|
const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
|
|
8906
9271
|
let logsRemoved = 0;
|
|
8907
9272
|
logFiles.forEach((logFile) => {
|
|
8908
|
-
const logPath =
|
|
8909
|
-
if (
|
|
8910
|
-
|
|
9273
|
+
const logPath = path25.join(logDir, logFile);
|
|
9274
|
+
if (fs23.existsSync(logPath)) {
|
|
9275
|
+
fs23.unlinkSync(logPath);
|
|
8911
9276
|
logsRemoved++;
|
|
8912
9277
|
}
|
|
8913
9278
|
});
|
|
8914
9279
|
try {
|
|
8915
|
-
const remainingFiles =
|
|
9280
|
+
const remainingFiles = fs23.readdirSync(logDir);
|
|
8916
9281
|
if (remainingFiles.length === 0) {
|
|
8917
|
-
|
|
9282
|
+
fs23.rmdirSync(logDir);
|
|
8918
9283
|
}
|
|
8919
9284
|
} catch {
|
|
8920
9285
|
}
|
|
@@ -9150,14 +9515,14 @@ function statusCommand(program2) {
|
|
|
9150
9515
|
// src/commands/logs.ts
|
|
9151
9516
|
init_dist();
|
|
9152
9517
|
import { spawn as spawn3 } from "child_process";
|
|
9153
|
-
import * as
|
|
9154
|
-
import * as
|
|
9518
|
+
import * as path26 from "path";
|
|
9519
|
+
import * as fs24 from "fs";
|
|
9155
9520
|
function getLastLines(filePath, lineCount) {
|
|
9156
|
-
if (!
|
|
9521
|
+
if (!fs24.existsSync(filePath)) {
|
|
9157
9522
|
return `Log file not found: ${filePath}`;
|
|
9158
9523
|
}
|
|
9159
9524
|
try {
|
|
9160
|
-
const content =
|
|
9525
|
+
const content = fs24.readFileSync(filePath, "utf-8");
|
|
9161
9526
|
const lines = content.trim().split("\n");
|
|
9162
9527
|
return lines.slice(-lineCount).join("\n");
|
|
9163
9528
|
} catch (error2) {
|
|
@@ -9165,7 +9530,7 @@ function getLastLines(filePath, lineCount) {
|
|
|
9165
9530
|
}
|
|
9166
9531
|
}
|
|
9167
9532
|
function followLog(filePath) {
|
|
9168
|
-
if (!
|
|
9533
|
+
if (!fs24.existsSync(filePath)) {
|
|
9169
9534
|
console.log(`Log file not found: ${filePath}`);
|
|
9170
9535
|
console.log("The log file will be created when the first execution runs.");
|
|
9171
9536
|
return;
|
|
@@ -9185,13 +9550,13 @@ function logsCommand(program2) {
|
|
|
9185
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) => {
|
|
9186
9551
|
try {
|
|
9187
9552
|
const projectDir = process.cwd();
|
|
9188
|
-
const logDir =
|
|
9553
|
+
const logDir = path26.join(projectDir, LOG_DIR);
|
|
9189
9554
|
const lineCount = parseInt(options.lines || "50", 10);
|
|
9190
|
-
const executorLog =
|
|
9191
|
-
const reviewerLog =
|
|
9192
|
-
const qaLog =
|
|
9193
|
-
const auditLog =
|
|
9194
|
-
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`);
|
|
9195
9560
|
const logType = options.type?.toLowerCase() || "all";
|
|
9196
9561
|
const showExecutor = logType === "all" || logType === "run" || logType === "executor";
|
|
9197
9562
|
const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
|
|
@@ -9255,15 +9620,15 @@ function logsCommand(program2) {
|
|
|
9255
9620
|
|
|
9256
9621
|
// src/commands/prd.ts
|
|
9257
9622
|
init_dist();
|
|
9258
|
-
import * as
|
|
9259
|
-
import * as
|
|
9623
|
+
import * as fs25 from "fs";
|
|
9624
|
+
import * as path27 from "path";
|
|
9260
9625
|
import * as readline2 from "readline";
|
|
9261
9626
|
function slugify2(name) {
|
|
9262
9627
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
9263
9628
|
}
|
|
9264
9629
|
function getNextPrdNumber2(prdDir) {
|
|
9265
|
-
if (!
|
|
9266
|
-
const files =
|
|
9630
|
+
if (!fs25.existsSync(prdDir)) return 1;
|
|
9631
|
+
const files = fs25.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
|
|
9267
9632
|
const numbers = files.map((f) => {
|
|
9268
9633
|
const match = f.match(/^(\d+)-/);
|
|
9269
9634
|
return match ? parseInt(match[1], 10) : 0;
|
|
@@ -9284,10 +9649,10 @@ function parseDependencies(content) {
|
|
|
9284
9649
|
}
|
|
9285
9650
|
function isClaimActive(claimPath, maxRuntime) {
|
|
9286
9651
|
try {
|
|
9287
|
-
if (!
|
|
9652
|
+
if (!fs25.existsSync(claimPath)) {
|
|
9288
9653
|
return { active: false };
|
|
9289
9654
|
}
|
|
9290
|
-
const content =
|
|
9655
|
+
const content = fs25.readFileSync(claimPath, "utf-8");
|
|
9291
9656
|
const claim = JSON.parse(content);
|
|
9292
9657
|
const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
|
|
9293
9658
|
if (age < maxRuntime) {
|
|
@@ -9303,9 +9668,9 @@ function prdCommand(program2) {
|
|
|
9303
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) => {
|
|
9304
9669
|
const projectDir = process.cwd();
|
|
9305
9670
|
const config = loadConfig(projectDir);
|
|
9306
|
-
const prdDir =
|
|
9307
|
-
if (!
|
|
9308
|
-
|
|
9671
|
+
const prdDir = path27.join(projectDir, config.prdDir);
|
|
9672
|
+
if (!fs25.existsSync(prdDir)) {
|
|
9673
|
+
fs25.mkdirSync(prdDir, { recursive: true });
|
|
9309
9674
|
}
|
|
9310
9675
|
let complexityScore = 5;
|
|
9311
9676
|
let dependsOn = [];
|
|
@@ -9364,20 +9729,20 @@ function prdCommand(program2) {
|
|
|
9364
9729
|
} else {
|
|
9365
9730
|
filename = `${slug}.md`;
|
|
9366
9731
|
}
|
|
9367
|
-
const filePath =
|
|
9368
|
-
if (
|
|
9732
|
+
const filePath = path27.join(prdDir, filename);
|
|
9733
|
+
if (fs25.existsSync(filePath)) {
|
|
9369
9734
|
error(`File already exists: ${filePath}`);
|
|
9370
9735
|
dim("Use a different name or remove the existing file.");
|
|
9371
9736
|
process.exit(1);
|
|
9372
9737
|
}
|
|
9373
9738
|
let customTemplate;
|
|
9374
9739
|
if (options.template) {
|
|
9375
|
-
const templatePath =
|
|
9376
|
-
if (!
|
|
9740
|
+
const templatePath = path27.resolve(options.template);
|
|
9741
|
+
if (!fs25.existsSync(templatePath)) {
|
|
9377
9742
|
error(`Template file not found: ${templatePath}`);
|
|
9378
9743
|
process.exit(1);
|
|
9379
9744
|
}
|
|
9380
|
-
customTemplate =
|
|
9745
|
+
customTemplate = fs25.readFileSync(templatePath, "utf-8");
|
|
9381
9746
|
}
|
|
9382
9747
|
const vars = {
|
|
9383
9748
|
title: name,
|
|
@@ -9388,7 +9753,7 @@ function prdCommand(program2) {
|
|
|
9388
9753
|
phaseCount
|
|
9389
9754
|
};
|
|
9390
9755
|
const content = renderPrdTemplate(vars, customTemplate);
|
|
9391
|
-
|
|
9756
|
+
fs25.writeFileSync(filePath, content, "utf-8");
|
|
9392
9757
|
header("PRD Created");
|
|
9393
9758
|
success(`Created: ${filePath}`);
|
|
9394
9759
|
info(`Title: ${name}`);
|
|
@@ -9400,15 +9765,15 @@ function prdCommand(program2) {
|
|
|
9400
9765
|
prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
|
|
9401
9766
|
const projectDir = process.cwd();
|
|
9402
9767
|
const config = loadConfig(projectDir);
|
|
9403
|
-
const absolutePrdDir =
|
|
9404
|
-
const doneDir =
|
|
9768
|
+
const absolutePrdDir = path27.join(projectDir, config.prdDir);
|
|
9769
|
+
const doneDir = path27.join(absolutePrdDir, "done");
|
|
9405
9770
|
const pending = [];
|
|
9406
|
-
if (
|
|
9407
|
-
const files =
|
|
9771
|
+
if (fs25.existsSync(absolutePrdDir)) {
|
|
9772
|
+
const files = fs25.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
|
|
9408
9773
|
for (const file of files) {
|
|
9409
|
-
const content =
|
|
9774
|
+
const content = fs25.readFileSync(path27.join(absolutePrdDir, file), "utf-8");
|
|
9410
9775
|
const deps = parseDependencies(content);
|
|
9411
|
-
const claimPath =
|
|
9776
|
+
const claimPath = path27.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
|
|
9412
9777
|
const claimStatus = isClaimActive(claimPath, config.maxRuntime);
|
|
9413
9778
|
pending.push({
|
|
9414
9779
|
name: file,
|
|
@@ -9419,10 +9784,10 @@ function prdCommand(program2) {
|
|
|
9419
9784
|
}
|
|
9420
9785
|
}
|
|
9421
9786
|
const done = [];
|
|
9422
|
-
if (
|
|
9423
|
-
const files =
|
|
9787
|
+
if (fs25.existsSync(doneDir)) {
|
|
9788
|
+
const files = fs25.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
|
|
9424
9789
|
for (const file of files) {
|
|
9425
|
-
const content =
|
|
9790
|
+
const content = fs25.readFileSync(path27.join(doneDir, file), "utf-8");
|
|
9426
9791
|
const deps = parseDependencies(content);
|
|
9427
9792
|
done.push({ name: file, dependencies: deps });
|
|
9428
9793
|
}
|
|
@@ -9461,7 +9826,7 @@ import blessed6 from "blessed";
|
|
|
9461
9826
|
// src/commands/dashboard/tab-status.ts
|
|
9462
9827
|
init_dist();
|
|
9463
9828
|
import blessed from "blessed";
|
|
9464
|
-
import * as
|
|
9829
|
+
import * as fs26 from "fs";
|
|
9465
9830
|
function sortPrdsByPriority(prds, priority) {
|
|
9466
9831
|
if (priority.length === 0) return prds;
|
|
9467
9832
|
const priorityMap = /* @__PURE__ */ new Map();
|
|
@@ -9557,7 +9922,7 @@ function renderLogPane(projectDir, logs) {
|
|
|
9557
9922
|
let newestMtime = 0;
|
|
9558
9923
|
for (const log of existingLogs) {
|
|
9559
9924
|
try {
|
|
9560
|
-
const stat =
|
|
9925
|
+
const stat = fs26.statSync(log.path);
|
|
9561
9926
|
if (stat.mtimeMs > newestMtime) {
|
|
9562
9927
|
newestMtime = stat.mtimeMs;
|
|
9563
9928
|
newestLog = log;
|
|
@@ -11212,8 +11577,8 @@ function createActionsTab() {
|
|
|
11212
11577
|
// src/commands/dashboard/tab-logs.ts
|
|
11213
11578
|
init_dist();
|
|
11214
11579
|
import blessed5 from "blessed";
|
|
11215
|
-
import * as
|
|
11216
|
-
import * as
|
|
11580
|
+
import * as fs27 from "fs";
|
|
11581
|
+
import * as path28 from "path";
|
|
11217
11582
|
var LOG_NAMES = ["executor", "reviewer"];
|
|
11218
11583
|
var LOG_LINES = 200;
|
|
11219
11584
|
function createLogsTab() {
|
|
@@ -11254,7 +11619,7 @@ function createLogsTab() {
|
|
|
11254
11619
|
let activeKeyHandlers = [];
|
|
11255
11620
|
let activeCtx = null;
|
|
11256
11621
|
function getLogPath(projectDir, logName) {
|
|
11257
|
-
return
|
|
11622
|
+
return path28.join(projectDir, "logs", `${logName}.log`);
|
|
11258
11623
|
}
|
|
11259
11624
|
function updateSelector() {
|
|
11260
11625
|
const tabs = LOG_NAMES.map((name, idx) => {
|
|
@@ -11268,7 +11633,7 @@ function createLogsTab() {
|
|
|
11268
11633
|
function loadLog(ctx) {
|
|
11269
11634
|
const logName = LOG_NAMES[selectedLogIndex];
|
|
11270
11635
|
const logPath = getLogPath(ctx.projectDir, logName);
|
|
11271
|
-
if (!
|
|
11636
|
+
if (!fs27.existsSync(logPath)) {
|
|
11272
11637
|
logContent.setContent(
|
|
11273
11638
|
`{yellow-fg}No ${logName}.log file found{/yellow-fg}
|
|
11274
11639
|
|
|
@@ -11278,7 +11643,7 @@ Log will appear here once the ${logName} runs.`
|
|
|
11278
11643
|
return;
|
|
11279
11644
|
}
|
|
11280
11645
|
try {
|
|
11281
|
-
const stat =
|
|
11646
|
+
const stat = fs27.statSync(logPath);
|
|
11282
11647
|
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
11283
11648
|
logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
|
|
11284
11649
|
} catch {
|
|
@@ -11784,12 +12149,12 @@ function doctorCommand(program2) {
|
|
|
11784
12149
|
|
|
11785
12150
|
// src/commands/serve.ts
|
|
11786
12151
|
init_dist();
|
|
11787
|
-
import * as
|
|
12152
|
+
import * as fs32 from "fs";
|
|
11788
12153
|
|
|
11789
12154
|
// ../server/dist/index.js
|
|
11790
12155
|
init_dist();
|
|
11791
|
-
import * as
|
|
11792
|
-
import * as
|
|
12156
|
+
import * as fs31 from "fs";
|
|
12157
|
+
import * as path34 from "path";
|
|
11793
12158
|
import { dirname as dirname7 } from "path";
|
|
11794
12159
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
11795
12160
|
import cors from "cors";
|
|
@@ -11874,8 +12239,8 @@ function setupGracefulShutdown(server, beforeClose) {
|
|
|
11874
12239
|
|
|
11875
12240
|
// ../server/dist/middleware/project-resolver.middleware.js
|
|
11876
12241
|
init_dist();
|
|
11877
|
-
import * as
|
|
11878
|
-
import * as
|
|
12242
|
+
import * as fs28 from "fs";
|
|
12243
|
+
import * as path29 from "path";
|
|
11879
12244
|
function resolveProject(req, res, next) {
|
|
11880
12245
|
const projectId = req.params.projectId;
|
|
11881
12246
|
const decodedId = decodeURIComponent(projectId).replace(/~/g, "/");
|
|
@@ -11885,7 +12250,7 @@ function resolveProject(req, res, next) {
|
|
|
11885
12250
|
res.status(404).json({ error: `Project not found: ${decodedId}` });
|
|
11886
12251
|
return;
|
|
11887
12252
|
}
|
|
11888
|
-
if (!
|
|
12253
|
+
if (!fs28.existsSync(entry.path) || !fs28.existsSync(path29.join(entry.path, CONFIG_FILE_NAME))) {
|
|
11889
12254
|
res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
|
|
11890
12255
|
return;
|
|
11891
12256
|
}
|
|
@@ -11930,8 +12295,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
|
|
|
11930
12295
|
|
|
11931
12296
|
// ../server/dist/routes/action.routes.js
|
|
11932
12297
|
init_dist();
|
|
11933
|
-
import * as
|
|
11934
|
-
import * as
|
|
12298
|
+
import * as fs29 from "fs";
|
|
12299
|
+
import * as path30 from "path";
|
|
11935
12300
|
import { execSync as execSync5, spawn as spawn5 } from "child_process";
|
|
11936
12301
|
import { Router } from "express";
|
|
11937
12302
|
|
|
@@ -11969,17 +12334,17 @@ function getBoardProvider(config, projectDir) {
|
|
|
11969
12334
|
function cleanOrphanedClaims(dir) {
|
|
11970
12335
|
let entries;
|
|
11971
12336
|
try {
|
|
11972
|
-
entries =
|
|
12337
|
+
entries = fs29.readdirSync(dir, { withFileTypes: true });
|
|
11973
12338
|
} catch {
|
|
11974
12339
|
return;
|
|
11975
12340
|
}
|
|
11976
12341
|
for (const entry of entries) {
|
|
11977
|
-
const fullPath =
|
|
12342
|
+
const fullPath = path30.join(dir, entry.name);
|
|
11978
12343
|
if (entry.isDirectory() && entry.name !== "done") {
|
|
11979
12344
|
cleanOrphanedClaims(fullPath);
|
|
11980
12345
|
} else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
|
|
11981
12346
|
try {
|
|
11982
|
-
|
|
12347
|
+
fs29.unlinkSync(fullPath);
|
|
11983
12348
|
} catch {
|
|
11984
12349
|
}
|
|
11985
12350
|
}
|
|
@@ -12076,6 +12441,9 @@ function createActionRouteHandlers(ctx) {
|
|
|
12076
12441
|
router.post(`/${p}audit`, (req, res) => {
|
|
12077
12442
|
spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
|
|
12078
12443
|
});
|
|
12444
|
+
router.post(`/${p}analytics`, (req, res) => {
|
|
12445
|
+
spawnAction2(ctx.getProjectDir(req), ["analytics"], req, res);
|
|
12446
|
+
});
|
|
12079
12447
|
router.post(`/${p}planner`, (req, res) => {
|
|
12080
12448
|
spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
|
|
12081
12449
|
});
|
|
@@ -12133,19 +12501,19 @@ function createActionRouteHandlers(ctx) {
|
|
|
12133
12501
|
res.status(400).json({ error: "Invalid PRD name" });
|
|
12134
12502
|
return;
|
|
12135
12503
|
}
|
|
12136
|
-
const prdDir =
|
|
12504
|
+
const prdDir = path30.join(projectDir, config.prdDir);
|
|
12137
12505
|
const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
|
|
12138
|
-
const pendingPath =
|
|
12139
|
-
const donePath =
|
|
12140
|
-
if (
|
|
12506
|
+
const pendingPath = path30.join(prdDir, normalized);
|
|
12507
|
+
const donePath = path30.join(prdDir, "done", normalized);
|
|
12508
|
+
if (fs29.existsSync(pendingPath)) {
|
|
12141
12509
|
res.json({ message: `"${normalized}" is already pending` });
|
|
12142
12510
|
return;
|
|
12143
12511
|
}
|
|
12144
|
-
if (!
|
|
12512
|
+
if (!fs29.existsSync(donePath)) {
|
|
12145
12513
|
res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
|
|
12146
12514
|
return;
|
|
12147
12515
|
}
|
|
12148
|
-
|
|
12516
|
+
fs29.renameSync(donePath, pendingPath);
|
|
12149
12517
|
res.json({ message: `Moved "${normalized}" back to pending` });
|
|
12150
12518
|
} catch (error2) {
|
|
12151
12519
|
res.status(500).json({
|
|
@@ -12163,11 +12531,11 @@ function createActionRouteHandlers(ctx) {
|
|
|
12163
12531
|
res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
|
|
12164
12532
|
return;
|
|
12165
12533
|
}
|
|
12166
|
-
if (
|
|
12167
|
-
|
|
12534
|
+
if (fs29.existsSync(lockPath)) {
|
|
12535
|
+
fs29.unlinkSync(lockPath);
|
|
12168
12536
|
}
|
|
12169
|
-
const prdDir =
|
|
12170
|
-
if (
|
|
12537
|
+
const prdDir = path30.join(projectDir, config.prdDir);
|
|
12538
|
+
if (fs29.existsSync(prdDir)) {
|
|
12171
12539
|
cleanOrphanedClaims(prdDir);
|
|
12172
12540
|
}
|
|
12173
12541
|
broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
|
|
@@ -12363,6 +12731,7 @@ function createBoardRouteHandlers(ctx) {
|
|
|
12363
12731
|
return;
|
|
12364
12732
|
}
|
|
12365
12733
|
await provider.closeIssue(issueNumber);
|
|
12734
|
+
await provider.moveIssue(issueNumber, "Done");
|
|
12366
12735
|
invalidateBoardCache(projectDir);
|
|
12367
12736
|
res.json({ closed: true });
|
|
12368
12737
|
} catch (error2) {
|
|
@@ -12669,6 +13038,34 @@ function validateConfigChanges(changes, currentConfig) {
|
|
|
12669
13038
|
return "audit.maxRuntime must be a number >= 60";
|
|
12670
13039
|
}
|
|
12671
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
|
+
}
|
|
12672
13069
|
if (changes.queue !== void 0) {
|
|
12673
13070
|
if (typeof changes.queue !== "object" || changes.queue === null) {
|
|
12674
13071
|
return "queue must be an object";
|
|
@@ -12687,7 +13084,14 @@ function validateConfigChanges(changes, currentConfig) {
|
|
|
12687
13084
|
if (typeof queue.priority !== "object" || queue.priority === null) {
|
|
12688
13085
|
return "queue.priority must be an object";
|
|
12689
13086
|
}
|
|
12690
|
-
const validQueueJobs = [
|
|
13087
|
+
const validQueueJobs = [
|
|
13088
|
+
"executor",
|
|
13089
|
+
"reviewer",
|
|
13090
|
+
"qa",
|
|
13091
|
+
"audit",
|
|
13092
|
+
"slicer",
|
|
13093
|
+
"analytics"
|
|
13094
|
+
];
|
|
12691
13095
|
for (const [jobType, value] of Object.entries(queue.priority)) {
|
|
12692
13096
|
if (!validQueueJobs.includes(jobType)) {
|
|
12693
13097
|
return `queue.priority contains invalid job type: ${jobType}`;
|
|
@@ -12823,8 +13227,8 @@ function createProjectConfigRoutes() {
|
|
|
12823
13227
|
|
|
12824
13228
|
// ../server/dist/routes/doctor.routes.js
|
|
12825
13229
|
init_dist();
|
|
12826
|
-
import * as
|
|
12827
|
-
import * as
|
|
13230
|
+
import * as fs30 from "fs";
|
|
13231
|
+
import * as path31 from "path";
|
|
12828
13232
|
import { execSync as execSync6 } from "child_process";
|
|
12829
13233
|
import { Router as Router4 } from "express";
|
|
12830
13234
|
function runDoctorChecks(projectDir, config) {
|
|
@@ -12857,7 +13261,7 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12857
13261
|
});
|
|
12858
13262
|
}
|
|
12859
13263
|
try {
|
|
12860
|
-
const projectName =
|
|
13264
|
+
const projectName = path31.basename(projectDir);
|
|
12861
13265
|
const marker = generateMarker(projectName);
|
|
12862
13266
|
const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
|
|
12863
13267
|
if (crontabEntries.length > 0) {
|
|
@@ -12880,8 +13284,8 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12880
13284
|
detail: "Failed to check crontab"
|
|
12881
13285
|
});
|
|
12882
13286
|
}
|
|
12883
|
-
const configPath =
|
|
12884
|
-
if (
|
|
13287
|
+
const configPath = path31.join(projectDir, CONFIG_FILE_NAME);
|
|
13288
|
+
if (fs30.existsSync(configPath)) {
|
|
12885
13289
|
checks.push({ name: "config", status: "pass", detail: "Config file exists" });
|
|
12886
13290
|
} else {
|
|
12887
13291
|
checks.push({
|
|
@@ -12890,9 +13294,9 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12890
13294
|
detail: "Config file not found (using defaults)"
|
|
12891
13295
|
});
|
|
12892
13296
|
}
|
|
12893
|
-
const prdDir =
|
|
12894
|
-
if (
|
|
12895
|
-
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"));
|
|
12896
13300
|
checks.push({
|
|
12897
13301
|
name: "prdDir",
|
|
12898
13302
|
status: "pass",
|
|
@@ -12935,7 +13339,7 @@ function createProjectDoctorRoutes() {
|
|
|
12935
13339
|
|
|
12936
13340
|
// ../server/dist/routes/log.routes.js
|
|
12937
13341
|
init_dist();
|
|
12938
|
-
import * as
|
|
13342
|
+
import * as path32 from "path";
|
|
12939
13343
|
import { Router as Router5 } from "express";
|
|
12940
13344
|
function createLogRoutes(deps) {
|
|
12941
13345
|
const { projectDir } = deps;
|
|
@@ -12943,7 +13347,7 @@ function createLogRoutes(deps) {
|
|
|
12943
13347
|
router.get("/:name", (req, res) => {
|
|
12944
13348
|
try {
|
|
12945
13349
|
const { name } = req.params;
|
|
12946
|
-
const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
|
|
13350
|
+
const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
|
|
12947
13351
|
if (!validNames.includes(name)) {
|
|
12948
13352
|
res.status(400).json({
|
|
12949
13353
|
error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
|
|
@@ -12954,7 +13358,7 @@ function createLogRoutes(deps) {
|
|
|
12954
13358
|
const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
|
|
12955
13359
|
const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
|
|
12956
13360
|
const fileName = LOG_FILE_NAMES[name] || name;
|
|
12957
|
-
const logPath =
|
|
13361
|
+
const logPath = path32.join(projectDir, LOG_DIR, `${fileName}.log`);
|
|
12958
13362
|
const logLines = getLastLogLines(logPath, linesToRead);
|
|
12959
13363
|
res.json({ name, lines: logLines });
|
|
12960
13364
|
} catch (error2) {
|
|
@@ -12969,7 +13373,7 @@ function createProjectLogRoutes() {
|
|
|
12969
13373
|
try {
|
|
12970
13374
|
const projectDir = req.projectDir;
|
|
12971
13375
|
const { name } = req.params;
|
|
12972
|
-
const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
|
|
13376
|
+
const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
|
|
12973
13377
|
if (!validNames.includes(name)) {
|
|
12974
13378
|
res.status(400).json({
|
|
12975
13379
|
error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
|
|
@@ -12980,7 +13384,7 @@ function createProjectLogRoutes() {
|
|
|
12980
13384
|
const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
|
|
12981
13385
|
const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
|
|
12982
13386
|
const fileName = LOG_FILE_NAMES[name] || name;
|
|
12983
|
-
const logPath =
|
|
13387
|
+
const logPath = path32.join(projectDir, LOG_DIR, `${fileName}.log`);
|
|
12984
13388
|
const logLines = getLastLogLines(logPath, linesToRead);
|
|
12985
13389
|
res.json({ name, lines: logLines });
|
|
12986
13390
|
} catch (error2) {
|
|
@@ -13015,7 +13419,7 @@ function createProjectPrdRoutes() {
|
|
|
13015
13419
|
|
|
13016
13420
|
// ../server/dist/routes/roadmap.routes.js
|
|
13017
13421
|
init_dist();
|
|
13018
|
-
import * as
|
|
13422
|
+
import * as path33 from "path";
|
|
13019
13423
|
import { Router as Router7 } from "express";
|
|
13020
13424
|
function createRoadmapRouteHandlers(ctx) {
|
|
13021
13425
|
const router = Router7({ mergeParams: true });
|
|
@@ -13025,7 +13429,7 @@ function createRoadmapRouteHandlers(ctx) {
|
|
|
13025
13429
|
const config = ctx.getConfig(req);
|
|
13026
13430
|
const projectDir = ctx.getProjectDir(req);
|
|
13027
13431
|
const status = getRoadmapStatus(projectDir, config);
|
|
13028
|
-
const prdDir =
|
|
13432
|
+
const prdDir = path33.join(projectDir, config.prdDir);
|
|
13029
13433
|
const state = loadRoadmapState(prdDir);
|
|
13030
13434
|
res.json({
|
|
13031
13435
|
...status,
|
|
@@ -13149,11 +13553,13 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
|
|
|
13149
13553
|
const qaPlan = getSchedulingPlan(projectDir, config, "qa");
|
|
13150
13554
|
const auditPlan = getSchedulingPlan(projectDir, config, "audit");
|
|
13151
13555
|
const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
|
|
13556
|
+
const analyticsPlan = getSchedulingPlan(projectDir, config, "analytics");
|
|
13152
13557
|
const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
|
|
13153
13558
|
const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
|
|
13154
13559
|
const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
|
|
13155
13560
|
const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
|
|
13156
13561
|
const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
|
|
13562
|
+
const analyticsInstalled = installed && config.analytics.enabled && hasScheduledCommand(entries, "analytics");
|
|
13157
13563
|
return {
|
|
13158
13564
|
executor: {
|
|
13159
13565
|
schedule: config.cronSchedule,
|
|
@@ -13195,6 +13601,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
|
|
|
13195
13601
|
manualDelayMinutes: plannerPlan.manualDelayMinutes,
|
|
13196
13602
|
balancedDelayMinutes: plannerPlan.balancedDelayMinutes
|
|
13197
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
|
+
},
|
|
13198
13612
|
paused: !installed,
|
|
13199
13613
|
schedulingPriority: config.schedulingPriority,
|
|
13200
13614
|
entries
|
|
@@ -13349,14 +13763,14 @@ function createQueueRoutes(deps) {
|
|
|
13349
13763
|
var __filename2 = fileURLToPath3(import.meta.url);
|
|
13350
13764
|
var __dirname3 = dirname7(__filename2);
|
|
13351
13765
|
function resolveWebDistPath() {
|
|
13352
|
-
const bundled =
|
|
13353
|
-
if (
|
|
13766
|
+
const bundled = path34.join(__dirname3, "web");
|
|
13767
|
+
if (fs31.existsSync(path34.join(bundled, "index.html")))
|
|
13354
13768
|
return bundled;
|
|
13355
13769
|
let d = __dirname3;
|
|
13356
13770
|
for (let i = 0; i < 8; i++) {
|
|
13357
|
-
if (
|
|
13358
|
-
const dev =
|
|
13359
|
-
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")))
|
|
13360
13774
|
return dev;
|
|
13361
13775
|
break;
|
|
13362
13776
|
}
|
|
@@ -13366,7 +13780,7 @@ function resolveWebDistPath() {
|
|
|
13366
13780
|
}
|
|
13367
13781
|
function setupStaticFiles(app) {
|
|
13368
13782
|
const webDistPath = resolveWebDistPath();
|
|
13369
|
-
if (
|
|
13783
|
+
if (fs31.existsSync(webDistPath)) {
|
|
13370
13784
|
app.use(express.static(webDistPath));
|
|
13371
13785
|
}
|
|
13372
13786
|
app.use((req, res, next) => {
|
|
@@ -13374,8 +13788,8 @@ function setupStaticFiles(app) {
|
|
|
13374
13788
|
next();
|
|
13375
13789
|
return;
|
|
13376
13790
|
}
|
|
13377
|
-
const indexPath =
|
|
13378
|
-
if (
|
|
13791
|
+
const indexPath = path34.resolve(webDistPath, "index.html");
|
|
13792
|
+
if (fs31.existsSync(indexPath)) {
|
|
13379
13793
|
res.sendFile(indexPath, (err) => {
|
|
13380
13794
|
if (err)
|
|
13381
13795
|
next();
|
|
@@ -13491,7 +13905,7 @@ function createGlobalApp() {
|
|
|
13491
13905
|
return app;
|
|
13492
13906
|
}
|
|
13493
13907
|
function bootContainer() {
|
|
13494
|
-
initContainer(
|
|
13908
|
+
initContainer(path34.dirname(getDbPath()));
|
|
13495
13909
|
}
|
|
13496
13910
|
function startServer(projectDir, port) {
|
|
13497
13911
|
bootContainer();
|
|
@@ -13544,8 +13958,8 @@ function isProcessRunning2(pid) {
|
|
|
13544
13958
|
}
|
|
13545
13959
|
function readPid(lockPath) {
|
|
13546
13960
|
try {
|
|
13547
|
-
if (!
|
|
13548
|
-
const raw =
|
|
13961
|
+
if (!fs32.existsSync(lockPath)) return null;
|
|
13962
|
+
const raw = fs32.readFileSync(lockPath, "utf-8").trim();
|
|
13549
13963
|
const pid = parseInt(raw, 10);
|
|
13550
13964
|
return Number.isFinite(pid) ? pid : null;
|
|
13551
13965
|
} catch {
|
|
@@ -13557,10 +13971,10 @@ function acquireServeLock(mode, port) {
|
|
|
13557
13971
|
let stalePidCleaned;
|
|
13558
13972
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
13559
13973
|
try {
|
|
13560
|
-
const fd =
|
|
13561
|
-
|
|
13974
|
+
const fd = fs32.openSync(lockPath, "wx");
|
|
13975
|
+
fs32.writeFileSync(fd, `${process.pid}
|
|
13562
13976
|
`);
|
|
13563
|
-
|
|
13977
|
+
fs32.closeSync(fd);
|
|
13564
13978
|
return { acquired: true, lockPath, stalePidCleaned };
|
|
13565
13979
|
} catch (error2) {
|
|
13566
13980
|
const err = error2;
|
|
@@ -13581,7 +13995,7 @@ function acquireServeLock(mode, port) {
|
|
|
13581
13995
|
};
|
|
13582
13996
|
}
|
|
13583
13997
|
try {
|
|
13584
|
-
|
|
13998
|
+
fs32.unlinkSync(lockPath);
|
|
13585
13999
|
if (existingPid) {
|
|
13586
14000
|
stalePidCleaned = existingPid;
|
|
13587
14001
|
}
|
|
@@ -13604,10 +14018,10 @@ function acquireServeLock(mode, port) {
|
|
|
13604
14018
|
}
|
|
13605
14019
|
function releaseServeLock(lockPath) {
|
|
13606
14020
|
try {
|
|
13607
|
-
if (!
|
|
14021
|
+
if (!fs32.existsSync(lockPath)) return;
|
|
13608
14022
|
const lockPid = readPid(lockPath);
|
|
13609
14023
|
if (lockPid !== null && lockPid !== process.pid) return;
|
|
13610
|
-
|
|
14024
|
+
fs32.unlinkSync(lockPath);
|
|
13611
14025
|
} catch {
|
|
13612
14026
|
}
|
|
13613
14027
|
}
|
|
@@ -13703,14 +14117,14 @@ function historyCommand(program2) {
|
|
|
13703
14117
|
// src/commands/update.ts
|
|
13704
14118
|
init_dist();
|
|
13705
14119
|
import { spawnSync } from "child_process";
|
|
13706
|
-
import * as
|
|
13707
|
-
import * as
|
|
14120
|
+
import * as fs33 from "fs";
|
|
14121
|
+
import * as path35 from "path";
|
|
13708
14122
|
var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
|
|
13709
14123
|
function parseProjectDirs(projects, cwd) {
|
|
13710
14124
|
if (!projects || projects.trim().length === 0) {
|
|
13711
14125
|
return [cwd];
|
|
13712
14126
|
}
|
|
13713
|
-
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));
|
|
13714
14128
|
return Array.from(new Set(dirs));
|
|
13715
14129
|
}
|
|
13716
14130
|
function shouldInstallGlobal(options) {
|
|
@@ -13752,7 +14166,7 @@ function updateCommand(program2) {
|
|
|
13752
14166
|
}
|
|
13753
14167
|
const nightWatchBin = resolveNightWatchBin();
|
|
13754
14168
|
for (const projectDir of projectDirs) {
|
|
13755
|
-
if (!
|
|
14169
|
+
if (!fs33.existsSync(projectDir) || !fs33.statSync(projectDir).isDirectory()) {
|
|
13756
14170
|
warn(`Skipping invalid project directory: ${projectDir}`);
|
|
13757
14171
|
continue;
|
|
13758
14172
|
}
|
|
@@ -13796,8 +14210,8 @@ function prdStateCommand(program2) {
|
|
|
13796
14210
|
|
|
13797
14211
|
// src/commands/retry.ts
|
|
13798
14212
|
init_dist();
|
|
13799
|
-
import * as
|
|
13800
|
-
import * as
|
|
14213
|
+
import * as fs34 from "fs";
|
|
14214
|
+
import * as path36 from "path";
|
|
13801
14215
|
function normalizePrdName(name) {
|
|
13802
14216
|
if (!name.endsWith(".md")) {
|
|
13803
14217
|
return `${name}.md`;
|
|
@@ -13805,26 +14219,26 @@ function normalizePrdName(name) {
|
|
|
13805
14219
|
return name;
|
|
13806
14220
|
}
|
|
13807
14221
|
function getDonePrds(doneDir) {
|
|
13808
|
-
if (!
|
|
14222
|
+
if (!fs34.existsSync(doneDir)) {
|
|
13809
14223
|
return [];
|
|
13810
14224
|
}
|
|
13811
|
-
return
|
|
14225
|
+
return fs34.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
|
|
13812
14226
|
}
|
|
13813
14227
|
function retryCommand(program2) {
|
|
13814
14228
|
program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
|
|
13815
14229
|
const projectDir = process.cwd();
|
|
13816
14230
|
const config = loadConfig(projectDir);
|
|
13817
|
-
const prdDir =
|
|
13818
|
-
const doneDir =
|
|
14231
|
+
const prdDir = path36.join(projectDir, config.prdDir);
|
|
14232
|
+
const doneDir = path36.join(prdDir, "done");
|
|
13819
14233
|
const normalizedPrdName = normalizePrdName(prdName);
|
|
13820
|
-
const pendingPath =
|
|
13821
|
-
if (
|
|
14234
|
+
const pendingPath = path36.join(prdDir, normalizedPrdName);
|
|
14235
|
+
if (fs34.existsSync(pendingPath)) {
|
|
13822
14236
|
info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
|
|
13823
14237
|
return;
|
|
13824
14238
|
}
|
|
13825
|
-
const donePath =
|
|
13826
|
-
if (
|
|
13827
|
-
|
|
14239
|
+
const donePath = path36.join(doneDir, normalizedPrdName);
|
|
14240
|
+
if (fs34.existsSync(donePath)) {
|
|
14241
|
+
fs34.renameSync(donePath, pendingPath);
|
|
13828
14242
|
success(`Moved "${normalizedPrdName}" back to pending.`);
|
|
13829
14243
|
dim(`From: ${donePath}`);
|
|
13830
14244
|
dim(`To: ${pendingPath}`);
|
|
@@ -14076,7 +14490,7 @@ function prdsCommand(program2) {
|
|
|
14076
14490
|
|
|
14077
14491
|
// src/commands/cancel.ts
|
|
14078
14492
|
init_dist();
|
|
14079
|
-
import * as
|
|
14493
|
+
import * as fs35 from "fs";
|
|
14080
14494
|
import * as readline3 from "readline";
|
|
14081
14495
|
function getLockFilePaths2(projectDir) {
|
|
14082
14496
|
const runtimeKey = projectRuntimeKey(projectDir);
|
|
@@ -14123,7 +14537,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
14123
14537
|
const pid = lockStatus.pid;
|
|
14124
14538
|
if (!lockStatus.running) {
|
|
14125
14539
|
try {
|
|
14126
|
-
|
|
14540
|
+
fs35.unlinkSync(lockPath);
|
|
14127
14541
|
return {
|
|
14128
14542
|
success: true,
|
|
14129
14543
|
message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
|
|
@@ -14161,7 +14575,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
14161
14575
|
await sleep2(3e3);
|
|
14162
14576
|
if (!isProcessRunning3(pid)) {
|
|
14163
14577
|
try {
|
|
14164
|
-
|
|
14578
|
+
fs35.unlinkSync(lockPath);
|
|
14165
14579
|
} catch {
|
|
14166
14580
|
}
|
|
14167
14581
|
return {
|
|
@@ -14196,7 +14610,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
14196
14610
|
await sleep2(500);
|
|
14197
14611
|
if (!isProcessRunning3(pid)) {
|
|
14198
14612
|
try {
|
|
14199
|
-
|
|
14613
|
+
fs35.unlinkSync(lockPath);
|
|
14200
14614
|
} catch {
|
|
14201
14615
|
}
|
|
14202
14616
|
return {
|
|
@@ -14257,31 +14671,31 @@ function cancelCommand(program2) {
|
|
|
14257
14671
|
|
|
14258
14672
|
// src/commands/slice.ts
|
|
14259
14673
|
init_dist();
|
|
14260
|
-
import * as
|
|
14261
|
-
import * as
|
|
14674
|
+
import * as fs36 from "fs";
|
|
14675
|
+
import * as path37 from "path";
|
|
14262
14676
|
function plannerLockPath2(projectDir) {
|
|
14263
14677
|
return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
|
|
14264
14678
|
}
|
|
14265
14679
|
function acquirePlannerLock(projectDir) {
|
|
14266
14680
|
const lockFile = plannerLockPath2(projectDir);
|
|
14267
|
-
if (
|
|
14268
|
-
const pidRaw =
|
|
14681
|
+
if (fs36.existsSync(lockFile)) {
|
|
14682
|
+
const pidRaw = fs36.readFileSync(lockFile, "utf-8").trim();
|
|
14269
14683
|
const pid = parseInt(pidRaw, 10);
|
|
14270
14684
|
if (!Number.isNaN(pid) && isProcessRunning(pid)) {
|
|
14271
14685
|
return { acquired: false, lockFile, pid };
|
|
14272
14686
|
}
|
|
14273
14687
|
try {
|
|
14274
|
-
|
|
14688
|
+
fs36.unlinkSync(lockFile);
|
|
14275
14689
|
} catch {
|
|
14276
14690
|
}
|
|
14277
14691
|
}
|
|
14278
|
-
|
|
14692
|
+
fs36.writeFileSync(lockFile, String(process.pid));
|
|
14279
14693
|
return { acquired: true, lockFile };
|
|
14280
14694
|
}
|
|
14281
14695
|
function releasePlannerLock(lockFile) {
|
|
14282
14696
|
try {
|
|
14283
|
-
if (
|
|
14284
|
-
|
|
14697
|
+
if (fs36.existsSync(lockFile)) {
|
|
14698
|
+
fs36.unlinkSync(lockFile);
|
|
14285
14699
|
}
|
|
14286
14700
|
} catch {
|
|
14287
14701
|
}
|
|
@@ -14290,12 +14704,12 @@ function resolvePlannerIssueColumn(config) {
|
|
|
14290
14704
|
return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
|
|
14291
14705
|
}
|
|
14292
14706
|
function buildPlannerIssueBody(projectDir, config, result) {
|
|
14293
|
-
const relativePrdPath =
|
|
14294
|
-
const absolutePrdPath =
|
|
14707
|
+
const relativePrdPath = path37.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
|
|
14708
|
+
const absolutePrdPath = path37.join(projectDir, config.prdDir, result.file ?? "");
|
|
14295
14709
|
const sourceItem = result.item;
|
|
14296
14710
|
let prdContent;
|
|
14297
14711
|
try {
|
|
14298
|
-
prdContent =
|
|
14712
|
+
prdContent = fs36.readFileSync(absolutePrdPath, "utf-8");
|
|
14299
14713
|
} catch {
|
|
14300
14714
|
prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
|
|
14301
14715
|
}
|
|
@@ -14471,7 +14885,7 @@ function sliceCommand(program2) {
|
|
|
14471
14885
|
if (!options.dryRun) {
|
|
14472
14886
|
await sendNotifications(config, {
|
|
14473
14887
|
event: "run_started",
|
|
14474
|
-
projectName:
|
|
14888
|
+
projectName: path37.basename(projectDir),
|
|
14475
14889
|
exitCode: 0,
|
|
14476
14890
|
provider: config.provider
|
|
14477
14891
|
});
|
|
@@ -14506,7 +14920,7 @@ function sliceCommand(program2) {
|
|
|
14506
14920
|
if (!options.dryRun && result.sliced) {
|
|
14507
14921
|
await sendNotifications(config, {
|
|
14508
14922
|
event: "run_succeeded",
|
|
14509
|
-
projectName:
|
|
14923
|
+
projectName: path37.basename(projectDir),
|
|
14510
14924
|
exitCode,
|
|
14511
14925
|
provider: config.provider,
|
|
14512
14926
|
prTitle: result.item?.title
|
|
@@ -14514,7 +14928,7 @@ function sliceCommand(program2) {
|
|
|
14514
14928
|
} else if (!options.dryRun && !nothingPending) {
|
|
14515
14929
|
await sendNotifications(config, {
|
|
14516
14930
|
event: "run_failed",
|
|
14517
|
-
projectName:
|
|
14931
|
+
projectName: path37.basename(projectDir),
|
|
14518
14932
|
exitCode,
|
|
14519
14933
|
provider: config.provider
|
|
14520
14934
|
});
|
|
@@ -14530,21 +14944,21 @@ function sliceCommand(program2) {
|
|
|
14530
14944
|
|
|
14531
14945
|
// src/commands/state.ts
|
|
14532
14946
|
init_dist();
|
|
14533
|
-
import * as
|
|
14534
|
-
import * as
|
|
14947
|
+
import * as os8 from "os";
|
|
14948
|
+
import * as path38 from "path";
|
|
14535
14949
|
import chalk5 from "chalk";
|
|
14536
14950
|
import { Command } from "commander";
|
|
14537
14951
|
function createStateCommand() {
|
|
14538
14952
|
const state = new Command("state");
|
|
14539
14953
|
state.description("Manage Night Watch state");
|
|
14540
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) => {
|
|
14541
|
-
const nightWatchHome = process.env.NIGHT_WATCH_HOME ||
|
|
14955
|
+
const nightWatchHome = process.env.NIGHT_WATCH_HOME || path38.join(os8.homedir(), GLOBAL_CONFIG_DIR);
|
|
14542
14956
|
if (opts.dryRun) {
|
|
14543
14957
|
console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
|
|
14544
14958
|
console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
|
|
14545
|
-
console.log(` ${
|
|
14546
|
-
console.log(` ${
|
|
14547
|
-
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")}`);
|
|
14548
14962
|
console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
|
|
14549
14963
|
console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
|
|
14550
14964
|
return;
|
|
@@ -14582,8 +14996,8 @@ function createStateCommand() {
|
|
|
14582
14996
|
init_dist();
|
|
14583
14997
|
init_dist();
|
|
14584
14998
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
14585
|
-
import * as
|
|
14586
|
-
import * as
|
|
14999
|
+
import * as fs37 from "fs";
|
|
15000
|
+
import * as path39 from "path";
|
|
14587
15001
|
import * as readline4 from "readline";
|
|
14588
15002
|
import chalk6 from "chalk";
|
|
14589
15003
|
async function run(fn) {
|
|
@@ -14605,7 +15019,7 @@ function getProvider(config, cwd) {
|
|
|
14605
15019
|
return createBoardProvider(bp, cwd);
|
|
14606
15020
|
}
|
|
14607
15021
|
function defaultBoardTitle(cwd) {
|
|
14608
|
-
return `${
|
|
15022
|
+
return `${path39.basename(cwd)} Night Watch`;
|
|
14609
15023
|
}
|
|
14610
15024
|
async function ensureBoardConfigured(config, cwd, provider, options) {
|
|
14611
15025
|
if (config.boardProvider?.projectNumber) {
|
|
@@ -14804,11 +15218,11 @@ function boardCommand(program2) {
|
|
|
14804
15218
|
let body = options.body ?? "";
|
|
14805
15219
|
if (options.bodyFile) {
|
|
14806
15220
|
const filePath = options.bodyFile;
|
|
14807
|
-
if (!
|
|
15221
|
+
if (!fs37.existsSync(filePath)) {
|
|
14808
15222
|
console.error(`File not found: ${filePath}`);
|
|
14809
15223
|
process.exit(1);
|
|
14810
15224
|
}
|
|
14811
|
-
body =
|
|
15225
|
+
body = fs37.readFileSync(filePath, "utf-8");
|
|
14812
15226
|
}
|
|
14813
15227
|
const labels = [];
|
|
14814
15228
|
if (options.label) {
|
|
@@ -15030,12 +15444,12 @@ function boardCommand(program2) {
|
|
|
15030
15444
|
const config = loadConfig(cwd);
|
|
15031
15445
|
const provider = getProvider(config, cwd);
|
|
15032
15446
|
await ensureBoardConfigured(config, cwd, provider);
|
|
15033
|
-
const roadmapPath = options.roadmap ??
|
|
15034
|
-
if (!
|
|
15447
|
+
const roadmapPath = options.roadmap ?? path39.join(cwd, "ROADMAP.md");
|
|
15448
|
+
if (!fs37.existsSync(roadmapPath)) {
|
|
15035
15449
|
console.error(`Roadmap file not found: ${roadmapPath}`);
|
|
15036
15450
|
process.exit(1);
|
|
15037
15451
|
}
|
|
15038
|
-
const roadmapContent =
|
|
15452
|
+
const roadmapContent = fs37.readFileSync(roadmapPath, "utf-8");
|
|
15039
15453
|
const items = parseRoadmap(roadmapContent);
|
|
15040
15454
|
const uncheckedItems = getUncheckedItems(items);
|
|
15041
15455
|
if (uncheckedItems.length === 0) {
|
|
@@ -15159,11 +15573,11 @@ function boardCommand(program2) {
|
|
|
15159
15573
|
// src/commands/queue.ts
|
|
15160
15574
|
init_dist();
|
|
15161
15575
|
init_dist();
|
|
15162
|
-
import * as
|
|
15576
|
+
import * as path40 from "path";
|
|
15163
15577
|
import { spawn as spawn6 } from "child_process";
|
|
15164
15578
|
import chalk7 from "chalk";
|
|
15165
15579
|
import { Command as Command2 } from "commander";
|
|
15166
|
-
var
|
|
15580
|
+
var logger4 = createLogger("queue");
|
|
15167
15581
|
var VALID_JOB_TYPES2 = ["executor", "reviewer", "qa", "audit", "slicer"];
|
|
15168
15582
|
function formatTimestamp(unixTs) {
|
|
15169
15583
|
if (unixTs === null) return "-";
|
|
@@ -15279,7 +15693,7 @@ function createQueueCommand() {
|
|
|
15279
15693
|
process.exit(1);
|
|
15280
15694
|
}
|
|
15281
15695
|
}
|
|
15282
|
-
const projectName =
|
|
15696
|
+
const projectName = path40.basename(projectDir);
|
|
15283
15697
|
const queueConfig = loadConfig(projectDir).queue;
|
|
15284
15698
|
const id = enqueueJob(projectDir, projectName, jobType, envVars, queueConfig);
|
|
15285
15699
|
console.log(chalk7.green(`Enqueued ${jobType} for ${projectName} (ID: ${id})`));
|
|
@@ -15287,13 +15701,13 @@ function createQueueCommand() {
|
|
|
15287
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) => {
|
|
15288
15702
|
const entry = dispatchNextJob(loadConfig(process.cwd()).queue);
|
|
15289
15703
|
if (!entry) {
|
|
15290
|
-
|
|
15704
|
+
logger4.info("No pending jobs to dispatch");
|
|
15291
15705
|
return;
|
|
15292
15706
|
}
|
|
15293
|
-
|
|
15707
|
+
logger4.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
|
|
15294
15708
|
const scriptName = getScriptNameForJobType(entry.jobType);
|
|
15295
15709
|
if (!scriptName) {
|
|
15296
|
-
|
|
15710
|
+
logger4.error(`Unknown job type: ${entry.jobType}`);
|
|
15297
15711
|
return;
|
|
15298
15712
|
}
|
|
15299
15713
|
let projectEnv;
|
|
@@ -15312,7 +15726,7 @@ function createQueueCommand() {
|
|
|
15312
15726
|
NW_QUEUE_ENTRY_ID: String(entry.id)
|
|
15313
15727
|
};
|
|
15314
15728
|
const scriptPath = getScriptPath(scriptName);
|
|
15315
|
-
|
|
15729
|
+
logger4.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
|
|
15316
15730
|
try {
|
|
15317
15731
|
const child = spawn6("bash", [scriptPath, entry.projectPath], {
|
|
15318
15732
|
detached: true,
|
|
@@ -15321,11 +15735,11 @@ function createQueueCommand() {
|
|
|
15321
15735
|
cwd: entry.projectPath
|
|
15322
15736
|
});
|
|
15323
15737
|
child.unref();
|
|
15324
|
-
|
|
15738
|
+
logger4.info(`Spawned PID: ${child.pid}`);
|
|
15325
15739
|
markJobRunning(entry.id);
|
|
15326
15740
|
} catch (error2) {
|
|
15327
15741
|
updateJobStatus(entry.id, "pending");
|
|
15328
|
-
|
|
15742
|
+
logger4.error(
|
|
15329
15743
|
`Failed to dispatch ${entry.jobType} for ${entry.projectName}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
15330
15744
|
);
|
|
15331
15745
|
process.exit(1);
|
|
@@ -15439,13 +15853,13 @@ var __dirname4 = dirname8(__filename3);
|
|
|
15439
15853
|
function findPackageRoot(dir) {
|
|
15440
15854
|
let d = dir;
|
|
15441
15855
|
for (let i = 0; i < 5; i++) {
|
|
15442
|
-
if (existsSync29(
|
|
15856
|
+
if (existsSync29(join35(d, "package.json"))) return d;
|
|
15443
15857
|
d = dirname8(d);
|
|
15444
15858
|
}
|
|
15445
15859
|
return dir;
|
|
15446
15860
|
}
|
|
15447
15861
|
var packageRoot = findPackageRoot(__dirname4);
|
|
15448
|
-
var packageJson = JSON.parse(readFileSync18(
|
|
15862
|
+
var packageJson = JSON.parse(readFileSync18(join35(packageRoot, "package.json"), "utf-8"));
|
|
15449
15863
|
var program = new Command3();
|
|
15450
15864
|
program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
|
|
15451
15865
|
initCommand(program);
|
|
@@ -15453,6 +15867,7 @@ runCommand(program);
|
|
|
15453
15867
|
reviewCommand(program);
|
|
15454
15868
|
qaCommand(program);
|
|
15455
15869
|
auditCommand(program);
|
|
15870
|
+
analyticsCommand(program);
|
|
15456
15871
|
installCommand(program);
|
|
15457
15872
|
uninstallCommand(program);
|
|
15458
15873
|
statusCommand(program);
|