@jonit-dev/night-watch-cli 1.8.1 → 1.8.3
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 +982 -412
- 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/board.d.ts.map +1 -1
- package/dist/commands/board.js +20 -0
- package/dist/commands/board.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-BFxPiKyy.js +381 -0
- 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, GLOBAL_NOTIFICATIONS_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,13 +217,15 @@ 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";
|
|
198
225
|
HISTORY_FILE_NAME = "history.json";
|
|
199
226
|
PRD_STATES_FILE_NAME = "prd-states.json";
|
|
200
227
|
STATE_DB_FILE_NAME = "state.db";
|
|
228
|
+
GLOBAL_NOTIFICATIONS_FILE_NAME = "global-notifications.json";
|
|
201
229
|
MAX_HISTORY_RECORDS_PER_PRD = 10;
|
|
202
230
|
DEFAULT_QUEUE_ENABLED = true;
|
|
203
231
|
DEFAULT_QUEUE_MODE = "conservative";
|
|
@@ -208,7 +236,8 @@ var init_constants = __esm({
|
|
|
208
236
|
reviewer: 40,
|
|
209
237
|
slicer: 30,
|
|
210
238
|
qa: 20,
|
|
211
|
-
audit: 10
|
|
239
|
+
audit: 10,
|
|
240
|
+
analytics: 10
|
|
212
241
|
};
|
|
213
242
|
DEFAULT_QUEUE = {
|
|
214
243
|
enabled: DEFAULT_QUEUE_ENABLED,
|
|
@@ -223,6 +252,15 @@ var init_constants = __esm({
|
|
|
223
252
|
}
|
|
224
253
|
});
|
|
225
254
|
|
|
255
|
+
// ../core/dist/board/types.js
|
|
256
|
+
var BOARD_COLUMNS;
|
|
257
|
+
var init_types2 = __esm({
|
|
258
|
+
"../core/dist/board/types.js"() {
|
|
259
|
+
"use strict";
|
|
260
|
+
BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
226
264
|
// ../core/dist/config-normalize.js
|
|
227
265
|
function validateProvider(value) {
|
|
228
266
|
const trimmed = value.trim();
|
|
@@ -418,6 +456,20 @@ function normalizeConfig(rawConfig) {
|
|
|
418
456
|
};
|
|
419
457
|
normalized.audit = audit;
|
|
420
458
|
}
|
|
459
|
+
const rawAnalytics = readObject(rawConfig.analytics);
|
|
460
|
+
if (rawAnalytics) {
|
|
461
|
+
const targetColumnRaw = readString(rawAnalytics.targetColumn);
|
|
462
|
+
const targetColumn = targetColumnRaw && BOARD_COLUMNS.includes(targetColumnRaw) ? targetColumnRaw : DEFAULT_ANALYTICS.targetColumn;
|
|
463
|
+
const analytics = {
|
|
464
|
+
enabled: readBoolean(rawAnalytics.enabled) ?? DEFAULT_ANALYTICS.enabled,
|
|
465
|
+
schedule: readString(rawAnalytics.schedule) ?? DEFAULT_ANALYTICS.schedule,
|
|
466
|
+
maxRuntime: readNumber(rawAnalytics.maxRuntime) ?? DEFAULT_ANALYTICS.maxRuntime,
|
|
467
|
+
lookbackDays: readNumber(rawAnalytics.lookbackDays) ?? DEFAULT_ANALYTICS.lookbackDays,
|
|
468
|
+
targetColumn,
|
|
469
|
+
analysisPrompt: readString(rawAnalytics.analysisPrompt) ?? DEFAULT_ANALYTICS.analysisPrompt
|
|
470
|
+
};
|
|
471
|
+
normalized.analytics = analytics;
|
|
472
|
+
}
|
|
421
473
|
const rawJobProviders = readObject(rawConfig.jobProviders);
|
|
422
474
|
if (rawJobProviders) {
|
|
423
475
|
const jobProviders = {};
|
|
@@ -477,6 +529,7 @@ function normalizeConfig(rawConfig) {
|
|
|
477
529
|
var init_config_normalize = __esm({
|
|
478
530
|
"../core/dist/config-normalize.js"() {
|
|
479
531
|
"use strict";
|
|
532
|
+
init_types2();
|
|
480
533
|
init_constants();
|
|
481
534
|
}
|
|
482
535
|
});
|
|
@@ -700,6 +753,25 @@ function buildEnvOverrideConfig(fileConfig) {
|
|
|
700
753
|
if (!isNaN(v) && v > 0)
|
|
701
754
|
env.audit = { ...auditBase(), maxRuntime: v };
|
|
702
755
|
}
|
|
756
|
+
const analyticsBase = () => env.analytics ?? fileConfig?.analytics ?? DEFAULT_ANALYTICS;
|
|
757
|
+
if (process.env.NW_ANALYTICS_ENABLED) {
|
|
758
|
+
const v = parseBoolean(process.env.NW_ANALYTICS_ENABLED);
|
|
759
|
+
if (v !== null)
|
|
760
|
+
env.analytics = { ...analyticsBase(), enabled: v };
|
|
761
|
+
}
|
|
762
|
+
if (process.env.NW_ANALYTICS_SCHEDULE) {
|
|
763
|
+
env.analytics = { ...analyticsBase(), schedule: process.env.NW_ANALYTICS_SCHEDULE };
|
|
764
|
+
}
|
|
765
|
+
if (process.env.NW_ANALYTICS_MAX_RUNTIME) {
|
|
766
|
+
const v = parseInt(process.env.NW_ANALYTICS_MAX_RUNTIME, 10);
|
|
767
|
+
if (!isNaN(v) && v > 0)
|
|
768
|
+
env.analytics = { ...analyticsBase(), maxRuntime: v };
|
|
769
|
+
}
|
|
770
|
+
if (process.env.NW_ANALYTICS_LOOKBACK_DAYS) {
|
|
771
|
+
const v = parseInt(process.env.NW_ANALYTICS_LOOKBACK_DAYS, 10);
|
|
772
|
+
if (!isNaN(v) && v > 0)
|
|
773
|
+
env.analytics = { ...analyticsBase(), lookbackDays: v };
|
|
774
|
+
}
|
|
703
775
|
const jobProvidersEnv = {};
|
|
704
776
|
for (const jobType of VALID_JOB_TYPES) {
|
|
705
777
|
const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
|
|
@@ -792,6 +864,7 @@ function getDefaultConfig() {
|
|
|
792
864
|
claudeModel: DEFAULT_CLAUDE_MODEL,
|
|
793
865
|
qa: { ...DEFAULT_QA },
|
|
794
866
|
audit: { ...DEFAULT_AUDIT },
|
|
867
|
+
analytics: { ...DEFAULT_ANALYTICS },
|
|
795
868
|
jobProviders: { ...DEFAULT_JOB_PROVIDERS },
|
|
796
869
|
queue: { ...DEFAULT_QUEUE }
|
|
797
870
|
};
|
|
@@ -858,7 +931,7 @@ function mergeConfigLayer(base, layer) {
|
|
|
858
931
|
...layerQueue,
|
|
859
932
|
providerBuckets: { ...baseQueue.providerBuckets, ...layerQueue.providerBuckets }
|
|
860
933
|
};
|
|
861
|
-
} else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit") {
|
|
934
|
+
} else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics") {
|
|
862
935
|
base[_key] = {
|
|
863
936
|
...base[_key],
|
|
864
937
|
...value
|
|
@@ -943,15 +1016,6 @@ var init_config = __esm({
|
|
|
943
1016
|
}
|
|
944
1017
|
});
|
|
945
1018
|
|
|
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
1019
|
// ../core/dist/storage/repositories/sqlite/execution-history.repository.js
|
|
956
1020
|
import Database from "better-sqlite3";
|
|
957
1021
|
import { inject, injectable } from "tsyringe";
|
|
@@ -1966,11 +2030,18 @@ var init_github_projects = __esm({
|
|
|
1966
2030
|
await this.ensureStatusColumns(existing.id);
|
|
1967
2031
|
return { id: existing.id, number: existing.number, title: existing.title, url: existing.url };
|
|
1968
2032
|
}
|
|
1969
|
-
const createData = await graphql(`
|
|
1970
|
-
|
|
1971
|
-
|
|
2033
|
+
const createData = await graphql(`
|
|
2034
|
+
mutation CreateProject($ownerId: ID!, $title: String!) {
|
|
2035
|
+
createProjectV2(input: { ownerId: $ownerId, title: $title }) {
|
|
2036
|
+
projectV2 {
|
|
2037
|
+
id
|
|
2038
|
+
number
|
|
2039
|
+
url
|
|
2040
|
+
title
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
1972
2043
|
}
|
|
1973
|
-
|
|
2044
|
+
`, { ownerId: owner.id, title }, this.cwd);
|
|
1974
2045
|
const project = createData.createProjectV2.projectV2;
|
|
1975
2046
|
this.cachedProjectId = project.id;
|
|
1976
2047
|
await this.linkProjectToRepository(project.id);
|
|
@@ -1986,24 +2057,34 @@ var init_github_projects = __esm({
|
|
|
1986
2057
|
const message = err instanceof Error ? err.message : String(err);
|
|
1987
2058
|
if (!message.includes("Status field not found"))
|
|
1988
2059
|
throw err;
|
|
1989
|
-
const createFieldData = await graphql(`
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2060
|
+
const createFieldData = await graphql(`
|
|
2061
|
+
mutation CreateStatusField($projectId: ID!) {
|
|
2062
|
+
createProjectV2Field(
|
|
2063
|
+
input: {
|
|
2064
|
+
projectId: $projectId
|
|
2065
|
+
dataType: SINGLE_SELECT
|
|
2066
|
+
name: "Status"
|
|
2067
|
+
singleSelectOptions: [
|
|
2068
|
+
{ name: "Draft", color: GRAY, description: "" }
|
|
2069
|
+
{ name: "Ready", color: BLUE, description: "" }
|
|
2070
|
+
{ name: "In Progress", color: YELLOW, description: "" }
|
|
2071
|
+
{ name: "Review", color: ORANGE, description: "" }
|
|
2072
|
+
{ name: "Done", color: GREEN, description: "" }
|
|
2073
|
+
]
|
|
2074
|
+
}
|
|
2075
|
+
) {
|
|
2076
|
+
projectV2Field {
|
|
2077
|
+
... on ProjectV2SingleSelectField {
|
|
2078
|
+
id
|
|
2079
|
+
options {
|
|
2080
|
+
id
|
|
2081
|
+
name
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2004
2085
|
}
|
|
2005
2086
|
}
|
|
2006
|
-
|
|
2087
|
+
`, { projectId: project.id }, this.cwd);
|
|
2007
2088
|
const field = createFieldData.createProjectV2Field.projectV2Field;
|
|
2008
2089
|
this.cachedFieldId = field.id;
|
|
2009
2090
|
this.cachedOptionIds = new Map(field.options.map((o) => [o.name, o.id]));
|
|
@@ -2030,11 +2111,23 @@ var init_github_projects = __esm({
|
|
|
2030
2111
|
async createIssue(input) {
|
|
2031
2112
|
const repo = await this.getRepo();
|
|
2032
2113
|
const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
|
|
2033
|
-
const issueArgs = [
|
|
2114
|
+
const issueArgs = [
|
|
2115
|
+
"issue",
|
|
2116
|
+
"create",
|
|
2117
|
+
"--title",
|
|
2118
|
+
input.title,
|
|
2119
|
+
"--body",
|
|
2120
|
+
input.body,
|
|
2121
|
+
"--repo",
|
|
2122
|
+
repo
|
|
2123
|
+
];
|
|
2034
2124
|
if (input.labels && input.labels.length > 0) {
|
|
2035
2125
|
issueArgs.push("--label", input.labels.join(","));
|
|
2036
2126
|
}
|
|
2037
|
-
const { stdout: issueUrlRaw } = await execFileAsync2("gh", issueArgs, {
|
|
2127
|
+
const { stdout: issueUrlRaw } = await execFileAsync2("gh", issueArgs, {
|
|
2128
|
+
cwd: this.cwd,
|
|
2129
|
+
encoding: "utf-8"
|
|
2130
|
+
});
|
|
2038
2131
|
const issueUrl = issueUrlRaw.trim();
|
|
2039
2132
|
const issueNumber = parseInt(issueUrl.split("/").pop() ?? "", 10);
|
|
2040
2133
|
if (!issueNumber)
|
|
@@ -2042,11 +2135,15 @@ var init_github_projects = __esm({
|
|
|
2042
2135
|
const [owner, repoName] = repo.split("/");
|
|
2043
2136
|
const { stdout: nodeIdRaw } = await execFileAsync2("gh", ["api", `repos/${owner}/${repoName}/issues/${issueNumber}`, "--jq", ".node_id"], { cwd: this.cwd, encoding: "utf-8" });
|
|
2044
2137
|
const issueJson = { number: issueNumber, id: nodeIdRaw.trim(), url: issueUrl };
|
|
2045
|
-
const addData = await graphql(`
|
|
2046
|
-
|
|
2047
|
-
|
|
2138
|
+
const addData = await graphql(`
|
|
2139
|
+
mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
|
|
2140
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
|
|
2141
|
+
item {
|
|
2142
|
+
id
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2048
2145
|
}
|
|
2049
|
-
|
|
2146
|
+
`, { projectId, contentId: issueJson.id }, this.cwd);
|
|
2050
2147
|
const itemId = addData.addProjectV2ItemById.item.id;
|
|
2051
2148
|
const targetColumn = input.column ?? "Draft";
|
|
2052
2149
|
const optionId = optionIds.get(targetColumn);
|
|
@@ -2066,11 +2163,45 @@ var init_github_projects = __esm({
|
|
|
2066
2163
|
assignees: []
|
|
2067
2164
|
};
|
|
2068
2165
|
}
|
|
2166
|
+
async addIssue(issueNumber, column = "Ready") {
|
|
2167
|
+
const repo = await this.getRepo();
|
|
2168
|
+
const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
|
|
2169
|
+
const [owner, repoName] = repo.split("/");
|
|
2170
|
+
const { stdout: nodeIdRaw } = await execFileAsync2("gh", ["api", `repos/${owner}/${repoName}/issues/${issueNumber}`, "--jq", ".node_id"], { cwd: this.cwd, encoding: "utf-8" });
|
|
2171
|
+
const nodeId = nodeIdRaw.trim();
|
|
2172
|
+
if (!nodeId)
|
|
2173
|
+
throw new Error(`Issue #${issueNumber} not found in ${repo}.`);
|
|
2174
|
+
const addData = await graphql(`
|
|
2175
|
+
mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
|
|
2176
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
|
|
2177
|
+
item {
|
|
2178
|
+
id
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
`, { projectId, contentId: nodeId }, this.cwd);
|
|
2183
|
+
const itemId = addData.addProjectV2ItemById.item.id;
|
|
2184
|
+
const optionId = optionIds.get(column);
|
|
2185
|
+
if (optionId)
|
|
2186
|
+
await this.setItemStatus(projectId, itemId, fieldId, optionId);
|
|
2187
|
+
const full = await this.getIssue(issueNumber);
|
|
2188
|
+
if (full)
|
|
2189
|
+
return { ...full, column };
|
|
2190
|
+
throw new Error(`Added issue #${issueNumber} to project but failed to fetch it back.`);
|
|
2191
|
+
}
|
|
2069
2192
|
async getIssue(issueNumber) {
|
|
2070
2193
|
const repo = await this.getRepo();
|
|
2071
2194
|
let rawIssue;
|
|
2072
2195
|
try {
|
|
2073
|
-
const { stdout: output } = await execFileAsync2("gh", [
|
|
2196
|
+
const { stdout: output } = await execFileAsync2("gh", [
|
|
2197
|
+
"issue",
|
|
2198
|
+
"view",
|
|
2199
|
+
String(issueNumber),
|
|
2200
|
+
"--repo",
|
|
2201
|
+
repo,
|
|
2202
|
+
"--json",
|
|
2203
|
+
"number,title,body,url,id,labels,assignees"
|
|
2204
|
+
], { cwd: this.cwd, encoding: "utf-8" });
|
|
2074
2205
|
rawIssue = JSON.parse(output);
|
|
2075
2206
|
} catch {
|
|
2076
2207
|
return null;
|
|
@@ -2175,6 +2306,9 @@ var init_local_kanban = __esm({
|
|
|
2175
2306
|
});
|
|
2176
2307
|
return toIBoardIssue(row);
|
|
2177
2308
|
}
|
|
2309
|
+
async addIssue(_issueNumber, _column) {
|
|
2310
|
+
throw new Error("addIssue is not supported by the local Kanban provider.");
|
|
2311
|
+
}
|
|
2178
2312
|
async getIssue(issueNumber) {
|
|
2179
2313
|
const row = this.repo.getByNumber(issueNumber);
|
|
2180
2314
|
return row ? toIBoardIssue(row) : null;
|
|
@@ -3046,6 +3180,9 @@ function auditLockPath(projectDir) {
|
|
|
3046
3180
|
function plannerLockPath(projectDir) {
|
|
3047
3181
|
return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
|
|
3048
3182
|
}
|
|
3183
|
+
function analyticsLockPath(projectDir) {
|
|
3184
|
+
return `${LOCK_FILE_PREFIX}analytics-${projectRuntimeKey(projectDir)}.lock`;
|
|
3185
|
+
}
|
|
3049
3186
|
function isProcessRunning(pid) {
|
|
3050
3187
|
try {
|
|
3051
3188
|
process.kill(pid, 0);
|
|
@@ -3404,7 +3541,8 @@ function collectLogInfo(projectDir) {
|
|
|
3404
3541
|
{ name: "reviewer", fileName: "reviewer.log" },
|
|
3405
3542
|
{ name: "qa", fileName: `${QA_LOG_NAME}.log` },
|
|
3406
3543
|
{ name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
|
|
3407
|
-
{ name: "planner", fileName: `${PLANNER_LOG_NAME}.log` }
|
|
3544
|
+
{ name: "planner", fileName: `${PLANNER_LOG_NAME}.log` },
|
|
3545
|
+
{ name: "analytics", fileName: `${ANALYTICS_LOG_NAME}.log` }
|
|
3408
3546
|
];
|
|
3409
3547
|
return logEntries.map(({ name, fileName }) => {
|
|
3410
3548
|
const logPath = path6.join(projectDir, LOG_DIR, fileName);
|
|
@@ -3433,12 +3571,14 @@ async function fetchStatusSnapshot(projectDir, config) {
|
|
|
3433
3571
|
const qaLock = checkLockFile(qaLockPath(projectDir));
|
|
3434
3572
|
const auditLock = checkLockFile(auditLockPath(projectDir));
|
|
3435
3573
|
const plannerLock = checkLockFile(plannerLockPath(projectDir));
|
|
3574
|
+
const analyticsLock = checkLockFile(analyticsLockPath(projectDir));
|
|
3436
3575
|
const processes = [
|
|
3437
3576
|
{ name: "executor", running: executorLock.running, pid: executorLock.pid },
|
|
3438
3577
|
{ name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
|
|
3439
3578
|
{ name: "qa", running: qaLock.running, pid: qaLock.pid },
|
|
3440
3579
|
{ name: "audit", running: auditLock.running, pid: auditLock.pid },
|
|
3441
|
-
{ name: "planner", running: plannerLock.running, pid: plannerLock.pid }
|
|
3580
|
+
{ name: "planner", running: plannerLock.running, pid: plannerLock.pid },
|
|
3581
|
+
{ name: "analytics", running: analyticsLock.running, pid: analyticsLock.pid }
|
|
3442
3582
|
];
|
|
3443
3583
|
const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
|
|
3444
3584
|
const prs = await collectPrInfo(projectDir, config.branchPatterns);
|
|
@@ -4206,6 +4346,36 @@ var init_log_utils = __esm({
|
|
|
4206
4346
|
}
|
|
4207
4347
|
});
|
|
4208
4348
|
|
|
4349
|
+
// ../core/dist/utils/global-config.js
|
|
4350
|
+
import * as fs11 from "fs";
|
|
4351
|
+
import * as os5 from "os";
|
|
4352
|
+
import * as path10 from "path";
|
|
4353
|
+
function getGlobalNotificationsPath() {
|
|
4354
|
+
return path10.join(os5.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_NOTIFICATIONS_FILE_NAME);
|
|
4355
|
+
}
|
|
4356
|
+
function loadGlobalNotificationsConfig() {
|
|
4357
|
+
const filePath = getGlobalNotificationsPath();
|
|
4358
|
+
try {
|
|
4359
|
+
if (!fs11.existsSync(filePath))
|
|
4360
|
+
return { webhook: null };
|
|
4361
|
+
const raw = fs11.readFileSync(filePath, "utf-8");
|
|
4362
|
+
return JSON.parse(raw);
|
|
4363
|
+
} catch {
|
|
4364
|
+
return { webhook: null };
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
function saveGlobalNotificationsConfig(config) {
|
|
4368
|
+
const filePath = getGlobalNotificationsPath();
|
|
4369
|
+
fs11.mkdirSync(path10.dirname(filePath), { recursive: true });
|
|
4370
|
+
fs11.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
4371
|
+
}
|
|
4372
|
+
var init_global_config = __esm({
|
|
4373
|
+
"../core/dist/utils/global-config.js"() {
|
|
4374
|
+
"use strict";
|
|
4375
|
+
init_constants();
|
|
4376
|
+
}
|
|
4377
|
+
});
|
|
4378
|
+
|
|
4209
4379
|
// ../core/dist/utils/ui.js
|
|
4210
4380
|
import chalk from "chalk";
|
|
4211
4381
|
import ora from "ora";
|
|
@@ -4562,24 +4732,33 @@ async function sendWebhook(webhook, ctx) {
|
|
|
4562
4732
|
warn(`Notification failed (${webhook.type}): ${message}`);
|
|
4563
4733
|
}
|
|
4564
4734
|
}
|
|
4735
|
+
function webhookIdentity(wh) {
|
|
4736
|
+
if (wh.type === "telegram")
|
|
4737
|
+
return `telegram:${wh.botToken}:${wh.chatId}`;
|
|
4738
|
+
return `${wh.type}:${wh.url}`;
|
|
4739
|
+
}
|
|
4565
4740
|
async function sendNotifications(config, ctx) {
|
|
4566
|
-
const
|
|
4567
|
-
const
|
|
4568
|
-
|
|
4569
|
-
|
|
4741
|
+
const projectWebhooks = config.notifications?.webhooks ?? [];
|
|
4742
|
+
const globalConfig = loadGlobalNotificationsConfig();
|
|
4743
|
+
const allWebhooks = [...projectWebhooks];
|
|
4744
|
+
if (globalConfig.webhook) {
|
|
4745
|
+
const projectIds = new Set(projectWebhooks.map(webhookIdentity));
|
|
4746
|
+
if (!projectIds.has(webhookIdentity(globalConfig.webhook))) {
|
|
4747
|
+
allWebhooks.push(globalConfig.webhook);
|
|
4748
|
+
}
|
|
4570
4749
|
}
|
|
4571
|
-
if (
|
|
4750
|
+
if (allWebhooks.length === 0) {
|
|
4572
4751
|
return;
|
|
4573
4752
|
}
|
|
4574
|
-
const results = await Promise.allSettled(
|
|
4753
|
+
const results = await Promise.allSettled(allWebhooks.map((wh) => sendWebhook(wh, ctx)));
|
|
4575
4754
|
const sent = results.filter((r) => r.status === "fulfilled").length;
|
|
4576
|
-
|
|
4577
|
-
info(`Sent ${sent}/${total} notifications`);
|
|
4755
|
+
info(`Sent ${sent}/${allWebhooks.length} notifications`);
|
|
4578
4756
|
}
|
|
4579
4757
|
var MAX_QA_SCREENSHOTS_IN_NOTIFICATION;
|
|
4580
4758
|
var init_notify = __esm({
|
|
4581
4759
|
"../core/dist/utils/notify.js"() {
|
|
4582
4760
|
"use strict";
|
|
4761
|
+
init_global_config();
|
|
4583
4762
|
init_ui();
|
|
4584
4763
|
init_github();
|
|
4585
4764
|
MAX_QA_SCREENSHOTS_IN_NOTIFICATION = 3;
|
|
@@ -4616,15 +4795,15 @@ var init_prd_discovery = __esm({
|
|
|
4616
4795
|
});
|
|
4617
4796
|
|
|
4618
4797
|
// ../core/dist/utils/prd-utils.js
|
|
4619
|
-
import * as
|
|
4620
|
-
import * as
|
|
4798
|
+
import * as fs12 from "fs";
|
|
4799
|
+
import * as path11 from "path";
|
|
4621
4800
|
function slugify(name) {
|
|
4622
4801
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4623
4802
|
}
|
|
4624
4803
|
function getNextPrdNumber(prdDir) {
|
|
4625
|
-
if (!
|
|
4804
|
+
if (!fs12.existsSync(prdDir))
|
|
4626
4805
|
return 1;
|
|
4627
|
-
const files =
|
|
4806
|
+
const files = fs12.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
|
|
4628
4807
|
const numbers = files.map((f) => {
|
|
4629
4808
|
const match = f.match(/^(\d+)-/);
|
|
4630
4809
|
return match ? parseInt(match[1], 10) : 0;
|
|
@@ -4632,16 +4811,16 @@ function getNextPrdNumber(prdDir) {
|
|
|
4632
4811
|
return Math.max(0, ...numbers) + 1;
|
|
4633
4812
|
}
|
|
4634
4813
|
function markPrdDone(prdDir, prdFile) {
|
|
4635
|
-
const sourcePath =
|
|
4636
|
-
if (!
|
|
4814
|
+
const sourcePath = path11.join(prdDir, prdFile);
|
|
4815
|
+
if (!fs12.existsSync(sourcePath)) {
|
|
4637
4816
|
return false;
|
|
4638
4817
|
}
|
|
4639
|
-
const doneDir =
|
|
4640
|
-
if (!
|
|
4641
|
-
|
|
4818
|
+
const doneDir = path11.join(prdDir, "done");
|
|
4819
|
+
if (!fs12.existsSync(doneDir)) {
|
|
4820
|
+
fs12.mkdirSync(doneDir, { recursive: true });
|
|
4642
4821
|
}
|
|
4643
|
-
const destPath =
|
|
4644
|
-
|
|
4822
|
+
const destPath = path11.join(doneDir, prdFile);
|
|
4823
|
+
fs12.renameSync(sourcePath, destPath);
|
|
4645
4824
|
return true;
|
|
4646
4825
|
}
|
|
4647
4826
|
var init_prd_utils = __esm({
|
|
@@ -4651,16 +4830,16 @@ var init_prd_utils = __esm({
|
|
|
4651
4830
|
});
|
|
4652
4831
|
|
|
4653
4832
|
// ../core/dist/utils/registry.js
|
|
4654
|
-
import * as
|
|
4655
|
-
import * as
|
|
4656
|
-
import * as
|
|
4833
|
+
import * as fs13 from "fs";
|
|
4834
|
+
import * as os6 from "os";
|
|
4835
|
+
import * as path12 from "path";
|
|
4657
4836
|
function readLegacyRegistryEntries() {
|
|
4658
4837
|
const registryPath = getRegistryPath();
|
|
4659
|
-
if (!
|
|
4838
|
+
if (!fs13.existsSync(registryPath)) {
|
|
4660
4839
|
return [];
|
|
4661
4840
|
}
|
|
4662
4841
|
try {
|
|
4663
|
-
const raw =
|
|
4842
|
+
const raw = fs13.readFileSync(registryPath, "utf-8");
|
|
4664
4843
|
const parsed = JSON.parse(raw);
|
|
4665
4844
|
if (!Array.isArray(parsed)) {
|
|
4666
4845
|
return [];
|
|
@@ -4695,8 +4874,8 @@ function loadRegistryEntriesWithLegacyFallback() {
|
|
|
4695
4874
|
return projectRegistry.getAll();
|
|
4696
4875
|
}
|
|
4697
4876
|
function getRegistryPath() {
|
|
4698
|
-
const base = process.env.NIGHT_WATCH_HOME ||
|
|
4699
|
-
return
|
|
4877
|
+
const base = process.env.NIGHT_WATCH_HOME || path12.join(os6.homedir(), GLOBAL_CONFIG_DIR);
|
|
4878
|
+
return path12.join(base, REGISTRY_FILE_NAME);
|
|
4700
4879
|
}
|
|
4701
4880
|
function loadRegistry() {
|
|
4702
4881
|
return loadRegistryEntriesWithLegacyFallback();
|
|
@@ -4709,7 +4888,7 @@ function saveRegistry(entries) {
|
|
|
4709
4888
|
}
|
|
4710
4889
|
}
|
|
4711
4890
|
function registerProject(projectDir) {
|
|
4712
|
-
const resolvedPath =
|
|
4891
|
+
const resolvedPath = path12.resolve(projectDir);
|
|
4713
4892
|
const { projectRegistry } = getRepositories();
|
|
4714
4893
|
const entries = loadRegistryEntriesWithLegacyFallback();
|
|
4715
4894
|
const existing = entries.find((e) => e.path === resolvedPath);
|
|
@@ -4718,13 +4897,13 @@ function registerProject(projectDir) {
|
|
|
4718
4897
|
}
|
|
4719
4898
|
const name = getProjectName(resolvedPath);
|
|
4720
4899
|
const nameExists = entries.some((e) => e.name === name);
|
|
4721
|
-
const finalName = nameExists ? `${name}-${
|
|
4900
|
+
const finalName = nameExists ? `${name}-${path12.basename(resolvedPath)}` : name;
|
|
4722
4901
|
const entry = { name: finalName, path: resolvedPath };
|
|
4723
4902
|
projectRegistry.upsert(entry);
|
|
4724
4903
|
return entry;
|
|
4725
4904
|
}
|
|
4726
4905
|
function unregisterProject(projectDir) {
|
|
4727
|
-
const resolvedPath =
|
|
4906
|
+
const resolvedPath = path12.resolve(projectDir);
|
|
4728
4907
|
loadRegistryEntriesWithLegacyFallback();
|
|
4729
4908
|
const { projectRegistry } = getRepositories();
|
|
4730
4909
|
return projectRegistry.remove(resolvedPath);
|
|
@@ -4734,7 +4913,7 @@ function validateRegistry() {
|
|
|
4734
4913
|
const valid = [];
|
|
4735
4914
|
const invalid = [];
|
|
4736
4915
|
for (const entry of entries) {
|
|
4737
|
-
if (
|
|
4916
|
+
if (fs13.existsSync(entry.path) && fs13.existsSync(path12.join(entry.path, CONFIG_FILE_NAME))) {
|
|
4738
4917
|
valid.push(entry);
|
|
4739
4918
|
} else {
|
|
4740
4919
|
invalid.push(entry);
|
|
@@ -4922,18 +5101,18 @@ var init_roadmap_parser = __esm({
|
|
|
4922
5101
|
});
|
|
4923
5102
|
|
|
4924
5103
|
// ../core/dist/utils/roadmap-state.js
|
|
4925
|
-
import * as
|
|
4926
|
-
import * as
|
|
5104
|
+
import * as fs14 from "fs";
|
|
5105
|
+
import * as path13 from "path";
|
|
4927
5106
|
function getStateFilePath(prdDir) {
|
|
4928
|
-
return
|
|
5107
|
+
return path13.join(prdDir, STATE_FILE_NAME);
|
|
4929
5108
|
}
|
|
4930
5109
|
function readJsonState(prdDir) {
|
|
4931
5110
|
const statePath = getStateFilePath(prdDir);
|
|
4932
|
-
if (!
|
|
5111
|
+
if (!fs14.existsSync(statePath)) {
|
|
4933
5112
|
return null;
|
|
4934
5113
|
}
|
|
4935
5114
|
try {
|
|
4936
|
-
const content =
|
|
5115
|
+
const content = fs14.readFileSync(statePath, "utf-8");
|
|
4937
5116
|
const parsed = JSON.parse(content);
|
|
4938
5117
|
if (typeof parsed !== "object" || parsed === null) {
|
|
4939
5118
|
return null;
|
|
@@ -4971,11 +5150,11 @@ function saveRoadmapState(prdDir, state) {
|
|
|
4971
5150
|
const { roadmapState } = getRepositories();
|
|
4972
5151
|
roadmapState.save(prdDir, state);
|
|
4973
5152
|
const statePath = getStateFilePath(prdDir);
|
|
4974
|
-
const dir =
|
|
4975
|
-
if (!
|
|
4976
|
-
|
|
5153
|
+
const dir = path13.dirname(statePath);
|
|
5154
|
+
if (!fs14.existsSync(dir)) {
|
|
5155
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
4977
5156
|
}
|
|
4978
|
-
|
|
5157
|
+
fs14.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
4979
5158
|
}
|
|
4980
5159
|
function createEmptyState() {
|
|
4981
5160
|
return {
|
|
@@ -5015,15 +5194,15 @@ var init_roadmap_state = __esm({
|
|
|
5015
5194
|
});
|
|
5016
5195
|
|
|
5017
5196
|
// ../core/dist/templates/slicer-prompt.js
|
|
5018
|
-
import * as
|
|
5019
|
-
import * as
|
|
5197
|
+
import * as fs15 from "fs";
|
|
5198
|
+
import * as path14 from "path";
|
|
5020
5199
|
function loadSlicerTemplate(templateDir) {
|
|
5021
5200
|
if (cachedTemplate) {
|
|
5022
5201
|
return cachedTemplate;
|
|
5023
5202
|
}
|
|
5024
|
-
const templatePath = templateDir ?
|
|
5203
|
+
const templatePath = templateDir ? path14.join(templateDir, "slicer.md") : path14.resolve(__dirname, "..", "..", "templates", "slicer.md");
|
|
5025
5204
|
try {
|
|
5026
|
-
cachedTemplate =
|
|
5205
|
+
cachedTemplate = fs15.readFileSync(templatePath, "utf-8");
|
|
5027
5206
|
return cachedTemplate;
|
|
5028
5207
|
} catch (error2) {
|
|
5029
5208
|
console.warn(`Warning: Could not load slicer template from ${templatePath}, using default:`, error2 instanceof Error ? error2.message : String(error2));
|
|
@@ -5048,7 +5227,7 @@ function createSlicerPromptVars(title, section, description, prdDir, prdFilename
|
|
|
5048
5227
|
title,
|
|
5049
5228
|
section,
|
|
5050
5229
|
description: description || "(No description provided)",
|
|
5051
|
-
outputFilePath:
|
|
5230
|
+
outputFilePath: path14.join(prdDir, prdFilename),
|
|
5052
5231
|
prdDir
|
|
5053
5232
|
};
|
|
5054
5233
|
}
|
|
@@ -5142,8 +5321,8 @@ DO NOT forget to write the file.
|
|
|
5142
5321
|
});
|
|
5143
5322
|
|
|
5144
5323
|
// ../core/dist/utils/roadmap-scanner.js
|
|
5145
|
-
import * as
|
|
5146
|
-
import * as
|
|
5324
|
+
import * as fs16 from "fs";
|
|
5325
|
+
import * as path15 from "path";
|
|
5147
5326
|
import { spawn } from "child_process";
|
|
5148
5327
|
import { createHash as createHash3 } from "crypto";
|
|
5149
5328
|
function normalizeAuditSeverity(raw) {
|
|
@@ -5244,11 +5423,11 @@ function auditFindingToRoadmapItem(finding) {
|
|
|
5244
5423
|
};
|
|
5245
5424
|
}
|
|
5246
5425
|
function collectAuditPlannerItems(projectDir) {
|
|
5247
|
-
const reportPath =
|
|
5248
|
-
if (!
|
|
5426
|
+
const reportPath = path15.join(projectDir, "logs", "audit-report.md");
|
|
5427
|
+
if (!fs16.existsSync(reportPath)) {
|
|
5249
5428
|
return [];
|
|
5250
5429
|
}
|
|
5251
|
-
const reportContent =
|
|
5430
|
+
const reportContent = fs16.readFileSync(reportPath, "utf-8");
|
|
5252
5431
|
if (!reportContent.trim() || /\bNO_ISSUES_FOUND\b/.test(reportContent)) {
|
|
5253
5432
|
return [];
|
|
5254
5433
|
}
|
|
@@ -5257,9 +5436,9 @@ function collectAuditPlannerItems(projectDir) {
|
|
|
5257
5436
|
return findings.map(auditFindingToRoadmapItem);
|
|
5258
5437
|
}
|
|
5259
5438
|
function getRoadmapStatus(projectDir, config) {
|
|
5260
|
-
const roadmapPath =
|
|
5439
|
+
const roadmapPath = path15.join(projectDir, config.roadmapScanner.roadmapPath);
|
|
5261
5440
|
const scannerEnabled = config.roadmapScanner.enabled;
|
|
5262
|
-
if (!
|
|
5441
|
+
if (!fs16.existsSync(roadmapPath)) {
|
|
5263
5442
|
return {
|
|
5264
5443
|
found: false,
|
|
5265
5444
|
enabled: scannerEnabled,
|
|
@@ -5270,9 +5449,9 @@ function getRoadmapStatus(projectDir, config) {
|
|
|
5270
5449
|
items: []
|
|
5271
5450
|
};
|
|
5272
5451
|
}
|
|
5273
|
-
const content =
|
|
5452
|
+
const content = fs16.readFileSync(roadmapPath, "utf-8");
|
|
5274
5453
|
const items = parseRoadmap(content);
|
|
5275
|
-
const prdDir =
|
|
5454
|
+
const prdDir = path15.join(projectDir, config.prdDir);
|
|
5276
5455
|
const state = loadRoadmapState(prdDir);
|
|
5277
5456
|
const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
|
|
5278
5457
|
const statusItems = items.map((item) => {
|
|
@@ -5309,10 +5488,10 @@ function getRoadmapStatus(projectDir, config) {
|
|
|
5309
5488
|
}
|
|
5310
5489
|
function scanExistingPrdSlugs(prdDir) {
|
|
5311
5490
|
const slugs = /* @__PURE__ */ new Set();
|
|
5312
|
-
if (!
|
|
5491
|
+
if (!fs16.existsSync(prdDir)) {
|
|
5313
5492
|
return slugs;
|
|
5314
5493
|
}
|
|
5315
|
-
const files =
|
|
5494
|
+
const files = fs16.readdirSync(prdDir);
|
|
5316
5495
|
for (const file of files) {
|
|
5317
5496
|
if (!file.endsWith(".md")) {
|
|
5318
5497
|
continue;
|
|
@@ -5348,20 +5527,20 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
|
|
|
5348
5527
|
const nextNum = getNextPrdNumber(prdDir);
|
|
5349
5528
|
const padded = String(nextNum).padStart(2, "0");
|
|
5350
5529
|
const filename = `${padded}-${itemSlug}.md`;
|
|
5351
|
-
const filePath =
|
|
5352
|
-
if (!
|
|
5353
|
-
|
|
5530
|
+
const filePath = path15.join(prdDir, filename);
|
|
5531
|
+
if (!fs16.existsSync(prdDir)) {
|
|
5532
|
+
fs16.mkdirSync(prdDir, { recursive: true });
|
|
5354
5533
|
}
|
|
5355
5534
|
const promptVars = createSlicerPromptVars(item.title, item.section, item.description, prdDir, filename);
|
|
5356
5535
|
const prompt2 = renderSlicerPrompt(promptVars);
|
|
5357
5536
|
const provider = resolveJobProvider(config, "slicer");
|
|
5358
5537
|
const providerArgs = buildProviderArgs(provider, prompt2, projectDir);
|
|
5359
|
-
const logDir =
|
|
5360
|
-
if (!
|
|
5361
|
-
|
|
5538
|
+
const logDir = path15.join(projectDir, "logs");
|
|
5539
|
+
if (!fs16.existsSync(logDir)) {
|
|
5540
|
+
fs16.mkdirSync(logDir, { recursive: true });
|
|
5362
5541
|
}
|
|
5363
|
-
const logFile =
|
|
5364
|
-
const logStream =
|
|
5542
|
+
const logFile = path15.join(logDir, `slicer-${itemSlug}.log`);
|
|
5543
|
+
const logStream = fs16.createWriteStream(logFile, { flags: "w" });
|
|
5365
5544
|
logStream.on("error", () => {
|
|
5366
5545
|
});
|
|
5367
5546
|
return new Promise((resolve10) => {
|
|
@@ -5398,7 +5577,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
|
|
|
5398
5577
|
});
|
|
5399
5578
|
return;
|
|
5400
5579
|
}
|
|
5401
|
-
if (!
|
|
5580
|
+
if (!fs16.existsSync(filePath)) {
|
|
5402
5581
|
resolve10({
|
|
5403
5582
|
sliced: false,
|
|
5404
5583
|
error: `Provider did not create expected file: ${filePath}`,
|
|
@@ -5421,23 +5600,23 @@ async function sliceNextItem(projectDir, config) {
|
|
|
5421
5600
|
error: "Roadmap scanner is disabled"
|
|
5422
5601
|
};
|
|
5423
5602
|
}
|
|
5424
|
-
const roadmapPath =
|
|
5603
|
+
const roadmapPath = path15.join(projectDir, config.roadmapScanner.roadmapPath);
|
|
5425
5604
|
const auditItems = collectAuditPlannerItems(projectDir);
|
|
5426
|
-
const roadmapExists =
|
|
5605
|
+
const roadmapExists = fs16.existsSync(roadmapPath);
|
|
5427
5606
|
if (!roadmapExists && auditItems.length === 0) {
|
|
5428
5607
|
return {
|
|
5429
5608
|
sliced: false,
|
|
5430
5609
|
error: "No pending items to process"
|
|
5431
5610
|
};
|
|
5432
5611
|
}
|
|
5433
|
-
const roadmapItems = roadmapExists ? parseRoadmap(
|
|
5612
|
+
const roadmapItems = roadmapExists ? parseRoadmap(fs16.readFileSync(roadmapPath, "utf-8")) : [];
|
|
5434
5613
|
if (roadmapExists && roadmapItems.length === 0 && auditItems.length === 0) {
|
|
5435
5614
|
return {
|
|
5436
5615
|
sliced: false,
|
|
5437
5616
|
error: "No items in roadmap"
|
|
5438
5617
|
};
|
|
5439
5618
|
}
|
|
5440
|
-
const prdDir =
|
|
5619
|
+
const prdDir = path15.join(projectDir, config.prdDir);
|
|
5441
5620
|
const state = loadRoadmapState(prdDir);
|
|
5442
5621
|
const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
|
|
5443
5622
|
const pickEligibleItem = (items) => {
|
|
@@ -5608,8 +5787,8 @@ var init_shell = __esm({
|
|
|
5608
5787
|
});
|
|
5609
5788
|
|
|
5610
5789
|
// ../core/dist/utils/scheduling.js
|
|
5611
|
-
import * as
|
|
5612
|
-
import * as
|
|
5790
|
+
import * as fs17 from "fs";
|
|
5791
|
+
import * as path16 from "path";
|
|
5613
5792
|
function normalizeSchedulingPriority(priority) {
|
|
5614
5793
|
if (!Number.isFinite(priority)) {
|
|
5615
5794
|
return DEFAULT_SCHEDULING_PRIORITY;
|
|
@@ -5628,12 +5807,14 @@ function isJobTypeEnabled(config, jobType) {
|
|
|
5628
5807
|
return config.audit.enabled;
|
|
5629
5808
|
case "slicer":
|
|
5630
5809
|
return config.roadmapScanner.enabled;
|
|
5810
|
+
case "analytics":
|
|
5811
|
+
return config.analytics.enabled;
|
|
5631
5812
|
default:
|
|
5632
5813
|
return true;
|
|
5633
5814
|
}
|
|
5634
5815
|
}
|
|
5635
5816
|
function loadPeerConfig(projectPath) {
|
|
5636
|
-
if (!
|
|
5817
|
+
if (!fs17.existsSync(projectPath) || !fs17.existsSync(path16.join(projectPath, CONFIG_FILE_NAME))) {
|
|
5637
5818
|
return null;
|
|
5638
5819
|
}
|
|
5639
5820
|
try {
|
|
@@ -5644,9 +5825,9 @@ function loadPeerConfig(projectPath) {
|
|
|
5644
5825
|
}
|
|
5645
5826
|
function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
|
|
5646
5827
|
const peers = /* @__PURE__ */ new Map();
|
|
5647
|
-
const currentPath =
|
|
5828
|
+
const currentPath = path16.resolve(currentProjectDir);
|
|
5648
5829
|
const addPeer = (projectPath, config) => {
|
|
5649
|
-
const resolvedPath =
|
|
5830
|
+
const resolvedPath = path16.resolve(projectPath);
|
|
5650
5831
|
if (!isJobTypeEnabled(config, jobType)) {
|
|
5651
5832
|
return;
|
|
5652
5833
|
}
|
|
@@ -5654,12 +5835,12 @@ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
|
|
|
5654
5835
|
path: resolvedPath,
|
|
5655
5836
|
config,
|
|
5656
5837
|
schedulingPriority: normalizeSchedulingPriority(config.schedulingPriority),
|
|
5657
|
-
sortKey: `${
|
|
5838
|
+
sortKey: `${path16.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
|
|
5658
5839
|
});
|
|
5659
5840
|
};
|
|
5660
5841
|
addPeer(currentPath, currentConfig);
|
|
5661
5842
|
for (const entry of loadRegistry()) {
|
|
5662
|
-
const resolvedPath =
|
|
5843
|
+
const resolvedPath = path16.resolve(entry.path);
|
|
5663
5844
|
if (resolvedPath === currentPath || peers.has(resolvedPath)) {
|
|
5664
5845
|
continue;
|
|
5665
5846
|
}
|
|
@@ -5677,7 +5858,7 @@ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
|
|
|
5677
5858
|
}
|
|
5678
5859
|
function getSchedulingPlan(projectDir, config, jobType) {
|
|
5679
5860
|
const peers = collectSchedulingPeers(projectDir, config, jobType);
|
|
5680
|
-
const currentPath =
|
|
5861
|
+
const currentPath = path16.resolve(projectDir);
|
|
5681
5862
|
const slotIndex = Math.max(0, peers.findIndex((peer) => peer.path === currentPath));
|
|
5682
5863
|
const peerCount = Math.max(1, peers.length);
|
|
5683
5864
|
const balancedDelayMinutes = peerCount <= 1 ? 0 : Math.floor(slotIndex * 60 / peerCount);
|
|
@@ -5769,8 +5950,8 @@ var init_webhook_validator = __esm({
|
|
|
5769
5950
|
|
|
5770
5951
|
// ../core/dist/utils/worktree-manager.js
|
|
5771
5952
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
5772
|
-
import * as
|
|
5773
|
-
import * as
|
|
5953
|
+
import * as fs18 from "fs";
|
|
5954
|
+
import * as path17 from "path";
|
|
5774
5955
|
function gitExec(args, cwd, logFile) {
|
|
5775
5956
|
try {
|
|
5776
5957
|
const result = execFileSync4("git", args, {
|
|
@@ -5780,7 +5961,7 @@ function gitExec(args, cwd, logFile) {
|
|
|
5780
5961
|
});
|
|
5781
5962
|
if (logFile && result) {
|
|
5782
5963
|
try {
|
|
5783
|
-
|
|
5964
|
+
fs18.appendFileSync(logFile, result);
|
|
5784
5965
|
} catch {
|
|
5785
5966
|
}
|
|
5786
5967
|
}
|
|
@@ -5789,7 +5970,7 @@ function gitExec(args, cwd, logFile) {
|
|
|
5789
5970
|
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
5790
5971
|
if (logFile) {
|
|
5791
5972
|
try {
|
|
5792
|
-
|
|
5973
|
+
fs18.appendFileSync(logFile, errorMessage + "\n");
|
|
5793
5974
|
} catch {
|
|
5794
5975
|
}
|
|
5795
5976
|
}
|
|
@@ -5814,11 +5995,11 @@ function branchExistsRemotely(projectDir, branchName) {
|
|
|
5814
5995
|
}
|
|
5815
5996
|
function prepareBranchWorktree(options) {
|
|
5816
5997
|
const { projectDir, worktreeDir, branchName, defaultBranch, logFile } = options;
|
|
5817
|
-
if (
|
|
5998
|
+
if (fs18.existsSync(worktreeDir)) {
|
|
5818
5999
|
const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
|
|
5819
6000
|
if (!isRegistered) {
|
|
5820
6001
|
try {
|
|
5821
|
-
|
|
6002
|
+
fs18.rmSync(worktreeDir, { recursive: true, force: true });
|
|
5822
6003
|
} catch {
|
|
5823
6004
|
}
|
|
5824
6005
|
}
|
|
@@ -5857,11 +6038,11 @@ function prepareBranchWorktree(options) {
|
|
|
5857
6038
|
}
|
|
5858
6039
|
function prepareDetachedWorktree(options) {
|
|
5859
6040
|
const { projectDir, worktreeDir, defaultBranch, logFile } = options;
|
|
5860
|
-
if (
|
|
6041
|
+
if (fs18.existsSync(worktreeDir)) {
|
|
5861
6042
|
const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
|
|
5862
6043
|
if (!isRegistered) {
|
|
5863
6044
|
try {
|
|
5864
|
-
|
|
6045
|
+
fs18.rmSync(worktreeDir, { recursive: true, force: true });
|
|
5865
6046
|
} catch {
|
|
5866
6047
|
}
|
|
5867
6048
|
}
|
|
@@ -5903,7 +6084,7 @@ function isWorktreeRegistered(projectDir, worktreePath) {
|
|
|
5903
6084
|
}
|
|
5904
6085
|
}
|
|
5905
6086
|
function cleanupWorktrees(projectDir, scope) {
|
|
5906
|
-
const projectName =
|
|
6087
|
+
const projectName = path17.basename(projectDir);
|
|
5907
6088
|
const matchToken = scope ? scope : `${projectName}-nw`;
|
|
5908
6089
|
const removed = [];
|
|
5909
6090
|
try {
|
|
@@ -5938,16 +6119,16 @@ var init_worktree_manager = __esm({
|
|
|
5938
6119
|
});
|
|
5939
6120
|
|
|
5940
6121
|
// ../core/dist/utils/job-queue.js
|
|
5941
|
-
import * as
|
|
5942
|
-
import * as
|
|
6122
|
+
import * as os7 from "os";
|
|
6123
|
+
import * as path18 from "path";
|
|
5943
6124
|
import Database7 from "better-sqlite3";
|
|
5944
6125
|
function getStateDbPath() {
|
|
5945
|
-
const base = process.env.NIGHT_WATCH_HOME ||
|
|
5946
|
-
return
|
|
6126
|
+
const base = process.env.NIGHT_WATCH_HOME || path18.join(os7.homedir(), GLOBAL_CONFIG_DIR);
|
|
6127
|
+
return path18.join(base, STATE_DB_FILE_NAME);
|
|
5947
6128
|
}
|
|
5948
6129
|
function getQueueLockPath() {
|
|
5949
|
-
const base = process.env.NIGHT_WATCH_HOME ||
|
|
5950
|
-
return
|
|
6130
|
+
const base = process.env.NIGHT_WATCH_HOME || path18.join(os7.homedir(), GLOBAL_CONFIG_DIR);
|
|
6131
|
+
return path18.join(base, QUEUE_LOCK_FILE_NAME);
|
|
5951
6132
|
}
|
|
5952
6133
|
function openDb() {
|
|
5953
6134
|
const dbPath = getStateDbPath();
|
|
@@ -5987,6 +6168,8 @@ function getLockPathForJob(projectPath, jobType) {
|
|
|
5987
6168
|
return auditLockPath(projectPath);
|
|
5988
6169
|
case "slicer":
|
|
5989
6170
|
return plannerLockPath(projectPath);
|
|
6171
|
+
case "analytics":
|
|
6172
|
+
return analyticsLockPath(projectPath);
|
|
5990
6173
|
}
|
|
5991
6174
|
}
|
|
5992
6175
|
function reconcileStaleRunningJobs(db) {
|
|
@@ -6166,7 +6349,10 @@ function fitsProviderCapacity(candidate, config, inFlightByBucket) {
|
|
|
6166
6349
|
}
|
|
6167
6350
|
const bucketConfig = config?.providerBuckets?.[bucketKey];
|
|
6168
6351
|
if (!bucketConfig) {
|
|
6169
|
-
logger.debug("Capacity check skipped: bucket not configured", {
|
|
6352
|
+
logger.debug("Capacity check skipped: bucket not configured", {
|
|
6353
|
+
id: candidate.id,
|
|
6354
|
+
bucket: bucketKey
|
|
6355
|
+
});
|
|
6170
6356
|
return true;
|
|
6171
6357
|
}
|
|
6172
6358
|
const inFlightCount = inFlightByBucket[bucketKey] ?? 0;
|
|
@@ -6197,7 +6383,10 @@ function dispatchNextJob(config) {
|
|
|
6197
6383
|
const runningCount = running?.count ?? 0;
|
|
6198
6384
|
logger.debug("Dispatch attempt", { mode, runningCount, maxConcurrency });
|
|
6199
6385
|
if (runningCount >= maxConcurrency) {
|
|
6200
|
-
logger.info("Dispatch skipped: global concurrency limit reached", {
|
|
6386
|
+
logger.info("Dispatch skipped: global concurrency limit reached", {
|
|
6387
|
+
runningCount,
|
|
6388
|
+
maxConcurrency
|
|
6389
|
+
});
|
|
6201
6390
|
return null;
|
|
6202
6391
|
}
|
|
6203
6392
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -6223,7 +6412,9 @@ function dispatchNextJob(config) {
|
|
|
6223
6412
|
logger.debug("Dispatch skipped: no pending jobs");
|
|
6224
6413
|
return null;
|
|
6225
6414
|
}
|
|
6226
|
-
logger.debug("Provider-aware dispatch: evaluating candidates", {
|
|
6415
|
+
logger.debug("Provider-aware dispatch: evaluating candidates", {
|
|
6416
|
+
candidateCount: candidates.length
|
|
6417
|
+
});
|
|
6227
6418
|
const inFlightByBucket = getInFlightCountByBucket(db);
|
|
6228
6419
|
for (const candidate of candidates) {
|
|
6229
6420
|
if (fitsProviderCapacity(candidate, config, inFlightByBucket)) {
|
|
@@ -6299,7 +6490,11 @@ function clearQueue(filter, force) {
|
|
|
6299
6490
|
} else {
|
|
6300
6491
|
result = db.prepare(`DELETE FROM job_queue WHERE status IN ${statuses}`).run();
|
|
6301
6492
|
}
|
|
6302
|
-
logger.info("Queue cleared", {
|
|
6493
|
+
logger.info("Queue cleared", {
|
|
6494
|
+
count: result.changes,
|
|
6495
|
+
filter: filter ?? "all",
|
|
6496
|
+
force: force ?? false
|
|
6497
|
+
});
|
|
6303
6498
|
return result.changes;
|
|
6304
6499
|
} finally {
|
|
6305
6500
|
db.close();
|
|
@@ -6432,6 +6627,192 @@ var init_job_queue = __esm({
|
|
|
6432
6627
|
}
|
|
6433
6628
|
});
|
|
6434
6629
|
|
|
6630
|
+
// ../core/dist/analytics/amplitude-client.js
|
|
6631
|
+
function buildAuthHeader(apiKey, secretKey) {
|
|
6632
|
+
return `Basic ${Buffer.from(`${apiKey}:${secretKey}`).toString("base64")}`;
|
|
6633
|
+
}
|
|
6634
|
+
function buildDateRange(lookbackDays) {
|
|
6635
|
+
const end = /* @__PURE__ */ new Date();
|
|
6636
|
+
const start = /* @__PURE__ */ new Date();
|
|
6637
|
+
start.setDate(start.getDate() - lookbackDays);
|
|
6638
|
+
const fmt = (d) => d.toISOString().slice(0, 10).replace(/-/g, "");
|
|
6639
|
+
return { start: fmt(start), end: fmt(end) };
|
|
6640
|
+
}
|
|
6641
|
+
async function amplitudeFetch(url, authHeader, label2) {
|
|
6642
|
+
logger2.debug(`Fetching ${label2}`, { url });
|
|
6643
|
+
const response = await fetch(url, {
|
|
6644
|
+
headers: { Authorization: authHeader }
|
|
6645
|
+
});
|
|
6646
|
+
if (!response.ok) {
|
|
6647
|
+
if (response.status === 401) {
|
|
6648
|
+
throw new Error(`Amplitude authentication failed (401). Check your API Key and Secret Key.`);
|
|
6649
|
+
}
|
|
6650
|
+
if (response.status === 429) {
|
|
6651
|
+
throw new Error(`Amplitude rate limit exceeded (429). Try again later.`);
|
|
6652
|
+
}
|
|
6653
|
+
throw new Error(`Amplitude API error: ${response.status} ${response.statusText}`);
|
|
6654
|
+
}
|
|
6655
|
+
return response.json();
|
|
6656
|
+
}
|
|
6657
|
+
async function fetchAmplitudeData(apiKey, secretKey, lookbackDays) {
|
|
6658
|
+
const authHeader = buildAuthHeader(apiKey, secretKey);
|
|
6659
|
+
const { start, end } = buildDateRange(lookbackDays);
|
|
6660
|
+
logger2.info("Fetching Amplitude data", { lookbackDays, start, end });
|
|
6661
|
+
const baseUrl = "https://amplitude.com/api/2";
|
|
6662
|
+
const allEventsParam = encodeURIComponent('{"event_type":"_all"}');
|
|
6663
|
+
const [activeUsers, eventSegmentation, retention, userSessions] = await Promise.allSettled([
|
|
6664
|
+
amplitudeFetch(`${baseUrl}/users/active?start=${start}&end=${end}`, authHeader, "active users"),
|
|
6665
|
+
amplitudeFetch(`${baseUrl}/events/segmentation?start=${start}&end=${end}&e=${allEventsParam}`, authHeader, "event segmentation"),
|
|
6666
|
+
amplitudeFetch(`${baseUrl}/retention?se=${allEventsParam}&re=${allEventsParam}&start=${start}&end=${end}`, authHeader, "retention"),
|
|
6667
|
+
amplitudeFetch(`${baseUrl}/sessions/average?start=${start}&end=${end}`, authHeader, "user sessions")
|
|
6668
|
+
]);
|
|
6669
|
+
const settled = [activeUsers, eventSegmentation, retention, userSessions];
|
|
6670
|
+
const labels = ["active users", "event segmentation", "retention", "user sessions"];
|
|
6671
|
+
if (settled.every((r) => r.status === "rejected")) {
|
|
6672
|
+
throw settled[0].reason;
|
|
6673
|
+
}
|
|
6674
|
+
const extract = (result, label2) => {
|
|
6675
|
+
if (result.status === "fulfilled")
|
|
6676
|
+
return result.value;
|
|
6677
|
+
logger2.warn(`Failed to fetch ${label2}`, { error: String(result.reason) });
|
|
6678
|
+
return null;
|
|
6679
|
+
};
|
|
6680
|
+
return {
|
|
6681
|
+
activeUsers: extract(activeUsers, labels[0]),
|
|
6682
|
+
eventSegmentation: extract(eventSegmentation, labels[1]),
|
|
6683
|
+
retention: extract(retention, labels[2]),
|
|
6684
|
+
userSessions: extract(userSessions, labels[3]),
|
|
6685
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6686
|
+
lookbackDays
|
|
6687
|
+
};
|
|
6688
|
+
}
|
|
6689
|
+
var logger2;
|
|
6690
|
+
var init_amplitude_client = __esm({
|
|
6691
|
+
"../core/dist/analytics/amplitude-client.js"() {
|
|
6692
|
+
"use strict";
|
|
6693
|
+
init_logger();
|
|
6694
|
+
logger2 = createLogger("amplitude-client");
|
|
6695
|
+
}
|
|
6696
|
+
});
|
|
6697
|
+
|
|
6698
|
+
// ../core/dist/analytics/analytics-runner.js
|
|
6699
|
+
import * as fs19 from "fs";
|
|
6700
|
+
import * as os8 from "os";
|
|
6701
|
+
import * as path19 from "path";
|
|
6702
|
+
function parseIssuesFromResponse(text) {
|
|
6703
|
+
const start = text.indexOf("[");
|
|
6704
|
+
const end = text.lastIndexOf("]");
|
|
6705
|
+
if (start === -1 || end === -1 || end <= start)
|
|
6706
|
+
return [];
|
|
6707
|
+
try {
|
|
6708
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
6709
|
+
if (!Array.isArray(parsed))
|
|
6710
|
+
return [];
|
|
6711
|
+
return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.title === "string" && typeof item.body === "string");
|
|
6712
|
+
} catch {
|
|
6713
|
+
logger3.warn("Failed to parse AI response as JSON");
|
|
6714
|
+
return [];
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
async function runAnalytics(config, projectDir) {
|
|
6718
|
+
const apiKey = config.providerEnv?.AMPLITUDE_API_KEY;
|
|
6719
|
+
const secretKey = config.providerEnv?.AMPLITUDE_SECRET_KEY;
|
|
6720
|
+
if (!apiKey || !secretKey) {
|
|
6721
|
+
throw new Error("AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics");
|
|
6722
|
+
}
|
|
6723
|
+
logger3.info("Fetching Amplitude data", { lookbackDays: config.analytics.lookbackDays });
|
|
6724
|
+
const data = await fetchAmplitudeData(apiKey, secretKey, config.analytics.lookbackDays);
|
|
6725
|
+
const systemPrompt = config.analytics.analysisPrompt?.trim() || DEFAULT_ANALYTICS_PROMPT;
|
|
6726
|
+
const prompt2 = `${systemPrompt}
|
|
6727
|
+
|
|
6728
|
+
--- AMPLITUDE DATA ---
|
|
6729
|
+
${JSON.stringify(data, null, 2)}`;
|
|
6730
|
+
const tmpDir = fs19.mkdtempSync(path19.join(os8.tmpdir(), "nw-analytics-"));
|
|
6731
|
+
const promptFile = path19.join(tmpDir, "analytics-prompt.md");
|
|
6732
|
+
fs19.writeFileSync(promptFile, prompt2, "utf-8");
|
|
6733
|
+
try {
|
|
6734
|
+
const provider = resolveJobProvider(config, "analytics");
|
|
6735
|
+
const providerCmd = PROVIDER_COMMANDS[provider];
|
|
6736
|
+
let scriptContent;
|
|
6737
|
+
if (provider === "claude") {
|
|
6738
|
+
const modelId = CLAUDE_MODEL_IDS[config.claudeModel ?? "sonnet"];
|
|
6739
|
+
scriptContent = `#!/usr/bin/env bash
|
|
6740
|
+
set -euo pipefail
|
|
6741
|
+
${providerCmd} -p "$(cat ${promptFile})" --model ${modelId} --dangerously-skip-permissions 2>&1
|
|
6742
|
+
`;
|
|
6743
|
+
} else {
|
|
6744
|
+
scriptContent = `#!/usr/bin/env bash
|
|
6745
|
+
set -euo pipefail
|
|
6746
|
+
${providerCmd} exec --yolo "$(cat ${promptFile})" 2>&1
|
|
6747
|
+
`;
|
|
6748
|
+
}
|
|
6749
|
+
const scriptFile = path19.join(tmpDir, "run-analytics.sh");
|
|
6750
|
+
fs19.writeFileSync(scriptFile, scriptContent, { mode: 493 });
|
|
6751
|
+
const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptFile, [], config.providerEnv ?? {});
|
|
6752
|
+
if (exitCode !== 0) {
|
|
6753
|
+
throw new Error(`AI provider exited with code ${exitCode}: ${stderr || stdout}`);
|
|
6754
|
+
}
|
|
6755
|
+
const fullOutput = `${stdout}
|
|
6756
|
+
${stderr}`;
|
|
6757
|
+
const issues = parseIssuesFromResponse(fullOutput);
|
|
6758
|
+
if (issues.length === 0) {
|
|
6759
|
+
logger3.info("No actionable insights found");
|
|
6760
|
+
return { issuesCreated: 0, summary: "No actionable insights found" };
|
|
6761
|
+
}
|
|
6762
|
+
const boardProvider = createBoardProvider(config.boardProvider, projectDir);
|
|
6763
|
+
const targetColumn = config.analytics.targetColumn;
|
|
6764
|
+
let created = 0;
|
|
6765
|
+
for (const issue of issues) {
|
|
6766
|
+
try {
|
|
6767
|
+
await boardProvider.createIssue({
|
|
6768
|
+
title: issue.title,
|
|
6769
|
+
body: issue.body,
|
|
6770
|
+
column: targetColumn,
|
|
6771
|
+
labels: issue.labels ?? ["analytics"]
|
|
6772
|
+
});
|
|
6773
|
+
created++;
|
|
6774
|
+
logger3.info("Created board issue", { title: issue.title, column: targetColumn });
|
|
6775
|
+
} catch (err) {
|
|
6776
|
+
logger3.error("Failed to create board issue", {
|
|
6777
|
+
title: issue.title,
|
|
6778
|
+
error: String(err)
|
|
6779
|
+
});
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
return {
|
|
6783
|
+
issuesCreated: created,
|
|
6784
|
+
summary: `Created ${created} issue(s) from analytics insights`
|
|
6785
|
+
};
|
|
6786
|
+
} finally {
|
|
6787
|
+
try {
|
|
6788
|
+
fs19.rmSync(tmpDir, { recursive: true, force: true });
|
|
6789
|
+
} catch {
|
|
6790
|
+
}
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6793
|
+
var logger3;
|
|
6794
|
+
var init_analytics_runner = __esm({
|
|
6795
|
+
"../core/dist/analytics/analytics-runner.js"() {
|
|
6796
|
+
"use strict";
|
|
6797
|
+
init_factory();
|
|
6798
|
+
init_config();
|
|
6799
|
+
init_constants();
|
|
6800
|
+
init_shell();
|
|
6801
|
+
init_logger();
|
|
6802
|
+
init_amplitude_client();
|
|
6803
|
+
logger3 = createLogger("analytics");
|
|
6804
|
+
}
|
|
6805
|
+
});
|
|
6806
|
+
|
|
6807
|
+
// ../core/dist/analytics/index.js
|
|
6808
|
+
var init_analytics = __esm({
|
|
6809
|
+
"../core/dist/analytics/index.js"() {
|
|
6810
|
+
"use strict";
|
|
6811
|
+
init_amplitude_client();
|
|
6812
|
+
init_analytics_runner();
|
|
6813
|
+
}
|
|
6814
|
+
});
|
|
6815
|
+
|
|
6435
6816
|
// ../core/dist/templates/prd-template.js
|
|
6436
6817
|
function renderDependsOn(deps) {
|
|
6437
6818
|
if (deps.length === 0) {
|
|
@@ -6608,6 +6989,7 @@ sequenceDiagram
|
|
|
6608
6989
|
// ../core/dist/index.js
|
|
6609
6990
|
var dist_exports = {};
|
|
6610
6991
|
__export(dist_exports, {
|
|
6992
|
+
ANALYTICS_LOG_NAME: () => ANALYTICS_LOG_NAME,
|
|
6611
6993
|
AUDIT_LOG_NAME: () => AUDIT_LOG_NAME,
|
|
6612
6994
|
BOARD_COLUMNS: () => BOARD_COLUMNS,
|
|
6613
6995
|
BUILT_IN_PRESETS: () => BUILT_IN_PRESETS,
|
|
@@ -6619,6 +7001,13 @@ __export(dist_exports, {
|
|
|
6619
7001
|
CONFIG_FILE_NAME: () => CONFIG_FILE_NAME,
|
|
6620
7002
|
CRONTAB_MARKER_PREFIX: () => CRONTAB_MARKER_PREFIX,
|
|
6621
7003
|
DATABASE_TOKEN: () => DATABASE_TOKEN,
|
|
7004
|
+
DEFAULT_ANALYTICS: () => DEFAULT_ANALYTICS,
|
|
7005
|
+
DEFAULT_ANALYTICS_ENABLED: () => DEFAULT_ANALYTICS_ENABLED,
|
|
7006
|
+
DEFAULT_ANALYTICS_LOOKBACK_DAYS: () => DEFAULT_ANALYTICS_LOOKBACK_DAYS,
|
|
7007
|
+
DEFAULT_ANALYTICS_MAX_RUNTIME: () => DEFAULT_ANALYTICS_MAX_RUNTIME,
|
|
7008
|
+
DEFAULT_ANALYTICS_PROMPT: () => DEFAULT_ANALYTICS_PROMPT,
|
|
7009
|
+
DEFAULT_ANALYTICS_SCHEDULE: () => DEFAULT_ANALYTICS_SCHEDULE,
|
|
7010
|
+
DEFAULT_ANALYTICS_TARGET_COLUMN: () => DEFAULT_ANALYTICS_TARGET_COLUMN,
|
|
6622
7011
|
DEFAULT_AUDIT: () => DEFAULT_AUDIT,
|
|
6623
7012
|
DEFAULT_AUDIT_ENABLED: () => DEFAULT_AUDIT_ENABLED,
|
|
6624
7013
|
DEFAULT_AUDIT_MAX_RUNTIME: () => DEFAULT_AUDIT_MAX_RUNTIME,
|
|
@@ -6674,6 +7063,7 @@ __export(dist_exports, {
|
|
|
6674
7063
|
EXECUTOR_LOG_FILE: () => EXECUTOR_LOG_FILE,
|
|
6675
7064
|
EXECUTOR_LOG_NAME: () => EXECUTOR_LOG_NAME,
|
|
6676
7065
|
GLOBAL_CONFIG_DIR: () => GLOBAL_CONFIG_DIR,
|
|
7066
|
+
GLOBAL_NOTIFICATIONS_FILE_NAME: () => GLOBAL_NOTIFICATIONS_FILE_NAME,
|
|
6677
7067
|
HISTORY_FILE_NAME: () => HISTORY_FILE_NAME,
|
|
6678
7068
|
HORIZON_LABELS: () => HORIZON_LABELS,
|
|
6679
7069
|
HORIZON_LABEL_INFO: () => HORIZON_LABEL_INFO,
|
|
@@ -6705,6 +7095,7 @@ __export(dist_exports, {
|
|
|
6705
7095
|
acquireLock: () => acquireLock,
|
|
6706
7096
|
addDelayToIsoString: () => addDelayToIsoString,
|
|
6707
7097
|
addEntry: () => addEntry,
|
|
7098
|
+
analyticsLockPath: () => analyticsLockPath,
|
|
6708
7099
|
auditLockPath: () => auditLockPath,
|
|
6709
7100
|
buildDescription: () => buildDescription,
|
|
6710
7101
|
calculateStringSimilarity: () => calculateStringSimilarity,
|
|
@@ -6755,6 +7146,7 @@ __export(dist_exports, {
|
|
|
6755
7146
|
extractPriority: () => extractPriority,
|
|
6756
7147
|
extractQaScreenshotUrls: () => extractQaScreenshotUrls,
|
|
6757
7148
|
extractSummary: () => extractSummary,
|
|
7149
|
+
fetchAmplitudeData: () => fetchAmplitudeData,
|
|
6758
7150
|
fetchLatestQaCommentBody: () => fetchLatestQaCommentBody,
|
|
6759
7151
|
fetchPrDetails: () => fetchPrDetails,
|
|
6760
7152
|
fetchPrDetailsByNumber: () => fetchPrDetailsByNumber,
|
|
@@ -6825,6 +7217,7 @@ __export(dist_exports, {
|
|
|
6825
7217
|
label: () => label,
|
|
6826
7218
|
listPrdStatesByStatus: () => listPrdStatesByStatus,
|
|
6827
7219
|
loadConfig: () => loadConfig,
|
|
7220
|
+
loadGlobalNotificationsConfig: () => loadGlobalNotificationsConfig,
|
|
6828
7221
|
loadHistory: () => loadHistory,
|
|
6829
7222
|
loadRegistry: () => loadRegistry,
|
|
6830
7223
|
loadRoadmapState: () => loadRoadmapState,
|
|
@@ -6862,8 +7255,10 @@ __export(dist_exports, {
|
|
|
6862
7255
|
reviewerLockPath: () => reviewerLockPath,
|
|
6863
7256
|
rotateLog: () => rotateLog,
|
|
6864
7257
|
runAllChecks: () => runAllChecks,
|
|
7258
|
+
runAnalytics: () => runAnalytics,
|
|
6865
7259
|
runMigrations: () => runMigrations,
|
|
6866
7260
|
saveConfig: () => saveConfig,
|
|
7261
|
+
saveGlobalNotificationsConfig: () => saveGlobalNotificationsConfig,
|
|
6867
7262
|
saveHistory: () => saveHistory,
|
|
6868
7263
|
saveRegistry: () => saveRegistry,
|
|
6869
7264
|
saveRoadmapState: () => saveRoadmapState,
|
|
@@ -6913,6 +7308,7 @@ var init_dist = __esm({
|
|
|
6913
7308
|
init_git_utils();
|
|
6914
7309
|
init_github();
|
|
6915
7310
|
init_log_utils();
|
|
7311
|
+
init_global_config();
|
|
6916
7312
|
init_notify();
|
|
6917
7313
|
init_prd_discovery();
|
|
6918
7314
|
init_prd_states();
|
|
@@ -6930,6 +7326,7 @@ var init_dist = __esm({
|
|
|
6930
7326
|
init_webhook_validator();
|
|
6931
7327
|
init_worktree_manager();
|
|
6932
7328
|
init_job_queue();
|
|
7329
|
+
init_analytics();
|
|
6933
7330
|
init_prd_template();
|
|
6934
7331
|
init_slicer_prompt();
|
|
6935
7332
|
}
|
|
@@ -6938,39 +7335,39 @@ var init_dist = __esm({
|
|
|
6938
7335
|
// src/cli.ts
|
|
6939
7336
|
import "reflect-metadata";
|
|
6940
7337
|
import { Command as Command3 } from "commander";
|
|
6941
|
-
import { existsSync as
|
|
7338
|
+
import { existsSync as existsSync30, readFileSync as readFileSync19 } from "fs";
|
|
6942
7339
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
6943
|
-
import { dirname as
|
|
7340
|
+
import { dirname as dirname9, join as join36 } from "path";
|
|
6944
7341
|
|
|
6945
7342
|
// src/commands/init.ts
|
|
6946
7343
|
init_dist();
|
|
6947
|
-
import
|
|
6948
|
-
import
|
|
7344
|
+
import fs20 from "fs";
|
|
7345
|
+
import path20 from "path";
|
|
6949
7346
|
import { execSync as execSync3 } from "child_process";
|
|
6950
7347
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6951
|
-
import { dirname as
|
|
7348
|
+
import { dirname as dirname5, join as join18 } from "path";
|
|
6952
7349
|
import * as readline from "readline";
|
|
6953
7350
|
var __filename = fileURLToPath2(import.meta.url);
|
|
6954
|
-
var __dirname2 =
|
|
7351
|
+
var __dirname2 = dirname5(__filename);
|
|
6955
7352
|
function findTemplatesDir(startDir) {
|
|
6956
7353
|
let d = startDir;
|
|
6957
7354
|
for (let i = 0; i < 8; i++) {
|
|
6958
|
-
const candidate =
|
|
6959
|
-
if (
|
|
7355
|
+
const candidate = join18(d, "templates");
|
|
7356
|
+
if (fs20.existsSync(candidate) && fs20.statSync(candidate).isDirectory()) {
|
|
6960
7357
|
return candidate;
|
|
6961
7358
|
}
|
|
6962
|
-
d =
|
|
7359
|
+
d = dirname5(d);
|
|
6963
7360
|
}
|
|
6964
|
-
return
|
|
7361
|
+
return join18(startDir, "templates");
|
|
6965
7362
|
}
|
|
6966
7363
|
var TEMPLATES_DIR = findTemplatesDir(__dirname2);
|
|
6967
7364
|
function hasPlaywrightDependency(cwd) {
|
|
6968
|
-
const packageJsonPath =
|
|
6969
|
-
if (!
|
|
7365
|
+
const packageJsonPath = path20.join(cwd, "package.json");
|
|
7366
|
+
if (!fs20.existsSync(packageJsonPath)) {
|
|
6970
7367
|
return false;
|
|
6971
7368
|
}
|
|
6972
7369
|
try {
|
|
6973
|
-
const packageJson2 = JSON.parse(
|
|
7370
|
+
const packageJson2 = JSON.parse(fs20.readFileSync(packageJsonPath, "utf-8"));
|
|
6974
7371
|
return Boolean(
|
|
6975
7372
|
packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright
|
|
6976
7373
|
);
|
|
@@ -6982,7 +7379,7 @@ function detectPlaywright(cwd) {
|
|
|
6982
7379
|
if (hasPlaywrightDependency(cwd)) {
|
|
6983
7380
|
return true;
|
|
6984
7381
|
}
|
|
6985
|
-
if (
|
|
7382
|
+
if (fs20.existsSync(path20.join(cwd, "node_modules", ".bin", "playwright"))) {
|
|
6986
7383
|
return true;
|
|
6987
7384
|
}
|
|
6988
7385
|
try {
|
|
@@ -6998,10 +7395,10 @@ function detectPlaywright(cwd) {
|
|
|
6998
7395
|
}
|
|
6999
7396
|
}
|
|
7000
7397
|
function resolvePlaywrightInstallCommand(cwd) {
|
|
7001
|
-
if (
|
|
7398
|
+
if (fs20.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) {
|
|
7002
7399
|
return "pnpm add -D @playwright/test";
|
|
7003
7400
|
}
|
|
7004
|
-
if (
|
|
7401
|
+
if (fs20.existsSync(path20.join(cwd, "yarn.lock"))) {
|
|
7005
7402
|
return "yarn add -D @playwright/test";
|
|
7006
7403
|
}
|
|
7007
7404
|
return "npm install -D @playwright/test";
|
|
@@ -7147,8 +7544,8 @@ function promptProviderSelection(providers) {
|
|
|
7147
7544
|
});
|
|
7148
7545
|
}
|
|
7149
7546
|
function ensureDir(dirPath) {
|
|
7150
|
-
if (!
|
|
7151
|
-
|
|
7547
|
+
if (!fs20.existsSync(dirPath)) {
|
|
7548
|
+
fs20.mkdirSync(dirPath, { recursive: true });
|
|
7152
7549
|
}
|
|
7153
7550
|
}
|
|
7154
7551
|
function buildInitConfig(params) {
|
|
@@ -7195,6 +7592,7 @@ function buildInitConfig(params) {
|
|
|
7195
7592
|
branchPatterns: [...defaults.qa.branchPatterns]
|
|
7196
7593
|
},
|
|
7197
7594
|
audit: { ...defaults.audit },
|
|
7595
|
+
analytics: { ...defaults.analytics },
|
|
7198
7596
|
jobProviders: { ...defaults.jobProviders },
|
|
7199
7597
|
queue: {
|
|
7200
7598
|
...defaults.queue,
|
|
@@ -7204,30 +7602,30 @@ function buildInitConfig(params) {
|
|
|
7204
7602
|
}
|
|
7205
7603
|
function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
|
|
7206
7604
|
if (customTemplatesDir !== null) {
|
|
7207
|
-
const customPath =
|
|
7208
|
-
if (
|
|
7605
|
+
const customPath = join18(customTemplatesDir, templateName);
|
|
7606
|
+
if (fs20.existsSync(customPath)) {
|
|
7209
7607
|
return { path: customPath, source: "custom" };
|
|
7210
7608
|
}
|
|
7211
7609
|
}
|
|
7212
|
-
return { path:
|
|
7610
|
+
return { path: join18(bundledTemplatesDir, templateName), source: "bundled" };
|
|
7213
7611
|
}
|
|
7214
7612
|
function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
|
|
7215
|
-
if (
|
|
7613
|
+
if (fs20.existsSync(targetPath) && !force) {
|
|
7216
7614
|
console.log(` Skipped (exists): ${targetPath}`);
|
|
7217
7615
|
return { created: false, source: source ?? "bundled" };
|
|
7218
7616
|
}
|
|
7219
|
-
const templatePath = sourcePath ??
|
|
7617
|
+
const templatePath = sourcePath ?? join18(TEMPLATES_DIR, templateName);
|
|
7220
7618
|
const resolvedSource = source ?? "bundled";
|
|
7221
|
-
let content =
|
|
7619
|
+
let content = fs20.readFileSync(templatePath, "utf-8");
|
|
7222
7620
|
for (const [key, value] of Object.entries(replacements)) {
|
|
7223
7621
|
content = content.replaceAll(key, value);
|
|
7224
7622
|
}
|
|
7225
|
-
|
|
7623
|
+
fs20.writeFileSync(targetPath, content);
|
|
7226
7624
|
console.log(` Created: ${targetPath} (${resolvedSource})`);
|
|
7227
7625
|
return { created: true, source: resolvedSource };
|
|
7228
7626
|
}
|
|
7229
7627
|
function addToGitignore(cwd) {
|
|
7230
|
-
const gitignorePath =
|
|
7628
|
+
const gitignorePath = path20.join(cwd, ".gitignore");
|
|
7231
7629
|
const entries = [
|
|
7232
7630
|
{
|
|
7233
7631
|
pattern: "/logs/",
|
|
@@ -7241,13 +7639,13 @@ function addToGitignore(cwd) {
|
|
|
7241
7639
|
},
|
|
7242
7640
|
{ pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
|
|
7243
7641
|
];
|
|
7244
|
-
if (!
|
|
7642
|
+
if (!fs20.existsSync(gitignorePath)) {
|
|
7245
7643
|
const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
|
|
7246
|
-
|
|
7644
|
+
fs20.writeFileSync(gitignorePath, lines.join("\n"));
|
|
7247
7645
|
console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
|
|
7248
7646
|
return;
|
|
7249
7647
|
}
|
|
7250
|
-
const content =
|
|
7648
|
+
const content = fs20.readFileSync(gitignorePath, "utf-8");
|
|
7251
7649
|
const missing = entries.filter((e) => !e.check(content));
|
|
7252
7650
|
if (missing.length === 0) {
|
|
7253
7651
|
console.log(` Skipped (exists): Night Watch entries in .gitignore`);
|
|
@@ -7255,7 +7653,7 @@ function addToGitignore(cwd) {
|
|
|
7255
7653
|
}
|
|
7256
7654
|
const additions = missing.map((e) => e.pattern).join("\n");
|
|
7257
7655
|
const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
|
|
7258
|
-
|
|
7656
|
+
fs20.writeFileSync(gitignorePath, newContent);
|
|
7259
7657
|
console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
|
|
7260
7658
|
}
|
|
7261
7659
|
function initCommand(program2) {
|
|
@@ -7376,28 +7774,28 @@ function initCommand(program2) {
|
|
|
7376
7774
|
"${DEFAULT_BRANCH}": defaultBranch
|
|
7377
7775
|
};
|
|
7378
7776
|
step(6, totalSteps, "Creating PRD directory structure...");
|
|
7379
|
-
const prdDirPath =
|
|
7380
|
-
const doneDirPath =
|
|
7777
|
+
const prdDirPath = path20.join(cwd, prdDir);
|
|
7778
|
+
const doneDirPath = path20.join(prdDirPath, "done");
|
|
7381
7779
|
ensureDir(doneDirPath);
|
|
7382
7780
|
success(`Created ${prdDirPath}/`);
|
|
7383
7781
|
success(`Created ${doneDirPath}/`);
|
|
7384
7782
|
step(7, totalSteps, "Creating logs directory...");
|
|
7385
|
-
const logsPath =
|
|
7783
|
+
const logsPath = path20.join(cwd, LOG_DIR);
|
|
7386
7784
|
ensureDir(logsPath);
|
|
7387
7785
|
success(`Created ${logsPath}/`);
|
|
7388
7786
|
addToGitignore(cwd);
|
|
7389
7787
|
step(8, totalSteps, "Creating instructions directory...");
|
|
7390
|
-
const instructionsDir =
|
|
7788
|
+
const instructionsDir = path20.join(cwd, "instructions");
|
|
7391
7789
|
ensureDir(instructionsDir);
|
|
7392
7790
|
success(`Created ${instructionsDir}/`);
|
|
7393
7791
|
const existingConfig = loadConfig(cwd);
|
|
7394
|
-
const customTemplatesDirPath =
|
|
7395
|
-
const customTemplatesDir =
|
|
7792
|
+
const customTemplatesDirPath = path20.join(cwd, existingConfig.templatesDir);
|
|
7793
|
+
const customTemplatesDir = fs20.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
|
|
7396
7794
|
const templateSources = [];
|
|
7397
7795
|
const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7398
7796
|
const nwResult = processTemplate(
|
|
7399
7797
|
"executor.md",
|
|
7400
|
-
|
|
7798
|
+
path20.join(instructionsDir, "executor.md"),
|
|
7401
7799
|
replacements,
|
|
7402
7800
|
force,
|
|
7403
7801
|
nwResolution.path,
|
|
@@ -7411,7 +7809,7 @@ function initCommand(program2) {
|
|
|
7411
7809
|
);
|
|
7412
7810
|
const peResult = processTemplate(
|
|
7413
7811
|
"prd-executor.md",
|
|
7414
|
-
|
|
7812
|
+
path20.join(instructionsDir, "prd-executor.md"),
|
|
7415
7813
|
replacements,
|
|
7416
7814
|
force,
|
|
7417
7815
|
peResolution.path,
|
|
@@ -7421,7 +7819,7 @@ function initCommand(program2) {
|
|
|
7421
7819
|
const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7422
7820
|
const prResult = processTemplate(
|
|
7423
7821
|
"pr-reviewer.md",
|
|
7424
|
-
|
|
7822
|
+
path20.join(instructionsDir, "pr-reviewer.md"),
|
|
7425
7823
|
replacements,
|
|
7426
7824
|
force,
|
|
7427
7825
|
prResolution.path,
|
|
@@ -7431,7 +7829,7 @@ function initCommand(program2) {
|
|
|
7431
7829
|
const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7432
7830
|
const qaResult = processTemplate(
|
|
7433
7831
|
"qa.md",
|
|
7434
|
-
|
|
7832
|
+
path20.join(instructionsDir, "qa.md"),
|
|
7435
7833
|
replacements,
|
|
7436
7834
|
force,
|
|
7437
7835
|
qaResolution.path,
|
|
@@ -7441,7 +7839,7 @@ function initCommand(program2) {
|
|
|
7441
7839
|
const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
|
|
7442
7840
|
const auditResult = processTemplate(
|
|
7443
7841
|
"audit.md",
|
|
7444
|
-
|
|
7842
|
+
path20.join(instructionsDir, "audit.md"),
|
|
7445
7843
|
replacements,
|
|
7446
7844
|
force,
|
|
7447
7845
|
auditResolution.path,
|
|
@@ -7449,8 +7847,8 @@ function initCommand(program2) {
|
|
|
7449
7847
|
);
|
|
7450
7848
|
templateSources.push({ name: "audit.md", source: auditResult.source });
|
|
7451
7849
|
step(9, totalSteps, "Creating configuration file...");
|
|
7452
|
-
const configPath =
|
|
7453
|
-
if (
|
|
7850
|
+
const configPath = path20.join(cwd, CONFIG_FILE_NAME);
|
|
7851
|
+
if (fs20.existsSync(configPath) && !force) {
|
|
7454
7852
|
console.log(` Skipped (exists): ${configPath}`);
|
|
7455
7853
|
} else {
|
|
7456
7854
|
const config = buildInitConfig({
|
|
@@ -7460,11 +7858,11 @@ function initCommand(program2) {
|
|
|
7460
7858
|
reviewerEnabled,
|
|
7461
7859
|
prdDir
|
|
7462
7860
|
});
|
|
7463
|
-
|
|
7861
|
+
fs20.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
7464
7862
|
success(`Created ${configPath}`);
|
|
7465
7863
|
}
|
|
7466
7864
|
step(10, totalSteps, "Setting up GitHub Project board...");
|
|
7467
|
-
const existingRaw = JSON.parse(
|
|
7865
|
+
const existingRaw = JSON.parse(fs20.readFileSync(configPath, "utf-8"));
|
|
7468
7866
|
const existingBoard = existingRaw.boardProvider;
|
|
7469
7867
|
let boardSetupStatus = "Skipped";
|
|
7470
7868
|
if (existingBoard?.projectNumber && !force) {
|
|
@@ -7486,13 +7884,13 @@ function initCommand(program2) {
|
|
|
7486
7884
|
const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
|
|
7487
7885
|
const boardTitle = `${projectName} Night Watch`;
|
|
7488
7886
|
const board = await provider.setupBoard(boardTitle);
|
|
7489
|
-
const rawConfig = JSON.parse(
|
|
7887
|
+
const rawConfig = JSON.parse(fs20.readFileSync(configPath, "utf-8"));
|
|
7490
7888
|
rawConfig.boardProvider = {
|
|
7491
7889
|
enabled: true,
|
|
7492
7890
|
provider: "github",
|
|
7493
7891
|
projectNumber: board.number
|
|
7494
7892
|
};
|
|
7495
|
-
|
|
7893
|
+
fs20.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
|
|
7496
7894
|
boardSetupStatus = `Created (#${board.number})`;
|
|
7497
7895
|
success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
|
|
7498
7896
|
} catch (boardErr) {
|
|
@@ -7616,8 +8014,8 @@ function getTelegramStatusWebhooks(config) {
|
|
|
7616
8014
|
}
|
|
7617
8015
|
|
|
7618
8016
|
// src/commands/run.ts
|
|
7619
|
-
import * as
|
|
7620
|
-
import * as
|
|
8017
|
+
import * as fs21 from "fs";
|
|
8018
|
+
import * as path21 from "path";
|
|
7621
8019
|
function resolveRunNotificationEvent(exitCode, scriptStatus) {
|
|
7622
8020
|
if (exitCode === 124) {
|
|
7623
8021
|
return "run_timeout";
|
|
@@ -7649,12 +8047,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
|
|
|
7649
8047
|
return scriptStatus === "skip_no_eligible_prd";
|
|
7650
8048
|
}
|
|
7651
8049
|
function getCrossProjectFallbackCandidates(currentProjectDir) {
|
|
7652
|
-
const current =
|
|
8050
|
+
const current = path21.resolve(currentProjectDir);
|
|
7653
8051
|
const { valid, invalid } = validateRegistry();
|
|
7654
8052
|
for (const entry of invalid) {
|
|
7655
8053
|
warn(`Skipping invalid registry entry: ${entry.path}`);
|
|
7656
8054
|
}
|
|
7657
|
-
return valid.filter((entry) =>
|
|
8055
|
+
return valid.filter((entry) => path21.resolve(entry.path) !== current);
|
|
7658
8056
|
}
|
|
7659
8057
|
async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
|
|
7660
8058
|
if (isRateLimitFallbackTriggered(scriptResult?.data)) {
|
|
@@ -7664,7 +8062,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
|
|
|
7664
8062
|
if (nonTelegramWebhooks.length > 0) {
|
|
7665
8063
|
const _rateLimitCtx = {
|
|
7666
8064
|
event: "rate_limit_fallback",
|
|
7667
|
-
projectName:
|
|
8065
|
+
projectName: path21.basename(projectDir),
|
|
7668
8066
|
exitCode,
|
|
7669
8067
|
provider: config.provider
|
|
7670
8068
|
};
|
|
@@ -7692,7 +8090,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
|
|
|
7692
8090
|
const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
|
|
7693
8091
|
const _ctx = {
|
|
7694
8092
|
event,
|
|
7695
|
-
projectName:
|
|
8093
|
+
projectName: path21.basename(projectDir),
|
|
7696
8094
|
exitCode,
|
|
7697
8095
|
provider: config.provider,
|
|
7698
8096
|
prdName: scriptResult?.data.prd,
|
|
@@ -7854,20 +8252,20 @@ function applyCliOverrides(config, options) {
|
|
|
7854
8252
|
return overridden;
|
|
7855
8253
|
}
|
|
7856
8254
|
function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
|
|
7857
|
-
const absolutePrdDir =
|
|
7858
|
-
const doneDir =
|
|
8255
|
+
const absolutePrdDir = path21.join(projectDir, prdDir);
|
|
8256
|
+
const doneDir = path21.join(absolutePrdDir, "done");
|
|
7859
8257
|
const pending = [];
|
|
7860
8258
|
const completed = [];
|
|
7861
|
-
if (
|
|
7862
|
-
const entries =
|
|
8259
|
+
if (fs21.existsSync(absolutePrdDir)) {
|
|
8260
|
+
const entries = fs21.readdirSync(absolutePrdDir, { withFileTypes: true });
|
|
7863
8261
|
for (const entry of entries) {
|
|
7864
8262
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7865
|
-
const claimPath =
|
|
8263
|
+
const claimPath = path21.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
|
|
7866
8264
|
let claimed = false;
|
|
7867
8265
|
let claimInfo = null;
|
|
7868
|
-
if (
|
|
8266
|
+
if (fs21.existsSync(claimPath)) {
|
|
7869
8267
|
try {
|
|
7870
|
-
const content =
|
|
8268
|
+
const content = fs21.readFileSync(claimPath, "utf-8");
|
|
7871
8269
|
const data = JSON.parse(content);
|
|
7872
8270
|
const age = Math.floor(Date.now() / 1e3) - data.timestamp;
|
|
7873
8271
|
if (age < maxRuntime) {
|
|
@@ -7881,8 +8279,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
|
|
|
7881
8279
|
}
|
|
7882
8280
|
}
|
|
7883
8281
|
}
|
|
7884
|
-
if (
|
|
7885
|
-
const entries =
|
|
8282
|
+
if (fs21.existsSync(doneDir)) {
|
|
8283
|
+
const entries = fs21.readdirSync(doneDir, { withFileTypes: true });
|
|
7886
8284
|
for (const entry of entries) {
|
|
7887
8285
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7888
8286
|
completed.push(entry.name);
|
|
@@ -8042,7 +8440,7 @@ ${stderr}`);
|
|
|
8042
8440
|
// src/commands/review.ts
|
|
8043
8441
|
init_dist();
|
|
8044
8442
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
8045
|
-
import * as
|
|
8443
|
+
import * as path22 from "path";
|
|
8046
8444
|
function shouldSendReviewNotification(scriptStatus) {
|
|
8047
8445
|
if (!scriptStatus) {
|
|
8048
8446
|
return true;
|
|
@@ -8282,7 +8680,7 @@ ${stderr}`);
|
|
|
8282
8680
|
const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
|
|
8283
8681
|
const _reviewCtx = {
|
|
8284
8682
|
event: "review_completed",
|
|
8285
|
-
projectName:
|
|
8683
|
+
projectName: path22.basename(projectDir),
|
|
8286
8684
|
exitCode,
|
|
8287
8685
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
8288
8686
|
prUrl: prDetails?.url,
|
|
@@ -8303,7 +8701,7 @@ ${stderr}`);
|
|
|
8303
8701
|
const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
|
|
8304
8702
|
const _mergeCtx = {
|
|
8305
8703
|
event: "pr_auto_merged",
|
|
8306
|
-
projectName:
|
|
8704
|
+
projectName: path22.basename(projectDir),
|
|
8307
8705
|
exitCode,
|
|
8308
8706
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
8309
8707
|
prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
|
|
@@ -8328,7 +8726,7 @@ ${stderr}`);
|
|
|
8328
8726
|
|
|
8329
8727
|
// src/commands/qa.ts
|
|
8330
8728
|
init_dist();
|
|
8331
|
-
import * as
|
|
8729
|
+
import * as path23 from "path";
|
|
8332
8730
|
function shouldSendQaNotification(scriptStatus) {
|
|
8333
8731
|
if (!scriptStatus) {
|
|
8334
8732
|
return true;
|
|
@@ -8462,7 +8860,7 @@ ${stderr}`);
|
|
|
8462
8860
|
const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
|
|
8463
8861
|
const _qaCtx = {
|
|
8464
8862
|
event: "qa_completed",
|
|
8465
|
-
projectName:
|
|
8863
|
+
projectName: path23.basename(projectDir),
|
|
8466
8864
|
exitCode,
|
|
8467
8865
|
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
|
|
8468
8866
|
prNumber: prDetails?.number ?? primaryQaPr,
|
|
@@ -8488,8 +8886,8 @@ ${stderr}`);
|
|
|
8488
8886
|
|
|
8489
8887
|
// src/commands/audit.ts
|
|
8490
8888
|
init_dist();
|
|
8491
|
-
import * as
|
|
8492
|
-
import * as
|
|
8889
|
+
import * as fs22 from "fs";
|
|
8890
|
+
import * as path24 from "path";
|
|
8493
8891
|
function buildEnvVars4(config, options) {
|
|
8494
8892
|
const env = buildBaseEnvVars(config, "audit", options.dryRun);
|
|
8495
8893
|
env.NW_AUDIT_MAX_RUNTIME = String(config.audit.maxRuntime);
|
|
@@ -8532,7 +8930,7 @@ function auditCommand(program2) {
|
|
|
8532
8930
|
configTable.push(["Provider", auditProvider]);
|
|
8533
8931
|
configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
|
|
8534
8932
|
configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
|
|
8535
|
-
configTable.push(["Report File",
|
|
8933
|
+
configTable.push(["Report File", path24.join(projectDir, "logs", "audit-report.md")]);
|
|
8536
8934
|
console.log(configTable.toString());
|
|
8537
8935
|
header("Provider Invocation");
|
|
8538
8936
|
const providerCmd = PROVIDER_COMMANDS[auditProvider];
|
|
@@ -8567,8 +8965,8 @@ ${stderr}`);
|
|
|
8567
8965
|
} else if (scriptResult?.status?.startsWith("skip_")) {
|
|
8568
8966
|
spinner.succeed("Code audit skipped");
|
|
8569
8967
|
} else {
|
|
8570
|
-
const reportPath =
|
|
8571
|
-
if (!
|
|
8968
|
+
const reportPath = path24.join(projectDir, "logs", "audit-report.md");
|
|
8969
|
+
if (!fs22.existsSync(reportPath)) {
|
|
8572
8970
|
spinner.fail("Code audit finished without a report file");
|
|
8573
8971
|
process.exit(1);
|
|
8574
8972
|
}
|
|
@@ -8579,9 +8977,9 @@ ${stderr}`);
|
|
|
8579
8977
|
const providerExit = scriptResult?.data?.provider_exit;
|
|
8580
8978
|
const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
|
|
8581
8979
|
spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
|
|
8582
|
-
const logPath =
|
|
8583
|
-
if (
|
|
8584
|
-
const logLines =
|
|
8980
|
+
const logPath = path24.join(projectDir, "logs", "audit.log");
|
|
8981
|
+
if (fs22.existsSync(logPath)) {
|
|
8982
|
+
const logLines = fs22.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
|
|
8585
8983
|
if (logLines.length > 0) {
|
|
8586
8984
|
process.stderr.write(logLines.join("\n") + "\n");
|
|
8587
8985
|
}
|
|
@@ -8595,19 +8993,80 @@ ${stderr}`);
|
|
|
8595
8993
|
});
|
|
8596
8994
|
}
|
|
8597
8995
|
|
|
8996
|
+
// src/commands/analytics.ts
|
|
8997
|
+
init_dist();
|
|
8998
|
+
function analyticsCommand(program2) {
|
|
8999
|
+
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) => {
|
|
9000
|
+
const projectDir = process.cwd();
|
|
9001
|
+
let config = loadConfig(projectDir);
|
|
9002
|
+
if (options.timeout) {
|
|
9003
|
+
const timeout = parseInt(options.timeout, 10);
|
|
9004
|
+
if (!isNaN(timeout)) {
|
|
9005
|
+
config = { ...config, analytics: { ...config.analytics, maxRuntime: timeout } };
|
|
9006
|
+
}
|
|
9007
|
+
}
|
|
9008
|
+
if (options.provider) {
|
|
9009
|
+
config = {
|
|
9010
|
+
...config,
|
|
9011
|
+
_cliProviderOverride: options.provider
|
|
9012
|
+
};
|
|
9013
|
+
}
|
|
9014
|
+
if (!config.analytics.enabled && !options.dryRun) {
|
|
9015
|
+
info("Analytics is disabled in config; skipping run.");
|
|
9016
|
+
process.exit(0);
|
|
9017
|
+
}
|
|
9018
|
+
const apiKey = config.providerEnv?.AMPLITUDE_API_KEY;
|
|
9019
|
+
const secretKey = config.providerEnv?.AMPLITUDE_SECRET_KEY;
|
|
9020
|
+
if (!apiKey || !secretKey) {
|
|
9021
|
+
info(
|
|
9022
|
+
"AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics."
|
|
9023
|
+
);
|
|
9024
|
+
process.exit(1);
|
|
9025
|
+
}
|
|
9026
|
+
if (options.dryRun) {
|
|
9027
|
+
header("Dry Run: Analytics Job");
|
|
9028
|
+
const analyticsProvider = resolveJobProvider(config, "analytics");
|
|
9029
|
+
header("Configuration");
|
|
9030
|
+
const configTable = createTable({ head: ["Setting", "Value"] });
|
|
9031
|
+
configTable.push(["Provider", analyticsProvider]);
|
|
9032
|
+
configTable.push(["Max Runtime", `${config.analytics.maxRuntime}s`]);
|
|
9033
|
+
configTable.push(["Lookback Days", String(config.analytics.lookbackDays)]);
|
|
9034
|
+
configTable.push(["Target Column", config.analytics.targetColumn]);
|
|
9035
|
+
configTable.push(["Amplitude API Key", apiKey ? "***" + apiKey.slice(-4) : "not set"]);
|
|
9036
|
+
console.log(configTable.toString());
|
|
9037
|
+
console.log();
|
|
9038
|
+
process.exit(0);
|
|
9039
|
+
}
|
|
9040
|
+
const spinner = createSpinner("Running analytics job...");
|
|
9041
|
+
spinner.start();
|
|
9042
|
+
try {
|
|
9043
|
+
await maybeApplyCronSchedulingDelay(config, "analytics", projectDir);
|
|
9044
|
+
const result = await runAnalytics(config, projectDir);
|
|
9045
|
+
if (result.issuesCreated > 0) {
|
|
9046
|
+
spinner.succeed(`Analytics complete \u2014 ${result.summary}`);
|
|
9047
|
+
} else {
|
|
9048
|
+
spinner.succeed("Analytics complete \u2014 no actionable insights found");
|
|
9049
|
+
}
|
|
9050
|
+
} catch (err) {
|
|
9051
|
+
spinner.fail(`Analytics failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
9052
|
+
process.exit(1);
|
|
9053
|
+
}
|
|
9054
|
+
});
|
|
9055
|
+
}
|
|
9056
|
+
|
|
8598
9057
|
// src/commands/install.ts
|
|
8599
9058
|
init_dist();
|
|
8600
9059
|
import { execSync as execSync4 } from "child_process";
|
|
8601
|
-
import * as
|
|
8602
|
-
import * as
|
|
9060
|
+
import * as path25 from "path";
|
|
9061
|
+
import * as fs23 from "fs";
|
|
8603
9062
|
function shellQuote(value) {
|
|
8604
9063
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
8605
9064
|
}
|
|
8606
9065
|
function getNightWatchBinPath() {
|
|
8607
9066
|
try {
|
|
8608
9067
|
const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
|
|
8609
|
-
const binPath =
|
|
8610
|
-
if (
|
|
9068
|
+
const binPath = path25.join(npmBin, "night-watch");
|
|
9069
|
+
if (fs23.existsSync(binPath)) {
|
|
8611
9070
|
return binPath;
|
|
8612
9071
|
}
|
|
8613
9072
|
} catch {
|
|
@@ -8620,17 +9079,17 @@ function getNightWatchBinPath() {
|
|
|
8620
9079
|
}
|
|
8621
9080
|
function getNodeBinDir() {
|
|
8622
9081
|
if (process.execPath && process.execPath !== "node") {
|
|
8623
|
-
return
|
|
9082
|
+
return path25.dirname(process.execPath);
|
|
8624
9083
|
}
|
|
8625
9084
|
try {
|
|
8626
9085
|
const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
|
|
8627
|
-
return
|
|
9086
|
+
return path25.dirname(nodePath);
|
|
8628
9087
|
} catch {
|
|
8629
9088
|
return "";
|
|
8630
9089
|
}
|
|
8631
9090
|
}
|
|
8632
9091
|
function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
|
|
8633
|
-
const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ?
|
|
9092
|
+
const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path25.dirname(nightWatchBin) : "";
|
|
8634
9093
|
const pathParts = Array.from(
|
|
8635
9094
|
new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0))
|
|
8636
9095
|
);
|
|
@@ -8646,12 +9105,12 @@ function performInstall(projectDir, config, options) {
|
|
|
8646
9105
|
const nightWatchBin = getNightWatchBinPath();
|
|
8647
9106
|
const projectName = getProjectName(projectDir);
|
|
8648
9107
|
const marker = generateMarker(projectName);
|
|
8649
|
-
const logDir =
|
|
8650
|
-
if (!
|
|
8651
|
-
|
|
9108
|
+
const logDir = path25.join(projectDir, LOG_DIR);
|
|
9109
|
+
if (!fs23.existsSync(logDir)) {
|
|
9110
|
+
fs23.mkdirSync(logDir, { recursive: true });
|
|
8652
9111
|
}
|
|
8653
|
-
const executorLog =
|
|
8654
|
-
const reviewerLog =
|
|
9112
|
+
const executorLog = path25.join(logDir, "executor.log");
|
|
9113
|
+
const reviewerLog = path25.join(logDir, "reviewer.log");
|
|
8655
9114
|
if (!options?.force) {
|
|
8656
9115
|
const existingEntries2 = Array.from(
|
|
8657
9116
|
/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
|
|
@@ -8688,7 +9147,7 @@ function performInstall(projectDir, config, options) {
|
|
|
8688
9147
|
const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
|
|
8689
9148
|
if (installSlicer) {
|
|
8690
9149
|
const slicerSchedule = config.roadmapScanner.slicerSchedule;
|
|
8691
|
-
const slicerLog =
|
|
9150
|
+
const slicerLog = path25.join(logDir, "slicer.log");
|
|
8692
9151
|
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
8693
9152
|
entries.push(slicerEntry);
|
|
8694
9153
|
}
|
|
@@ -8696,7 +9155,7 @@ function performInstall(projectDir, config, options) {
|
|
|
8696
9155
|
const installQa = disableQa ? false : config.qa.enabled;
|
|
8697
9156
|
if (installQa) {
|
|
8698
9157
|
const qaSchedule = config.qa.schedule;
|
|
8699
|
-
const qaLog =
|
|
9158
|
+
const qaLog = path25.join(logDir, "qa.log");
|
|
8700
9159
|
const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
|
|
8701
9160
|
entries.push(qaEntry);
|
|
8702
9161
|
}
|
|
@@ -8704,10 +9163,18 @@ function performInstall(projectDir, config, options) {
|
|
|
8704
9163
|
const installAudit = disableAudit ? false : config.audit.enabled;
|
|
8705
9164
|
if (installAudit) {
|
|
8706
9165
|
const auditSchedule = config.audit.schedule;
|
|
8707
|
-
const auditLog =
|
|
9166
|
+
const auditLog = path25.join(logDir, "audit.log");
|
|
8708
9167
|
const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
|
|
8709
9168
|
entries.push(auditEntry);
|
|
8710
9169
|
}
|
|
9170
|
+
const disableAnalytics = options?.noAnalytics === true || options?.analytics === false;
|
|
9171
|
+
const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
|
|
9172
|
+
if (installAnalytics) {
|
|
9173
|
+
const analyticsSchedule = config.analytics.schedule;
|
|
9174
|
+
const analyticsLog = path25.join(logDir, "analytics.log");
|
|
9175
|
+
const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
|
|
9176
|
+
entries.push(analyticsEntry);
|
|
9177
|
+
}
|
|
8711
9178
|
const existingEntries = new Set(
|
|
8712
9179
|
Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]))
|
|
8713
9180
|
);
|
|
@@ -8726,7 +9193,7 @@ function performInstall(projectDir, config, options) {
|
|
|
8726
9193
|
}
|
|
8727
9194
|
}
|
|
8728
9195
|
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) => {
|
|
9196
|
+
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
9197
|
try {
|
|
8731
9198
|
const projectDir = process.cwd();
|
|
8732
9199
|
const config = loadConfig(projectDir);
|
|
@@ -8735,12 +9202,12 @@ function installCommand(program2) {
|
|
|
8735
9202
|
const nightWatchBin = getNightWatchBinPath();
|
|
8736
9203
|
const projectName = getProjectName(projectDir);
|
|
8737
9204
|
const marker = generateMarker(projectName);
|
|
8738
|
-
const logDir =
|
|
8739
|
-
if (!
|
|
8740
|
-
|
|
9205
|
+
const logDir = path25.join(projectDir, LOG_DIR);
|
|
9206
|
+
if (!fs23.existsSync(logDir)) {
|
|
9207
|
+
fs23.mkdirSync(logDir, { recursive: true });
|
|
8741
9208
|
}
|
|
8742
|
-
const executorLog =
|
|
8743
|
-
const reviewerLog =
|
|
9209
|
+
const executorLog = path25.join(logDir, "executor.log");
|
|
9210
|
+
const reviewerLog = path25.join(logDir, "reviewer.log");
|
|
8744
9211
|
const existingEntries = Array.from(
|
|
8745
9212
|
/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
|
|
8746
9213
|
);
|
|
@@ -8776,7 +9243,7 @@ function installCommand(program2) {
|
|
|
8776
9243
|
const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
|
|
8777
9244
|
let slicerLog;
|
|
8778
9245
|
if (installSlicer) {
|
|
8779
|
-
slicerLog =
|
|
9246
|
+
slicerLog = path25.join(logDir, "slicer.log");
|
|
8780
9247
|
const slicerSchedule = config.roadmapScanner.slicerSchedule;
|
|
8781
9248
|
const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
|
|
8782
9249
|
entries.push(slicerEntry);
|
|
@@ -8785,7 +9252,7 @@ function installCommand(program2) {
|
|
|
8785
9252
|
const installQa = disableQa ? false : config.qa.enabled;
|
|
8786
9253
|
let qaLog;
|
|
8787
9254
|
if (installQa) {
|
|
8788
|
-
qaLog =
|
|
9255
|
+
qaLog = path25.join(logDir, "qa.log");
|
|
8789
9256
|
const qaSchedule = config.qa.schedule;
|
|
8790
9257
|
const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
|
|
8791
9258
|
entries.push(qaEntry);
|
|
@@ -8794,11 +9261,20 @@ function installCommand(program2) {
|
|
|
8794
9261
|
const installAudit = disableAudit ? false : config.audit.enabled;
|
|
8795
9262
|
let auditLog;
|
|
8796
9263
|
if (installAudit) {
|
|
8797
|
-
auditLog =
|
|
9264
|
+
auditLog = path25.join(logDir, "audit.log");
|
|
8798
9265
|
const auditSchedule = config.audit.schedule;
|
|
8799
9266
|
const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
|
|
8800
9267
|
entries.push(auditEntry);
|
|
8801
9268
|
}
|
|
9269
|
+
const disableAnalytics = options.noAnalytics === true || options.analytics === false;
|
|
9270
|
+
const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
|
|
9271
|
+
let analyticsLog;
|
|
9272
|
+
if (installAnalytics) {
|
|
9273
|
+
analyticsLog = path25.join(logDir, "analytics.log");
|
|
9274
|
+
const analyticsSchedule = config.analytics.schedule;
|
|
9275
|
+
const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
|
|
9276
|
+
entries.push(analyticsEntry);
|
|
9277
|
+
}
|
|
8802
9278
|
const existingEntrySet = new Set(existingEntries);
|
|
8803
9279
|
const currentCrontab = readCrontab();
|
|
8804
9280
|
const baseCrontab = options.force ? currentCrontab.filter((line) => !existingEntrySet.has(line) && !line.includes(marker)) : currentCrontab;
|
|
@@ -8825,6 +9301,9 @@ function installCommand(program2) {
|
|
|
8825
9301
|
if (installAudit && auditLog) {
|
|
8826
9302
|
dim(` Audit: ${auditLog}`);
|
|
8827
9303
|
}
|
|
9304
|
+
if (installAnalytics && analyticsLog) {
|
|
9305
|
+
dim(` Analytics: ${analyticsLog}`);
|
|
9306
|
+
}
|
|
8828
9307
|
console.log();
|
|
8829
9308
|
dim("To uninstall, run: night-watch uninstall");
|
|
8830
9309
|
dim("To check status, run: night-watch status");
|
|
@@ -8839,8 +9318,8 @@ function installCommand(program2) {
|
|
|
8839
9318
|
|
|
8840
9319
|
// src/commands/uninstall.ts
|
|
8841
9320
|
init_dist();
|
|
8842
|
-
import * as
|
|
8843
|
-
import * as
|
|
9321
|
+
import * as path26 from "path";
|
|
9322
|
+
import * as fs24 from "fs";
|
|
8844
9323
|
function performUninstall(projectDir, options) {
|
|
8845
9324
|
try {
|
|
8846
9325
|
const projectName = getProjectName(projectDir);
|
|
@@ -8855,19 +9334,19 @@ function performUninstall(projectDir, options) {
|
|
|
8855
9334
|
const removedCount = removeEntriesForProject(projectDir, marker);
|
|
8856
9335
|
unregisterProject(projectDir);
|
|
8857
9336
|
if (!options?.keepLogs) {
|
|
8858
|
-
const logDir =
|
|
8859
|
-
if (
|
|
9337
|
+
const logDir = path26.join(projectDir, "logs");
|
|
9338
|
+
if (fs24.existsSync(logDir)) {
|
|
8860
9339
|
const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
|
|
8861
9340
|
logFiles.forEach((logFile) => {
|
|
8862
|
-
const logPath =
|
|
8863
|
-
if (
|
|
8864
|
-
|
|
9341
|
+
const logPath = path26.join(logDir, logFile);
|
|
9342
|
+
if (fs24.existsSync(logPath)) {
|
|
9343
|
+
fs24.unlinkSync(logPath);
|
|
8865
9344
|
}
|
|
8866
9345
|
});
|
|
8867
9346
|
try {
|
|
8868
|
-
const remainingFiles =
|
|
9347
|
+
const remainingFiles = fs24.readdirSync(logDir);
|
|
8869
9348
|
if (remainingFiles.length === 0) {
|
|
8870
|
-
|
|
9349
|
+
fs24.rmdirSync(logDir);
|
|
8871
9350
|
}
|
|
8872
9351
|
} catch {
|
|
8873
9352
|
}
|
|
@@ -8900,21 +9379,21 @@ function uninstallCommand(program2) {
|
|
|
8900
9379
|
existingEntries.forEach((entry) => dim(` ${entry}`));
|
|
8901
9380
|
const removedCount = removeEntriesForProject(projectDir, marker);
|
|
8902
9381
|
if (!options.keepLogs) {
|
|
8903
|
-
const logDir =
|
|
8904
|
-
if (
|
|
9382
|
+
const logDir = path26.join(projectDir, "logs");
|
|
9383
|
+
if (fs24.existsSync(logDir)) {
|
|
8905
9384
|
const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
|
|
8906
9385
|
let logsRemoved = 0;
|
|
8907
9386
|
logFiles.forEach((logFile) => {
|
|
8908
|
-
const logPath =
|
|
8909
|
-
if (
|
|
8910
|
-
|
|
9387
|
+
const logPath = path26.join(logDir, logFile);
|
|
9388
|
+
if (fs24.existsSync(logPath)) {
|
|
9389
|
+
fs24.unlinkSync(logPath);
|
|
8911
9390
|
logsRemoved++;
|
|
8912
9391
|
}
|
|
8913
9392
|
});
|
|
8914
9393
|
try {
|
|
8915
|
-
const remainingFiles =
|
|
9394
|
+
const remainingFiles = fs24.readdirSync(logDir);
|
|
8916
9395
|
if (remainingFiles.length === 0) {
|
|
8917
|
-
|
|
9396
|
+
fs24.rmdirSync(logDir);
|
|
8918
9397
|
}
|
|
8919
9398
|
} catch {
|
|
8920
9399
|
}
|
|
@@ -9150,14 +9629,14 @@ function statusCommand(program2) {
|
|
|
9150
9629
|
// src/commands/logs.ts
|
|
9151
9630
|
init_dist();
|
|
9152
9631
|
import { spawn as spawn3 } from "child_process";
|
|
9153
|
-
import * as
|
|
9154
|
-
import * as
|
|
9632
|
+
import * as path27 from "path";
|
|
9633
|
+
import * as fs25 from "fs";
|
|
9155
9634
|
function getLastLines(filePath, lineCount) {
|
|
9156
|
-
if (!
|
|
9635
|
+
if (!fs25.existsSync(filePath)) {
|
|
9157
9636
|
return `Log file not found: ${filePath}`;
|
|
9158
9637
|
}
|
|
9159
9638
|
try {
|
|
9160
|
-
const content =
|
|
9639
|
+
const content = fs25.readFileSync(filePath, "utf-8");
|
|
9161
9640
|
const lines = content.trim().split("\n");
|
|
9162
9641
|
return lines.slice(-lineCount).join("\n");
|
|
9163
9642
|
} catch (error2) {
|
|
@@ -9165,7 +9644,7 @@ function getLastLines(filePath, lineCount) {
|
|
|
9165
9644
|
}
|
|
9166
9645
|
}
|
|
9167
9646
|
function followLog(filePath) {
|
|
9168
|
-
if (!
|
|
9647
|
+
if (!fs25.existsSync(filePath)) {
|
|
9169
9648
|
console.log(`Log file not found: ${filePath}`);
|
|
9170
9649
|
console.log("The log file will be created when the first execution runs.");
|
|
9171
9650
|
return;
|
|
@@ -9185,13 +9664,13 @@ function logsCommand(program2) {
|
|
|
9185
9664
|
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
9665
|
try {
|
|
9187
9666
|
const projectDir = process.cwd();
|
|
9188
|
-
const logDir =
|
|
9667
|
+
const logDir = path27.join(projectDir, LOG_DIR);
|
|
9189
9668
|
const lineCount = parseInt(options.lines || "50", 10);
|
|
9190
|
-
const executorLog =
|
|
9191
|
-
const reviewerLog =
|
|
9192
|
-
const qaLog =
|
|
9193
|
-
const auditLog =
|
|
9194
|
-
const plannerLog =
|
|
9669
|
+
const executorLog = path27.join(logDir, EXECUTOR_LOG_FILE);
|
|
9670
|
+
const reviewerLog = path27.join(logDir, REVIEWER_LOG_FILE);
|
|
9671
|
+
const qaLog = path27.join(logDir, `${QA_LOG_NAME}.log`);
|
|
9672
|
+
const auditLog = path27.join(logDir, `${AUDIT_LOG_NAME}.log`);
|
|
9673
|
+
const plannerLog = path27.join(logDir, `${PLANNER_LOG_NAME}.log`);
|
|
9195
9674
|
const logType = options.type?.toLowerCase() || "all";
|
|
9196
9675
|
const showExecutor = logType === "all" || logType === "run" || logType === "executor";
|
|
9197
9676
|
const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
|
|
@@ -9255,15 +9734,15 @@ function logsCommand(program2) {
|
|
|
9255
9734
|
|
|
9256
9735
|
// src/commands/prd.ts
|
|
9257
9736
|
init_dist();
|
|
9258
|
-
import * as
|
|
9259
|
-
import * as
|
|
9737
|
+
import * as fs26 from "fs";
|
|
9738
|
+
import * as path28 from "path";
|
|
9260
9739
|
import * as readline2 from "readline";
|
|
9261
9740
|
function slugify2(name) {
|
|
9262
9741
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
9263
9742
|
}
|
|
9264
9743
|
function getNextPrdNumber2(prdDir) {
|
|
9265
|
-
if (!
|
|
9266
|
-
const files =
|
|
9744
|
+
if (!fs26.existsSync(prdDir)) return 1;
|
|
9745
|
+
const files = fs26.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
|
|
9267
9746
|
const numbers = files.map((f) => {
|
|
9268
9747
|
const match = f.match(/^(\d+)-/);
|
|
9269
9748
|
return match ? parseInt(match[1], 10) : 0;
|
|
@@ -9284,10 +9763,10 @@ function parseDependencies(content) {
|
|
|
9284
9763
|
}
|
|
9285
9764
|
function isClaimActive(claimPath, maxRuntime) {
|
|
9286
9765
|
try {
|
|
9287
|
-
if (!
|
|
9766
|
+
if (!fs26.existsSync(claimPath)) {
|
|
9288
9767
|
return { active: false };
|
|
9289
9768
|
}
|
|
9290
|
-
const content =
|
|
9769
|
+
const content = fs26.readFileSync(claimPath, "utf-8");
|
|
9291
9770
|
const claim = JSON.parse(content);
|
|
9292
9771
|
const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
|
|
9293
9772
|
if (age < maxRuntime) {
|
|
@@ -9303,9 +9782,9 @@ function prdCommand(program2) {
|
|
|
9303
9782
|
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
9783
|
const projectDir = process.cwd();
|
|
9305
9784
|
const config = loadConfig(projectDir);
|
|
9306
|
-
const prdDir =
|
|
9307
|
-
if (!
|
|
9308
|
-
|
|
9785
|
+
const prdDir = path28.join(projectDir, config.prdDir);
|
|
9786
|
+
if (!fs26.existsSync(prdDir)) {
|
|
9787
|
+
fs26.mkdirSync(prdDir, { recursive: true });
|
|
9309
9788
|
}
|
|
9310
9789
|
let complexityScore = 5;
|
|
9311
9790
|
let dependsOn = [];
|
|
@@ -9364,20 +9843,20 @@ function prdCommand(program2) {
|
|
|
9364
9843
|
} else {
|
|
9365
9844
|
filename = `${slug}.md`;
|
|
9366
9845
|
}
|
|
9367
|
-
const filePath =
|
|
9368
|
-
if (
|
|
9846
|
+
const filePath = path28.join(prdDir, filename);
|
|
9847
|
+
if (fs26.existsSync(filePath)) {
|
|
9369
9848
|
error(`File already exists: ${filePath}`);
|
|
9370
9849
|
dim("Use a different name or remove the existing file.");
|
|
9371
9850
|
process.exit(1);
|
|
9372
9851
|
}
|
|
9373
9852
|
let customTemplate;
|
|
9374
9853
|
if (options.template) {
|
|
9375
|
-
const templatePath =
|
|
9376
|
-
if (!
|
|
9854
|
+
const templatePath = path28.resolve(options.template);
|
|
9855
|
+
if (!fs26.existsSync(templatePath)) {
|
|
9377
9856
|
error(`Template file not found: ${templatePath}`);
|
|
9378
9857
|
process.exit(1);
|
|
9379
9858
|
}
|
|
9380
|
-
customTemplate =
|
|
9859
|
+
customTemplate = fs26.readFileSync(templatePath, "utf-8");
|
|
9381
9860
|
}
|
|
9382
9861
|
const vars = {
|
|
9383
9862
|
title: name,
|
|
@@ -9388,7 +9867,7 @@ function prdCommand(program2) {
|
|
|
9388
9867
|
phaseCount
|
|
9389
9868
|
};
|
|
9390
9869
|
const content = renderPrdTemplate(vars, customTemplate);
|
|
9391
|
-
|
|
9870
|
+
fs26.writeFileSync(filePath, content, "utf-8");
|
|
9392
9871
|
header("PRD Created");
|
|
9393
9872
|
success(`Created: ${filePath}`);
|
|
9394
9873
|
info(`Title: ${name}`);
|
|
@@ -9400,15 +9879,15 @@ function prdCommand(program2) {
|
|
|
9400
9879
|
prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
|
|
9401
9880
|
const projectDir = process.cwd();
|
|
9402
9881
|
const config = loadConfig(projectDir);
|
|
9403
|
-
const absolutePrdDir =
|
|
9404
|
-
const doneDir =
|
|
9882
|
+
const absolutePrdDir = path28.join(projectDir, config.prdDir);
|
|
9883
|
+
const doneDir = path28.join(absolutePrdDir, "done");
|
|
9405
9884
|
const pending = [];
|
|
9406
|
-
if (
|
|
9407
|
-
const files =
|
|
9885
|
+
if (fs26.existsSync(absolutePrdDir)) {
|
|
9886
|
+
const files = fs26.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
|
|
9408
9887
|
for (const file of files) {
|
|
9409
|
-
const content =
|
|
9888
|
+
const content = fs26.readFileSync(path28.join(absolutePrdDir, file), "utf-8");
|
|
9410
9889
|
const deps = parseDependencies(content);
|
|
9411
|
-
const claimPath =
|
|
9890
|
+
const claimPath = path28.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
|
|
9412
9891
|
const claimStatus = isClaimActive(claimPath, config.maxRuntime);
|
|
9413
9892
|
pending.push({
|
|
9414
9893
|
name: file,
|
|
@@ -9419,10 +9898,10 @@ function prdCommand(program2) {
|
|
|
9419
9898
|
}
|
|
9420
9899
|
}
|
|
9421
9900
|
const done = [];
|
|
9422
|
-
if (
|
|
9423
|
-
const files =
|
|
9901
|
+
if (fs26.existsSync(doneDir)) {
|
|
9902
|
+
const files = fs26.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
|
|
9424
9903
|
for (const file of files) {
|
|
9425
|
-
const content =
|
|
9904
|
+
const content = fs26.readFileSync(path28.join(doneDir, file), "utf-8");
|
|
9426
9905
|
const deps = parseDependencies(content);
|
|
9427
9906
|
done.push({ name: file, dependencies: deps });
|
|
9428
9907
|
}
|
|
@@ -9461,7 +9940,7 @@ import blessed6 from "blessed";
|
|
|
9461
9940
|
// src/commands/dashboard/tab-status.ts
|
|
9462
9941
|
init_dist();
|
|
9463
9942
|
import blessed from "blessed";
|
|
9464
|
-
import * as
|
|
9943
|
+
import * as fs27 from "fs";
|
|
9465
9944
|
function sortPrdsByPriority(prds, priority) {
|
|
9466
9945
|
if (priority.length === 0) return prds;
|
|
9467
9946
|
const priorityMap = /* @__PURE__ */ new Map();
|
|
@@ -9557,7 +10036,7 @@ function renderLogPane(projectDir, logs) {
|
|
|
9557
10036
|
let newestMtime = 0;
|
|
9558
10037
|
for (const log of existingLogs) {
|
|
9559
10038
|
try {
|
|
9560
|
-
const stat =
|
|
10039
|
+
const stat = fs27.statSync(log.path);
|
|
9561
10040
|
if (stat.mtimeMs > newestMtime) {
|
|
9562
10041
|
newestMtime = stat.mtimeMs;
|
|
9563
10042
|
newestLog = log;
|
|
@@ -11212,8 +11691,8 @@ function createActionsTab() {
|
|
|
11212
11691
|
// src/commands/dashboard/tab-logs.ts
|
|
11213
11692
|
init_dist();
|
|
11214
11693
|
import blessed5 from "blessed";
|
|
11215
|
-
import * as
|
|
11216
|
-
import * as
|
|
11694
|
+
import * as fs28 from "fs";
|
|
11695
|
+
import * as path29 from "path";
|
|
11217
11696
|
var LOG_NAMES = ["executor", "reviewer"];
|
|
11218
11697
|
var LOG_LINES = 200;
|
|
11219
11698
|
function createLogsTab() {
|
|
@@ -11254,7 +11733,7 @@ function createLogsTab() {
|
|
|
11254
11733
|
let activeKeyHandlers = [];
|
|
11255
11734
|
let activeCtx = null;
|
|
11256
11735
|
function getLogPath(projectDir, logName) {
|
|
11257
|
-
return
|
|
11736
|
+
return path29.join(projectDir, "logs", `${logName}.log`);
|
|
11258
11737
|
}
|
|
11259
11738
|
function updateSelector() {
|
|
11260
11739
|
const tabs = LOG_NAMES.map((name, idx) => {
|
|
@@ -11268,7 +11747,7 @@ function createLogsTab() {
|
|
|
11268
11747
|
function loadLog(ctx) {
|
|
11269
11748
|
const logName = LOG_NAMES[selectedLogIndex];
|
|
11270
11749
|
const logPath = getLogPath(ctx.projectDir, logName);
|
|
11271
|
-
if (!
|
|
11750
|
+
if (!fs28.existsSync(logPath)) {
|
|
11272
11751
|
logContent.setContent(
|
|
11273
11752
|
`{yellow-fg}No ${logName}.log file found{/yellow-fg}
|
|
11274
11753
|
|
|
@@ -11278,7 +11757,7 @@ Log will appear here once the ${logName} runs.`
|
|
|
11278
11757
|
return;
|
|
11279
11758
|
}
|
|
11280
11759
|
try {
|
|
11281
|
-
const stat =
|
|
11760
|
+
const stat = fs28.statSync(logPath);
|
|
11282
11761
|
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
11283
11762
|
logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
|
|
11284
11763
|
} catch {
|
|
@@ -11784,13 +12263,13 @@ function doctorCommand(program2) {
|
|
|
11784
12263
|
|
|
11785
12264
|
// src/commands/serve.ts
|
|
11786
12265
|
init_dist();
|
|
11787
|
-
import * as
|
|
12266
|
+
import * as fs33 from "fs";
|
|
11788
12267
|
|
|
11789
12268
|
// ../server/dist/index.js
|
|
11790
12269
|
init_dist();
|
|
11791
|
-
import * as
|
|
11792
|
-
import * as
|
|
11793
|
-
import { dirname as
|
|
12270
|
+
import * as fs32 from "fs";
|
|
12271
|
+
import * as path35 from "path";
|
|
12272
|
+
import { dirname as dirname8 } from "path";
|
|
11794
12273
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
11795
12274
|
import cors from "cors";
|
|
11796
12275
|
import express from "express";
|
|
@@ -11874,8 +12353,8 @@ function setupGracefulShutdown(server, beforeClose) {
|
|
|
11874
12353
|
|
|
11875
12354
|
// ../server/dist/middleware/project-resolver.middleware.js
|
|
11876
12355
|
init_dist();
|
|
11877
|
-
import * as
|
|
11878
|
-
import * as
|
|
12356
|
+
import * as fs29 from "fs";
|
|
12357
|
+
import * as path30 from "path";
|
|
11879
12358
|
function resolveProject(req, res, next) {
|
|
11880
12359
|
const projectId = req.params.projectId;
|
|
11881
12360
|
const decodedId = decodeURIComponent(projectId).replace(/~/g, "/");
|
|
@@ -11885,7 +12364,7 @@ function resolveProject(req, res, next) {
|
|
|
11885
12364
|
res.status(404).json({ error: `Project not found: ${decodedId}` });
|
|
11886
12365
|
return;
|
|
11887
12366
|
}
|
|
11888
|
-
if (!
|
|
12367
|
+
if (!fs29.existsSync(entry.path) || !fs29.existsSync(path30.join(entry.path, CONFIG_FILE_NAME))) {
|
|
11889
12368
|
res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
|
|
11890
12369
|
return;
|
|
11891
12370
|
}
|
|
@@ -11930,8 +12409,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
|
|
|
11930
12409
|
|
|
11931
12410
|
// ../server/dist/routes/action.routes.js
|
|
11932
12411
|
init_dist();
|
|
11933
|
-
import * as
|
|
11934
|
-
import * as
|
|
12412
|
+
import * as fs30 from "fs";
|
|
12413
|
+
import * as path31 from "path";
|
|
11935
12414
|
import { execSync as execSync5, spawn as spawn5 } from "child_process";
|
|
11936
12415
|
import { Router } from "express";
|
|
11937
12416
|
|
|
@@ -11969,17 +12448,17 @@ function getBoardProvider(config, projectDir) {
|
|
|
11969
12448
|
function cleanOrphanedClaims(dir) {
|
|
11970
12449
|
let entries;
|
|
11971
12450
|
try {
|
|
11972
|
-
entries =
|
|
12451
|
+
entries = fs30.readdirSync(dir, { withFileTypes: true });
|
|
11973
12452
|
} catch {
|
|
11974
12453
|
return;
|
|
11975
12454
|
}
|
|
11976
12455
|
for (const entry of entries) {
|
|
11977
|
-
const fullPath =
|
|
12456
|
+
const fullPath = path31.join(dir, entry.name);
|
|
11978
12457
|
if (entry.isDirectory() && entry.name !== "done") {
|
|
11979
12458
|
cleanOrphanedClaims(fullPath);
|
|
11980
12459
|
} else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
|
|
11981
12460
|
try {
|
|
11982
|
-
|
|
12461
|
+
fs30.unlinkSync(fullPath);
|
|
11983
12462
|
} catch {
|
|
11984
12463
|
}
|
|
11985
12464
|
}
|
|
@@ -12076,6 +12555,9 @@ function createActionRouteHandlers(ctx) {
|
|
|
12076
12555
|
router.post(`/${p}audit`, (req, res) => {
|
|
12077
12556
|
spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
|
|
12078
12557
|
});
|
|
12558
|
+
router.post(`/${p}analytics`, (req, res) => {
|
|
12559
|
+
spawnAction2(ctx.getProjectDir(req), ["analytics"], req, res);
|
|
12560
|
+
});
|
|
12079
12561
|
router.post(`/${p}planner`, (req, res) => {
|
|
12080
12562
|
spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
|
|
12081
12563
|
});
|
|
@@ -12133,19 +12615,19 @@ function createActionRouteHandlers(ctx) {
|
|
|
12133
12615
|
res.status(400).json({ error: "Invalid PRD name" });
|
|
12134
12616
|
return;
|
|
12135
12617
|
}
|
|
12136
|
-
const prdDir =
|
|
12618
|
+
const prdDir = path31.join(projectDir, config.prdDir);
|
|
12137
12619
|
const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
|
|
12138
|
-
const pendingPath =
|
|
12139
|
-
const donePath =
|
|
12140
|
-
if (
|
|
12620
|
+
const pendingPath = path31.join(prdDir, normalized);
|
|
12621
|
+
const donePath = path31.join(prdDir, "done", normalized);
|
|
12622
|
+
if (fs30.existsSync(pendingPath)) {
|
|
12141
12623
|
res.json({ message: `"${normalized}" is already pending` });
|
|
12142
12624
|
return;
|
|
12143
12625
|
}
|
|
12144
|
-
if (!
|
|
12626
|
+
if (!fs30.existsSync(donePath)) {
|
|
12145
12627
|
res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
|
|
12146
12628
|
return;
|
|
12147
12629
|
}
|
|
12148
|
-
|
|
12630
|
+
fs30.renameSync(donePath, pendingPath);
|
|
12149
12631
|
res.json({ message: `Moved "${normalized}" back to pending` });
|
|
12150
12632
|
} catch (error2) {
|
|
12151
12633
|
res.status(500).json({
|
|
@@ -12163,11 +12645,11 @@ function createActionRouteHandlers(ctx) {
|
|
|
12163
12645
|
res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
|
|
12164
12646
|
return;
|
|
12165
12647
|
}
|
|
12166
|
-
if (
|
|
12167
|
-
|
|
12648
|
+
if (fs30.existsSync(lockPath)) {
|
|
12649
|
+
fs30.unlinkSync(lockPath);
|
|
12168
12650
|
}
|
|
12169
|
-
const prdDir =
|
|
12170
|
-
if (
|
|
12651
|
+
const prdDir = path31.join(projectDir, config.prdDir);
|
|
12652
|
+
if (fs30.existsSync(prdDir)) {
|
|
12171
12653
|
cleanOrphanedClaims(prdDir);
|
|
12172
12654
|
}
|
|
12173
12655
|
broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
|
|
@@ -12363,6 +12845,7 @@ function createBoardRouteHandlers(ctx) {
|
|
|
12363
12845
|
return;
|
|
12364
12846
|
}
|
|
12365
12847
|
await provider.closeIssue(issueNumber);
|
|
12848
|
+
await provider.moveIssue(issueNumber, "Done");
|
|
12366
12849
|
invalidateBoardCache(projectDir);
|
|
12367
12850
|
res.json({ closed: true });
|
|
12368
12851
|
} catch (error2) {
|
|
@@ -12669,6 +13152,34 @@ function validateConfigChanges(changes, currentConfig) {
|
|
|
12669
13152
|
return "audit.maxRuntime must be a number >= 60";
|
|
12670
13153
|
}
|
|
12671
13154
|
}
|
|
13155
|
+
if (changes.analytics !== void 0) {
|
|
13156
|
+
if (typeof changes.analytics !== "object" || changes.analytics === null) {
|
|
13157
|
+
return "analytics must be an object";
|
|
13158
|
+
}
|
|
13159
|
+
const analytics = changes.analytics;
|
|
13160
|
+
if (analytics.enabled !== void 0 && typeof analytics.enabled !== "boolean") {
|
|
13161
|
+
return "analytics.enabled must be a boolean";
|
|
13162
|
+
}
|
|
13163
|
+
const analyticsScheduleError = validateCronField("analytics.schedule", analytics.schedule);
|
|
13164
|
+
if (analyticsScheduleError) {
|
|
13165
|
+
return analyticsScheduleError;
|
|
13166
|
+
}
|
|
13167
|
+
if (analytics.maxRuntime !== void 0 && (typeof analytics.maxRuntime !== "number" || analytics.maxRuntime < 60)) {
|
|
13168
|
+
return "analytics.maxRuntime must be a number >= 60";
|
|
13169
|
+
}
|
|
13170
|
+
if (analytics.lookbackDays !== void 0 && (typeof analytics.lookbackDays !== "number" || analytics.lookbackDays < 1 || analytics.lookbackDays > 90)) {
|
|
13171
|
+
return "analytics.lookbackDays must be a number between 1 and 90";
|
|
13172
|
+
}
|
|
13173
|
+
if (analytics.targetColumn !== void 0) {
|
|
13174
|
+
const validColumns = ["Draft", "Ready", "In Progress", "Review", "Done"];
|
|
13175
|
+
if (!validColumns.includes(analytics.targetColumn)) {
|
|
13176
|
+
return `analytics.targetColumn must be one of: ${validColumns.join(", ")}`;
|
|
13177
|
+
}
|
|
13178
|
+
}
|
|
13179
|
+
if (analytics.analysisPrompt !== void 0 && typeof analytics.analysisPrompt !== "string") {
|
|
13180
|
+
return "analytics.analysisPrompt must be a string";
|
|
13181
|
+
}
|
|
13182
|
+
}
|
|
12672
13183
|
if (changes.queue !== void 0) {
|
|
12673
13184
|
if (typeof changes.queue !== "object" || changes.queue === null) {
|
|
12674
13185
|
return "queue must be an object";
|
|
@@ -12687,7 +13198,14 @@ function validateConfigChanges(changes, currentConfig) {
|
|
|
12687
13198
|
if (typeof queue.priority !== "object" || queue.priority === null) {
|
|
12688
13199
|
return "queue.priority must be an object";
|
|
12689
13200
|
}
|
|
12690
|
-
const validQueueJobs = [
|
|
13201
|
+
const validQueueJobs = [
|
|
13202
|
+
"executor",
|
|
13203
|
+
"reviewer",
|
|
13204
|
+
"qa",
|
|
13205
|
+
"audit",
|
|
13206
|
+
"slicer",
|
|
13207
|
+
"analytics"
|
|
13208
|
+
];
|
|
12691
13209
|
for (const [jobType, value] of Object.entries(queue.priority)) {
|
|
12692
13210
|
if (!validQueueJobs.includes(jobType)) {
|
|
12693
13211
|
return `queue.priority contains invalid job type: ${jobType}`;
|
|
@@ -12790,6 +13308,26 @@ function createConfigRoutes(deps) {
|
|
|
12790
13308
|
});
|
|
12791
13309
|
return router;
|
|
12792
13310
|
}
|
|
13311
|
+
function createGlobalNotificationsRoutes() {
|
|
13312
|
+
const router = Router3();
|
|
13313
|
+
router.get("/", (_req, res) => {
|
|
13314
|
+
res.json(loadGlobalNotificationsConfig());
|
|
13315
|
+
});
|
|
13316
|
+
router.put("/", (req, res) => {
|
|
13317
|
+
const { webhook } = req.body;
|
|
13318
|
+
if (webhook !== null && webhook !== void 0) {
|
|
13319
|
+
const issues = validateWebhook(webhook);
|
|
13320
|
+
if (issues.length > 0) {
|
|
13321
|
+
res.status(400).json({ error: `Invalid webhook: ${issues.join(", ")}` });
|
|
13322
|
+
return;
|
|
13323
|
+
}
|
|
13324
|
+
}
|
|
13325
|
+
const config = { webhook: webhook ?? null };
|
|
13326
|
+
saveGlobalNotificationsConfig(config);
|
|
13327
|
+
res.json(loadGlobalNotificationsConfig());
|
|
13328
|
+
});
|
|
13329
|
+
return router;
|
|
13330
|
+
}
|
|
12793
13331
|
function createProjectConfigRoutes() {
|
|
12794
13332
|
const router = Router3({ mergeParams: true });
|
|
12795
13333
|
router.get("/config", (req, res) => {
|
|
@@ -12823,8 +13361,8 @@ function createProjectConfigRoutes() {
|
|
|
12823
13361
|
|
|
12824
13362
|
// ../server/dist/routes/doctor.routes.js
|
|
12825
13363
|
init_dist();
|
|
12826
|
-
import * as
|
|
12827
|
-
import * as
|
|
13364
|
+
import * as fs31 from "fs";
|
|
13365
|
+
import * as path32 from "path";
|
|
12828
13366
|
import { execSync as execSync6 } from "child_process";
|
|
12829
13367
|
import { Router as Router4 } from "express";
|
|
12830
13368
|
function runDoctorChecks(projectDir, config) {
|
|
@@ -12857,7 +13395,7 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12857
13395
|
});
|
|
12858
13396
|
}
|
|
12859
13397
|
try {
|
|
12860
|
-
const projectName =
|
|
13398
|
+
const projectName = path32.basename(projectDir);
|
|
12861
13399
|
const marker = generateMarker(projectName);
|
|
12862
13400
|
const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
|
|
12863
13401
|
if (crontabEntries.length > 0) {
|
|
@@ -12880,8 +13418,8 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12880
13418
|
detail: "Failed to check crontab"
|
|
12881
13419
|
});
|
|
12882
13420
|
}
|
|
12883
|
-
const configPath =
|
|
12884
|
-
if (
|
|
13421
|
+
const configPath = path32.join(projectDir, CONFIG_FILE_NAME);
|
|
13422
|
+
if (fs31.existsSync(configPath)) {
|
|
12885
13423
|
checks.push({ name: "config", status: "pass", detail: "Config file exists" });
|
|
12886
13424
|
} else {
|
|
12887
13425
|
checks.push({
|
|
@@ -12890,9 +13428,9 @@ function runDoctorChecks(projectDir, config) {
|
|
|
12890
13428
|
detail: "Config file not found (using defaults)"
|
|
12891
13429
|
});
|
|
12892
13430
|
}
|
|
12893
|
-
const prdDir =
|
|
12894
|
-
if (
|
|
12895
|
-
const prds =
|
|
13431
|
+
const prdDir = path32.join(projectDir, config.prdDir);
|
|
13432
|
+
if (fs31.existsSync(prdDir)) {
|
|
13433
|
+
const prds = fs31.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
|
|
12896
13434
|
checks.push({
|
|
12897
13435
|
name: "prdDir",
|
|
12898
13436
|
status: "pass",
|
|
@@ -12935,7 +13473,7 @@ function createProjectDoctorRoutes() {
|
|
|
12935
13473
|
|
|
12936
13474
|
// ../server/dist/routes/log.routes.js
|
|
12937
13475
|
init_dist();
|
|
12938
|
-
import * as
|
|
13476
|
+
import * as path33 from "path";
|
|
12939
13477
|
import { Router as Router5 } from "express";
|
|
12940
13478
|
function createLogRoutes(deps) {
|
|
12941
13479
|
const { projectDir } = deps;
|
|
@@ -12943,7 +13481,7 @@ function createLogRoutes(deps) {
|
|
|
12943
13481
|
router.get("/:name", (req, res) => {
|
|
12944
13482
|
try {
|
|
12945
13483
|
const { name } = req.params;
|
|
12946
|
-
const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
|
|
13484
|
+
const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
|
|
12947
13485
|
if (!validNames.includes(name)) {
|
|
12948
13486
|
res.status(400).json({
|
|
12949
13487
|
error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
|
|
@@ -12954,7 +13492,7 @@ function createLogRoutes(deps) {
|
|
|
12954
13492
|
const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
|
|
12955
13493
|
const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
|
|
12956
13494
|
const fileName = LOG_FILE_NAMES[name] || name;
|
|
12957
|
-
const logPath =
|
|
13495
|
+
const logPath = path33.join(projectDir, LOG_DIR, `${fileName}.log`);
|
|
12958
13496
|
const logLines = getLastLogLines(logPath, linesToRead);
|
|
12959
13497
|
res.json({ name, lines: logLines });
|
|
12960
13498
|
} catch (error2) {
|
|
@@ -12969,7 +13507,7 @@ function createProjectLogRoutes() {
|
|
|
12969
13507
|
try {
|
|
12970
13508
|
const projectDir = req.projectDir;
|
|
12971
13509
|
const { name } = req.params;
|
|
12972
|
-
const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
|
|
13510
|
+
const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
|
|
12973
13511
|
if (!validNames.includes(name)) {
|
|
12974
13512
|
res.status(400).json({
|
|
12975
13513
|
error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
|
|
@@ -12980,7 +13518,7 @@ function createProjectLogRoutes() {
|
|
|
12980
13518
|
const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
|
|
12981
13519
|
const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
|
|
12982
13520
|
const fileName = LOG_FILE_NAMES[name] || name;
|
|
12983
|
-
const logPath =
|
|
13521
|
+
const logPath = path33.join(projectDir, LOG_DIR, `${fileName}.log`);
|
|
12984
13522
|
const logLines = getLastLogLines(logPath, linesToRead);
|
|
12985
13523
|
res.json({ name, lines: logLines });
|
|
12986
13524
|
} catch (error2) {
|
|
@@ -13015,7 +13553,7 @@ function createProjectPrdRoutes() {
|
|
|
13015
13553
|
|
|
13016
13554
|
// ../server/dist/routes/roadmap.routes.js
|
|
13017
13555
|
init_dist();
|
|
13018
|
-
import * as
|
|
13556
|
+
import * as path34 from "path";
|
|
13019
13557
|
import { Router as Router7 } from "express";
|
|
13020
13558
|
function createRoadmapRouteHandlers(ctx) {
|
|
13021
13559
|
const router = Router7({ mergeParams: true });
|
|
@@ -13025,7 +13563,7 @@ function createRoadmapRouteHandlers(ctx) {
|
|
|
13025
13563
|
const config = ctx.getConfig(req);
|
|
13026
13564
|
const projectDir = ctx.getProjectDir(req);
|
|
13027
13565
|
const status = getRoadmapStatus(projectDir, config);
|
|
13028
|
-
const prdDir =
|
|
13566
|
+
const prdDir = path34.join(projectDir, config.prdDir);
|
|
13029
13567
|
const state = loadRoadmapState(prdDir);
|
|
13030
13568
|
res.json({
|
|
13031
13569
|
...status,
|
|
@@ -13149,11 +13687,13 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
|
|
|
13149
13687
|
const qaPlan = getSchedulingPlan(projectDir, config, "qa");
|
|
13150
13688
|
const auditPlan = getSchedulingPlan(projectDir, config, "audit");
|
|
13151
13689
|
const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
|
|
13690
|
+
const analyticsPlan = getSchedulingPlan(projectDir, config, "analytics");
|
|
13152
13691
|
const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
|
|
13153
13692
|
const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
|
|
13154
13693
|
const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
|
|
13155
13694
|
const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
|
|
13156
13695
|
const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
|
|
13696
|
+
const analyticsInstalled = installed && config.analytics.enabled && hasScheduledCommand(entries, "analytics");
|
|
13157
13697
|
return {
|
|
13158
13698
|
executor: {
|
|
13159
13699
|
schedule: config.cronSchedule,
|
|
@@ -13195,6 +13735,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
|
|
|
13195
13735
|
manualDelayMinutes: plannerPlan.manualDelayMinutes,
|
|
13196
13736
|
balancedDelayMinutes: plannerPlan.balancedDelayMinutes
|
|
13197
13737
|
},
|
|
13738
|
+
analytics: {
|
|
13739
|
+
schedule: config.analytics.schedule,
|
|
13740
|
+
installed: analyticsInstalled,
|
|
13741
|
+
nextRun: analyticsInstalled ? addDelayToIsoString(computeNextRun(config.analytics.schedule), analyticsPlan.totalDelayMinutes) : null,
|
|
13742
|
+
delayMinutes: analyticsPlan.totalDelayMinutes,
|
|
13743
|
+
manualDelayMinutes: analyticsPlan.manualDelayMinutes,
|
|
13744
|
+
balancedDelayMinutes: analyticsPlan.balancedDelayMinutes
|
|
13745
|
+
},
|
|
13198
13746
|
paused: !installed,
|
|
13199
13747
|
schedulingPriority: config.schedulingPriority,
|
|
13200
13748
|
entries
|
|
@@ -13347,26 +13895,26 @@ function createQueueRoutes(deps) {
|
|
|
13347
13895
|
|
|
13348
13896
|
// ../server/dist/index.js
|
|
13349
13897
|
var __filename2 = fileURLToPath3(import.meta.url);
|
|
13350
|
-
var __dirname3 =
|
|
13898
|
+
var __dirname3 = dirname8(__filename2);
|
|
13351
13899
|
function resolveWebDistPath() {
|
|
13352
|
-
const bundled =
|
|
13353
|
-
if (
|
|
13900
|
+
const bundled = path35.join(__dirname3, "web");
|
|
13901
|
+
if (fs32.existsSync(path35.join(bundled, "index.html")))
|
|
13354
13902
|
return bundled;
|
|
13355
13903
|
let d = __dirname3;
|
|
13356
13904
|
for (let i = 0; i < 8; i++) {
|
|
13357
|
-
if (
|
|
13358
|
-
const dev =
|
|
13359
|
-
if (
|
|
13905
|
+
if (fs32.existsSync(path35.join(d, "turbo.json"))) {
|
|
13906
|
+
const dev = path35.join(d, "web/dist");
|
|
13907
|
+
if (fs32.existsSync(path35.join(dev, "index.html")))
|
|
13360
13908
|
return dev;
|
|
13361
13909
|
break;
|
|
13362
13910
|
}
|
|
13363
|
-
d =
|
|
13911
|
+
d = dirname8(d);
|
|
13364
13912
|
}
|
|
13365
13913
|
return bundled;
|
|
13366
13914
|
}
|
|
13367
13915
|
function setupStaticFiles(app) {
|
|
13368
13916
|
const webDistPath = resolveWebDistPath();
|
|
13369
|
-
if (
|
|
13917
|
+
if (fs32.existsSync(webDistPath)) {
|
|
13370
13918
|
app.use(express.static(webDistPath));
|
|
13371
13919
|
}
|
|
13372
13920
|
app.use((req, res, next) => {
|
|
@@ -13374,8 +13922,8 @@ function setupStaticFiles(app) {
|
|
|
13374
13922
|
next();
|
|
13375
13923
|
return;
|
|
13376
13924
|
}
|
|
13377
|
-
const indexPath =
|
|
13378
|
-
if (
|
|
13925
|
+
const indexPath = path35.resolve(webDistPath, "index.html");
|
|
13926
|
+
if (fs32.existsSync(indexPath)) {
|
|
13379
13927
|
res.sendFile(indexPath, (err) => {
|
|
13380
13928
|
if (err)
|
|
13381
13929
|
next();
|
|
@@ -13413,6 +13961,7 @@ function createApp(projectDir) {
|
|
|
13413
13961
|
app.use("/api/logs", createLogRoutes({ projectDir }));
|
|
13414
13962
|
app.use("/api/doctor", createDoctorRoutes({ projectDir, getConfig: () => config }));
|
|
13415
13963
|
app.use("/api/queue", createQueueRoutes({ getConfig: () => config }));
|
|
13964
|
+
app.use("/api/global-notifications", createGlobalNotificationsRoutes());
|
|
13416
13965
|
app.get("/api/prs", async (_req, res) => {
|
|
13417
13966
|
try {
|
|
13418
13967
|
res.json(await collectPrInfo(projectDir, config.branchPatterns));
|
|
@@ -13485,13 +14034,14 @@ function createGlobalApp() {
|
|
|
13485
14034
|
}
|
|
13486
14035
|
});
|
|
13487
14036
|
app.use("/api/queue", createGlobalQueueRoutes());
|
|
14037
|
+
app.use("/api/global-notifications", createGlobalNotificationsRoutes());
|
|
13488
14038
|
app.use("/api/projects/:projectId", resolveProject, createProjectRouter());
|
|
13489
14039
|
setupStaticFiles(app);
|
|
13490
14040
|
app.use(errorHandler);
|
|
13491
14041
|
return app;
|
|
13492
14042
|
}
|
|
13493
14043
|
function bootContainer() {
|
|
13494
|
-
initContainer(
|
|
14044
|
+
initContainer(path35.dirname(getDbPath()));
|
|
13495
14045
|
}
|
|
13496
14046
|
function startServer(projectDir, port) {
|
|
13497
14047
|
bootContainer();
|
|
@@ -13544,8 +14094,8 @@ function isProcessRunning2(pid) {
|
|
|
13544
14094
|
}
|
|
13545
14095
|
function readPid(lockPath) {
|
|
13546
14096
|
try {
|
|
13547
|
-
if (!
|
|
13548
|
-
const raw =
|
|
14097
|
+
if (!fs33.existsSync(lockPath)) return null;
|
|
14098
|
+
const raw = fs33.readFileSync(lockPath, "utf-8").trim();
|
|
13549
14099
|
const pid = parseInt(raw, 10);
|
|
13550
14100
|
return Number.isFinite(pid) ? pid : null;
|
|
13551
14101
|
} catch {
|
|
@@ -13557,10 +14107,10 @@ function acquireServeLock(mode, port) {
|
|
|
13557
14107
|
let stalePidCleaned;
|
|
13558
14108
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
13559
14109
|
try {
|
|
13560
|
-
const fd =
|
|
13561
|
-
|
|
14110
|
+
const fd = fs33.openSync(lockPath, "wx");
|
|
14111
|
+
fs33.writeFileSync(fd, `${process.pid}
|
|
13562
14112
|
`);
|
|
13563
|
-
|
|
14113
|
+
fs33.closeSync(fd);
|
|
13564
14114
|
return { acquired: true, lockPath, stalePidCleaned };
|
|
13565
14115
|
} catch (error2) {
|
|
13566
14116
|
const err = error2;
|
|
@@ -13581,7 +14131,7 @@ function acquireServeLock(mode, port) {
|
|
|
13581
14131
|
};
|
|
13582
14132
|
}
|
|
13583
14133
|
try {
|
|
13584
|
-
|
|
14134
|
+
fs33.unlinkSync(lockPath);
|
|
13585
14135
|
if (existingPid) {
|
|
13586
14136
|
stalePidCleaned = existingPid;
|
|
13587
14137
|
}
|
|
@@ -13604,10 +14154,10 @@ function acquireServeLock(mode, port) {
|
|
|
13604
14154
|
}
|
|
13605
14155
|
function releaseServeLock(lockPath) {
|
|
13606
14156
|
try {
|
|
13607
|
-
if (!
|
|
14157
|
+
if (!fs33.existsSync(lockPath)) return;
|
|
13608
14158
|
const lockPid = readPid(lockPath);
|
|
13609
14159
|
if (lockPid !== null && lockPid !== process.pid) return;
|
|
13610
|
-
|
|
14160
|
+
fs33.unlinkSync(lockPath);
|
|
13611
14161
|
} catch {
|
|
13612
14162
|
}
|
|
13613
14163
|
}
|
|
@@ -13703,14 +14253,14 @@ function historyCommand(program2) {
|
|
|
13703
14253
|
// src/commands/update.ts
|
|
13704
14254
|
init_dist();
|
|
13705
14255
|
import { spawnSync } from "child_process";
|
|
13706
|
-
import * as
|
|
13707
|
-
import * as
|
|
14256
|
+
import * as fs34 from "fs";
|
|
14257
|
+
import * as path36 from "path";
|
|
13708
14258
|
var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
|
|
13709
14259
|
function parseProjectDirs(projects, cwd) {
|
|
13710
14260
|
if (!projects || projects.trim().length === 0) {
|
|
13711
14261
|
return [cwd];
|
|
13712
14262
|
}
|
|
13713
|
-
const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) =>
|
|
14263
|
+
const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path36.resolve(cwd, entry));
|
|
13714
14264
|
return Array.from(new Set(dirs));
|
|
13715
14265
|
}
|
|
13716
14266
|
function shouldInstallGlobal(options) {
|
|
@@ -13752,7 +14302,7 @@ function updateCommand(program2) {
|
|
|
13752
14302
|
}
|
|
13753
14303
|
const nightWatchBin = resolveNightWatchBin();
|
|
13754
14304
|
for (const projectDir of projectDirs) {
|
|
13755
|
-
if (!
|
|
14305
|
+
if (!fs34.existsSync(projectDir) || !fs34.statSync(projectDir).isDirectory()) {
|
|
13756
14306
|
warn(`Skipping invalid project directory: ${projectDir}`);
|
|
13757
14307
|
continue;
|
|
13758
14308
|
}
|
|
@@ -13796,8 +14346,8 @@ function prdStateCommand(program2) {
|
|
|
13796
14346
|
|
|
13797
14347
|
// src/commands/retry.ts
|
|
13798
14348
|
init_dist();
|
|
13799
|
-
import * as
|
|
13800
|
-
import * as
|
|
14349
|
+
import * as fs35 from "fs";
|
|
14350
|
+
import * as path37 from "path";
|
|
13801
14351
|
function normalizePrdName(name) {
|
|
13802
14352
|
if (!name.endsWith(".md")) {
|
|
13803
14353
|
return `${name}.md`;
|
|
@@ -13805,26 +14355,26 @@ function normalizePrdName(name) {
|
|
|
13805
14355
|
return name;
|
|
13806
14356
|
}
|
|
13807
14357
|
function getDonePrds(doneDir) {
|
|
13808
|
-
if (!
|
|
14358
|
+
if (!fs35.existsSync(doneDir)) {
|
|
13809
14359
|
return [];
|
|
13810
14360
|
}
|
|
13811
|
-
return
|
|
14361
|
+
return fs35.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
|
|
13812
14362
|
}
|
|
13813
14363
|
function retryCommand(program2) {
|
|
13814
14364
|
program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
|
|
13815
14365
|
const projectDir = process.cwd();
|
|
13816
14366
|
const config = loadConfig(projectDir);
|
|
13817
|
-
const prdDir =
|
|
13818
|
-
const doneDir =
|
|
14367
|
+
const prdDir = path37.join(projectDir, config.prdDir);
|
|
14368
|
+
const doneDir = path37.join(prdDir, "done");
|
|
13819
14369
|
const normalizedPrdName = normalizePrdName(prdName);
|
|
13820
|
-
const pendingPath =
|
|
13821
|
-
if (
|
|
14370
|
+
const pendingPath = path37.join(prdDir, normalizedPrdName);
|
|
14371
|
+
if (fs35.existsSync(pendingPath)) {
|
|
13822
14372
|
info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
|
|
13823
14373
|
return;
|
|
13824
14374
|
}
|
|
13825
|
-
const donePath =
|
|
13826
|
-
if (
|
|
13827
|
-
|
|
14375
|
+
const donePath = path37.join(doneDir, normalizedPrdName);
|
|
14376
|
+
if (fs35.existsSync(donePath)) {
|
|
14377
|
+
fs35.renameSync(donePath, pendingPath);
|
|
13828
14378
|
success(`Moved "${normalizedPrdName}" back to pending.`);
|
|
13829
14379
|
dim(`From: ${donePath}`);
|
|
13830
14380
|
dim(`To: ${pendingPath}`);
|
|
@@ -14076,7 +14626,7 @@ function prdsCommand(program2) {
|
|
|
14076
14626
|
|
|
14077
14627
|
// src/commands/cancel.ts
|
|
14078
14628
|
init_dist();
|
|
14079
|
-
import * as
|
|
14629
|
+
import * as fs36 from "fs";
|
|
14080
14630
|
import * as readline3 from "readline";
|
|
14081
14631
|
function getLockFilePaths2(projectDir) {
|
|
14082
14632
|
const runtimeKey = projectRuntimeKey(projectDir);
|
|
@@ -14123,7 +14673,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
14123
14673
|
const pid = lockStatus.pid;
|
|
14124
14674
|
if (!lockStatus.running) {
|
|
14125
14675
|
try {
|
|
14126
|
-
|
|
14676
|
+
fs36.unlinkSync(lockPath);
|
|
14127
14677
|
return {
|
|
14128
14678
|
success: true,
|
|
14129
14679
|
message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
|
|
@@ -14161,7 +14711,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
14161
14711
|
await sleep2(3e3);
|
|
14162
14712
|
if (!isProcessRunning3(pid)) {
|
|
14163
14713
|
try {
|
|
14164
|
-
|
|
14714
|
+
fs36.unlinkSync(lockPath);
|
|
14165
14715
|
} catch {
|
|
14166
14716
|
}
|
|
14167
14717
|
return {
|
|
@@ -14196,7 +14746,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
|
|
|
14196
14746
|
await sleep2(500);
|
|
14197
14747
|
if (!isProcessRunning3(pid)) {
|
|
14198
14748
|
try {
|
|
14199
|
-
|
|
14749
|
+
fs36.unlinkSync(lockPath);
|
|
14200
14750
|
} catch {
|
|
14201
14751
|
}
|
|
14202
14752
|
return {
|
|
@@ -14257,31 +14807,31 @@ function cancelCommand(program2) {
|
|
|
14257
14807
|
|
|
14258
14808
|
// src/commands/slice.ts
|
|
14259
14809
|
init_dist();
|
|
14260
|
-
import * as
|
|
14261
|
-
import * as
|
|
14810
|
+
import * as fs37 from "fs";
|
|
14811
|
+
import * as path38 from "path";
|
|
14262
14812
|
function plannerLockPath2(projectDir) {
|
|
14263
14813
|
return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
|
|
14264
14814
|
}
|
|
14265
14815
|
function acquirePlannerLock(projectDir) {
|
|
14266
14816
|
const lockFile = plannerLockPath2(projectDir);
|
|
14267
|
-
if (
|
|
14268
|
-
const pidRaw =
|
|
14817
|
+
if (fs37.existsSync(lockFile)) {
|
|
14818
|
+
const pidRaw = fs37.readFileSync(lockFile, "utf-8").trim();
|
|
14269
14819
|
const pid = parseInt(pidRaw, 10);
|
|
14270
14820
|
if (!Number.isNaN(pid) && isProcessRunning(pid)) {
|
|
14271
14821
|
return { acquired: false, lockFile, pid };
|
|
14272
14822
|
}
|
|
14273
14823
|
try {
|
|
14274
|
-
|
|
14824
|
+
fs37.unlinkSync(lockFile);
|
|
14275
14825
|
} catch {
|
|
14276
14826
|
}
|
|
14277
14827
|
}
|
|
14278
|
-
|
|
14828
|
+
fs37.writeFileSync(lockFile, String(process.pid));
|
|
14279
14829
|
return { acquired: true, lockFile };
|
|
14280
14830
|
}
|
|
14281
14831
|
function releasePlannerLock(lockFile) {
|
|
14282
14832
|
try {
|
|
14283
|
-
if (
|
|
14284
|
-
|
|
14833
|
+
if (fs37.existsSync(lockFile)) {
|
|
14834
|
+
fs37.unlinkSync(lockFile);
|
|
14285
14835
|
}
|
|
14286
14836
|
} catch {
|
|
14287
14837
|
}
|
|
@@ -14290,12 +14840,12 @@ function resolvePlannerIssueColumn(config) {
|
|
|
14290
14840
|
return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
|
|
14291
14841
|
}
|
|
14292
14842
|
function buildPlannerIssueBody(projectDir, config, result) {
|
|
14293
|
-
const relativePrdPath =
|
|
14294
|
-
const absolutePrdPath =
|
|
14843
|
+
const relativePrdPath = path38.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
|
|
14844
|
+
const absolutePrdPath = path38.join(projectDir, config.prdDir, result.file ?? "");
|
|
14295
14845
|
const sourceItem = result.item;
|
|
14296
14846
|
let prdContent;
|
|
14297
14847
|
try {
|
|
14298
|
-
prdContent =
|
|
14848
|
+
prdContent = fs37.readFileSync(absolutePrdPath, "utf-8");
|
|
14299
14849
|
} catch {
|
|
14300
14850
|
prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
|
|
14301
14851
|
}
|
|
@@ -14471,7 +15021,7 @@ function sliceCommand(program2) {
|
|
|
14471
15021
|
if (!options.dryRun) {
|
|
14472
15022
|
await sendNotifications(config, {
|
|
14473
15023
|
event: "run_started",
|
|
14474
|
-
projectName:
|
|
15024
|
+
projectName: path38.basename(projectDir),
|
|
14475
15025
|
exitCode: 0,
|
|
14476
15026
|
provider: config.provider
|
|
14477
15027
|
});
|
|
@@ -14506,7 +15056,7 @@ function sliceCommand(program2) {
|
|
|
14506
15056
|
if (!options.dryRun && result.sliced) {
|
|
14507
15057
|
await sendNotifications(config, {
|
|
14508
15058
|
event: "run_succeeded",
|
|
14509
|
-
projectName:
|
|
15059
|
+
projectName: path38.basename(projectDir),
|
|
14510
15060
|
exitCode,
|
|
14511
15061
|
provider: config.provider,
|
|
14512
15062
|
prTitle: result.item?.title
|
|
@@ -14514,7 +15064,7 @@ function sliceCommand(program2) {
|
|
|
14514
15064
|
} else if (!options.dryRun && !nothingPending) {
|
|
14515
15065
|
await sendNotifications(config, {
|
|
14516
15066
|
event: "run_failed",
|
|
14517
|
-
projectName:
|
|
15067
|
+
projectName: path38.basename(projectDir),
|
|
14518
15068
|
exitCode,
|
|
14519
15069
|
provider: config.provider
|
|
14520
15070
|
});
|
|
@@ -14530,21 +15080,21 @@ function sliceCommand(program2) {
|
|
|
14530
15080
|
|
|
14531
15081
|
// src/commands/state.ts
|
|
14532
15082
|
init_dist();
|
|
14533
|
-
import * as
|
|
14534
|
-
import * as
|
|
15083
|
+
import * as os9 from "os";
|
|
15084
|
+
import * as path39 from "path";
|
|
14535
15085
|
import chalk5 from "chalk";
|
|
14536
15086
|
import { Command } from "commander";
|
|
14537
15087
|
function createStateCommand() {
|
|
14538
15088
|
const state = new Command("state");
|
|
14539
15089
|
state.description("Manage Night Watch state");
|
|
14540
15090
|
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 ||
|
|
15091
|
+
const nightWatchHome = process.env.NIGHT_WATCH_HOME || path39.join(os9.homedir(), GLOBAL_CONFIG_DIR);
|
|
14542
15092
|
if (opts.dryRun) {
|
|
14543
15093
|
console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
|
|
14544
15094
|
console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
|
|
14545
|
-
console.log(` ${
|
|
14546
|
-
console.log(` ${
|
|
14547
|
-
console.log(` ${
|
|
15095
|
+
console.log(` ${path39.join(nightWatchHome, "projects.json")}`);
|
|
15096
|
+
console.log(` ${path39.join(nightWatchHome, "history.json")}`);
|
|
15097
|
+
console.log(` ${path39.join(nightWatchHome, "prd-states.json")}`);
|
|
14548
15098
|
console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
|
|
14549
15099
|
console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
|
|
14550
15100
|
return;
|
|
@@ -14582,8 +15132,8 @@ function createStateCommand() {
|
|
|
14582
15132
|
init_dist();
|
|
14583
15133
|
init_dist();
|
|
14584
15134
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
14585
|
-
import * as
|
|
14586
|
-
import * as
|
|
15135
|
+
import * as fs38 from "fs";
|
|
15136
|
+
import * as path40 from "path";
|
|
14587
15137
|
import * as readline4 from "readline";
|
|
14588
15138
|
import chalk6 from "chalk";
|
|
14589
15139
|
async function run(fn) {
|
|
@@ -14605,7 +15155,7 @@ function getProvider(config, cwd) {
|
|
|
14605
15155
|
return createBoardProvider(bp, cwd);
|
|
14606
15156
|
}
|
|
14607
15157
|
function defaultBoardTitle(cwd) {
|
|
14608
|
-
return `${
|
|
15158
|
+
return `${path40.basename(cwd)} Night Watch`;
|
|
14609
15159
|
}
|
|
14610
15160
|
async function ensureBoardConfigured(config, cwd, provider, options) {
|
|
14611
15161
|
if (config.boardProvider?.projectNumber) {
|
|
@@ -14804,11 +15354,11 @@ function boardCommand(program2) {
|
|
|
14804
15354
|
let body = options.body ?? "";
|
|
14805
15355
|
if (options.bodyFile) {
|
|
14806
15356
|
const filePath = options.bodyFile;
|
|
14807
|
-
if (!
|
|
15357
|
+
if (!fs38.existsSync(filePath)) {
|
|
14808
15358
|
console.error(`File not found: ${filePath}`);
|
|
14809
15359
|
process.exit(1);
|
|
14810
15360
|
}
|
|
14811
|
-
body =
|
|
15361
|
+
body = fs38.readFileSync(filePath, "utf-8");
|
|
14812
15362
|
}
|
|
14813
15363
|
const labels = [];
|
|
14814
15364
|
if (options.label) {
|
|
@@ -14836,6 +15386,25 @@ function boardCommand(program2) {
|
|
|
14836
15386
|
}
|
|
14837
15387
|
})
|
|
14838
15388
|
);
|
|
15389
|
+
board.command("add-issue").description("Add an existing GitHub issue to the board").argument("<number>", "Issue number").option("--column <name>", "Target column (default: Ready)", "Ready").action(
|
|
15390
|
+
async (number, options) => run(async () => {
|
|
15391
|
+
const cwd = process.cwd();
|
|
15392
|
+
const config = loadConfig(cwd);
|
|
15393
|
+
const provider = getProvider(config, cwd);
|
|
15394
|
+
await ensureBoardConfigured(config, cwd, provider);
|
|
15395
|
+
if (!BOARD_COLUMNS.includes(options.column)) {
|
|
15396
|
+
console.error(
|
|
15397
|
+
`Invalid column "${options.column}". Valid columns: ${BOARD_COLUMNS.join(", ")}`
|
|
15398
|
+
);
|
|
15399
|
+
process.exit(1);
|
|
15400
|
+
}
|
|
15401
|
+
const issue = await provider.addIssue(
|
|
15402
|
+
parseInt(number, 10),
|
|
15403
|
+
options.column
|
|
15404
|
+
);
|
|
15405
|
+
success(`Added issue #${issue.number} "${issue.title}" to ${options.column}`);
|
|
15406
|
+
})
|
|
15407
|
+
);
|
|
14839
15408
|
board.command("status").description("Show the current state of all issues grouped by column").option("--json", "Output raw JSON").option("--group-by <field>", "Group by: priority, category, or column (default: column)").action(
|
|
14840
15409
|
async (options) => run(async () => {
|
|
14841
15410
|
const cwd = process.cwd();
|
|
@@ -15030,12 +15599,12 @@ function boardCommand(program2) {
|
|
|
15030
15599
|
const config = loadConfig(cwd);
|
|
15031
15600
|
const provider = getProvider(config, cwd);
|
|
15032
15601
|
await ensureBoardConfigured(config, cwd, provider);
|
|
15033
|
-
const roadmapPath = options.roadmap ??
|
|
15034
|
-
if (!
|
|
15602
|
+
const roadmapPath = options.roadmap ?? path40.join(cwd, "ROADMAP.md");
|
|
15603
|
+
if (!fs38.existsSync(roadmapPath)) {
|
|
15035
15604
|
console.error(`Roadmap file not found: ${roadmapPath}`);
|
|
15036
15605
|
process.exit(1);
|
|
15037
15606
|
}
|
|
15038
|
-
const roadmapContent =
|
|
15607
|
+
const roadmapContent = fs38.readFileSync(roadmapPath, "utf-8");
|
|
15039
15608
|
const items = parseRoadmap(roadmapContent);
|
|
15040
15609
|
const uncheckedItems = getUncheckedItems(items);
|
|
15041
15610
|
if (uncheckedItems.length === 0) {
|
|
@@ -15159,11 +15728,11 @@ function boardCommand(program2) {
|
|
|
15159
15728
|
// src/commands/queue.ts
|
|
15160
15729
|
init_dist();
|
|
15161
15730
|
init_dist();
|
|
15162
|
-
import * as
|
|
15731
|
+
import * as path41 from "path";
|
|
15163
15732
|
import { spawn as spawn6 } from "child_process";
|
|
15164
15733
|
import chalk7 from "chalk";
|
|
15165
15734
|
import { Command as Command2 } from "commander";
|
|
15166
|
-
var
|
|
15735
|
+
var logger4 = createLogger("queue");
|
|
15167
15736
|
var VALID_JOB_TYPES2 = ["executor", "reviewer", "qa", "audit", "slicer"];
|
|
15168
15737
|
function formatTimestamp(unixTs) {
|
|
15169
15738
|
if (unixTs === null) return "-";
|
|
@@ -15279,7 +15848,7 @@ function createQueueCommand() {
|
|
|
15279
15848
|
process.exit(1);
|
|
15280
15849
|
}
|
|
15281
15850
|
}
|
|
15282
|
-
const projectName =
|
|
15851
|
+
const projectName = path41.basename(projectDir);
|
|
15283
15852
|
const queueConfig = loadConfig(projectDir).queue;
|
|
15284
15853
|
const id = enqueueJob(projectDir, projectName, jobType, envVars, queueConfig);
|
|
15285
15854
|
console.log(chalk7.green(`Enqueued ${jobType} for ${projectName} (ID: ${id})`));
|
|
@@ -15287,13 +15856,13 @@ function createQueueCommand() {
|
|
|
15287
15856
|
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
15857
|
const entry = dispatchNextJob(loadConfig(process.cwd()).queue);
|
|
15289
15858
|
if (!entry) {
|
|
15290
|
-
|
|
15859
|
+
logger4.info("No pending jobs to dispatch");
|
|
15291
15860
|
return;
|
|
15292
15861
|
}
|
|
15293
|
-
|
|
15862
|
+
logger4.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
|
|
15294
15863
|
const scriptName = getScriptNameForJobType(entry.jobType);
|
|
15295
15864
|
if (!scriptName) {
|
|
15296
|
-
|
|
15865
|
+
logger4.error(`Unknown job type: ${entry.jobType}`);
|
|
15297
15866
|
return;
|
|
15298
15867
|
}
|
|
15299
15868
|
let projectEnv;
|
|
@@ -15312,7 +15881,7 @@ function createQueueCommand() {
|
|
|
15312
15881
|
NW_QUEUE_ENTRY_ID: String(entry.id)
|
|
15313
15882
|
};
|
|
15314
15883
|
const scriptPath = getScriptPath(scriptName);
|
|
15315
|
-
|
|
15884
|
+
logger4.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
|
|
15316
15885
|
try {
|
|
15317
15886
|
const child = spawn6("bash", [scriptPath, entry.projectPath], {
|
|
15318
15887
|
detached: true,
|
|
@@ -15321,11 +15890,11 @@ function createQueueCommand() {
|
|
|
15321
15890
|
cwd: entry.projectPath
|
|
15322
15891
|
});
|
|
15323
15892
|
child.unref();
|
|
15324
|
-
|
|
15893
|
+
logger4.info(`Spawned PID: ${child.pid}`);
|
|
15325
15894
|
markJobRunning(entry.id);
|
|
15326
15895
|
} catch (error2) {
|
|
15327
15896
|
updateJobStatus(entry.id, "pending");
|
|
15328
|
-
|
|
15897
|
+
logger4.error(
|
|
15329
15898
|
`Failed to dispatch ${entry.jobType} for ${entry.projectName}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
15330
15899
|
);
|
|
15331
15900
|
process.exit(1);
|
|
@@ -15435,17 +16004,17 @@ function notifyCommand(program2) {
|
|
|
15435
16004
|
|
|
15436
16005
|
// src/cli.ts
|
|
15437
16006
|
var __filename3 = fileURLToPath4(import.meta.url);
|
|
15438
|
-
var __dirname4 =
|
|
16007
|
+
var __dirname4 = dirname9(__filename3);
|
|
15439
16008
|
function findPackageRoot(dir) {
|
|
15440
16009
|
let d = dir;
|
|
15441
16010
|
for (let i = 0; i < 5; i++) {
|
|
15442
|
-
if (
|
|
15443
|
-
d =
|
|
16011
|
+
if (existsSync30(join36(d, "package.json"))) return d;
|
|
16012
|
+
d = dirname9(d);
|
|
15444
16013
|
}
|
|
15445
16014
|
return dir;
|
|
15446
16015
|
}
|
|
15447
16016
|
var packageRoot = findPackageRoot(__dirname4);
|
|
15448
|
-
var packageJson = JSON.parse(
|
|
16017
|
+
var packageJson = JSON.parse(readFileSync19(join36(packageRoot, "package.json"), "utf-8"));
|
|
15449
16018
|
var program = new Command3();
|
|
15450
16019
|
program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
|
|
15451
16020
|
initCommand(program);
|
|
@@ -15453,6 +16022,7 @@ runCommand(program);
|
|
|
15453
16022
|
reviewCommand(program);
|
|
15454
16023
|
qaCommand(program);
|
|
15455
16024
|
auditCommand(program);
|
|
16025
|
+
analyticsCommand(program);
|
|
15456
16026
|
installCommand(program);
|
|
15457
16027
|
uninstallCommand(program);
|
|
15458
16028
|
statusCommand(program);
|