@jonit-dev/night-watch-cli 1.1.4 → 1.2.0
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/README.md +49 -426
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/dashboard.d.ts +29 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +297 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/doctor.d.ts +16 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +155 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +23 -17
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install.d.ts +1 -1
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +2 -2
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/logs.d.ts +1 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +1 -1
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/prd.d.ts +24 -0
- package/dist/commands/prd.d.ts.map +1 -0
- package/dist/commands/prd.js +283 -0
- package/dist/commands/prd.js.map +1 -0
- package/dist/commands/review.d.ts +3 -3
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +26 -2
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts +22 -3
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +57 -8
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +21 -182
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/uninstall.d.ts +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +2 -2
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +40 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +3 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -0
- package/dist/constants.js.map +1 -1
- package/dist/templates/prd-template.d.ts +11 -0
- package/dist/templates/prd-template.d.ts.map +1 -0
- package/dist/templates/prd-template.js +166 -0
- package/dist/templates/prd-template.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/crontab.js +1 -1
- package/dist/utils/crontab.js.map +1 -1
- package/dist/utils/github.d.ts +30 -0
- package/dist/utils/github.d.ts.map +1 -0
- package/dist/utils/github.js +104 -0
- package/dist/utils/github.js.map +1 -0
- package/dist/utils/notify.d.ts +63 -0
- package/dist/utils/notify.d.ts.map +1 -0
- package/dist/utils/notify.js +237 -0
- package/dist/utils/notify.js.map +1 -0
- package/dist/utils/status-data.d.ts +128 -0
- package/dist/utils/status-data.d.ts.map +1 -0
- package/dist/utils/status-data.js +403 -0
- package/dist/utils/status-data.js.map +1 -0
- package/package.json +13 -5
- package/scripts/night-watch-cron.sh +8 -1
- package/scripts/night-watch-helpers.sh +51 -0
- package/scripts/test-helpers.bats +77 -0
- package/templates/prd.md +26 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status data layer for Night Watch CLI
|
|
3
|
+
* Provides data-fetching functions used by both the status command and the dashboard TUI.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { CLAIM_FILE_EXTENSION, LOCK_FILE_PREFIX, LOG_DIR } from "../constants.js";
|
|
9
|
+
import { generateMarker, getEntries, getProjectEntries } from "./crontab.js";
|
|
10
|
+
/**
|
|
11
|
+
* Get the project name from directory or package.json
|
|
12
|
+
*/
|
|
13
|
+
export function getProjectName(projectDir) {
|
|
14
|
+
const packageJsonPath = path.join(projectDir, "package.json");
|
|
15
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
18
|
+
if (packageJson.name) {
|
|
19
|
+
return packageJson.name;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Ignore parse errors
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return path.basename(projectDir);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a process with the given PID is running
|
|
30
|
+
*/
|
|
31
|
+
export function isProcessRunning(pid) {
|
|
32
|
+
try {
|
|
33
|
+
process.kill(pid, 0);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read PID from lock file and check if process is running
|
|
42
|
+
*/
|
|
43
|
+
export function checkLockFile(lockPath) {
|
|
44
|
+
if (!fs.existsSync(lockPath)) {
|
|
45
|
+
return { running: false, pid: null };
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const pidStr = fs.readFileSync(lockPath, "utf-8").trim();
|
|
49
|
+
const pid = parseInt(pidStr, 10);
|
|
50
|
+
if (isNaN(pid)) {
|
|
51
|
+
return { running: false, pid: null };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
running: isProcessRunning(pid),
|
|
55
|
+
pid,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { running: false, pid: null };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Count PRDs in the PRD directory and return counts
|
|
64
|
+
*/
|
|
65
|
+
export function countPRDs(projectDir, prdDir, maxRuntime) {
|
|
66
|
+
const fullPrdPath = path.join(projectDir, prdDir);
|
|
67
|
+
if (!fs.existsSync(fullPrdPath)) {
|
|
68
|
+
return { pending: 0, claimed: 0, done: 0 };
|
|
69
|
+
}
|
|
70
|
+
let pending = 0;
|
|
71
|
+
let claimed = 0;
|
|
72
|
+
let done = 0;
|
|
73
|
+
const countInDir = (dir) => {
|
|
74
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(dir, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
if (entry.name === "done") {
|
|
79
|
+
try {
|
|
80
|
+
const doneEntries = fs.readdirSync(fullPath);
|
|
81
|
+
done += doneEntries.filter((e) => e.endsWith(".md")).length;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Ignore errors
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
countInDir(fullPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (entry.name.endsWith(".md")) {
|
|
92
|
+
const claimPath = path.join(dir, entry.name + CLAIM_FILE_EXTENSION);
|
|
93
|
+
if (fs.existsSync(claimPath)) {
|
|
94
|
+
try {
|
|
95
|
+
const content = fs.readFileSync(claimPath, "utf-8");
|
|
96
|
+
const claimData = JSON.parse(content);
|
|
97
|
+
const age = Math.floor(Date.now() / 1000) - claimData.timestamp;
|
|
98
|
+
if (age < maxRuntime) {
|
|
99
|
+
claimed++;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
pending++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
pending++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
pending++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
countInDir(fullPrdPath);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Ignore errors
|
|
120
|
+
}
|
|
121
|
+
return { pending, claimed, done };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parse dependency references from a PRD file.
|
|
125
|
+
* Looks for a line matching "depends on: `name1`, `name2`" (case-insensitive).
|
|
126
|
+
*/
|
|
127
|
+
export function parsePrdDependencies(prdPath) {
|
|
128
|
+
try {
|
|
129
|
+
const content = fs.readFileSync(prdPath, "utf-8");
|
|
130
|
+
const match = content.match(/depends on[:\s]*([^\n]+)/i);
|
|
131
|
+
if (!match)
|
|
132
|
+
return [];
|
|
133
|
+
return match[1]
|
|
134
|
+
.split(",")
|
|
135
|
+
.map((d) => d.trim().replace(/`/g, ""))
|
|
136
|
+
.filter((d) => d.length > 0);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Collect PRD info items from the PRD directory
|
|
144
|
+
*/
|
|
145
|
+
export function collectPrdInfo(projectDir, prdDir, maxRuntime) {
|
|
146
|
+
const fullPrdPath = path.join(projectDir, prdDir);
|
|
147
|
+
const prds = [];
|
|
148
|
+
if (!fs.existsSync(fullPrdPath)) {
|
|
149
|
+
return prds;
|
|
150
|
+
}
|
|
151
|
+
const collectInDir = (dir) => {
|
|
152
|
+
let entries;
|
|
153
|
+
try {
|
|
154
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const fullPath = path.join(dir, entry.name);
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
if (entry.name === "done") {
|
|
163
|
+
try {
|
|
164
|
+
const doneEntries = fs.readdirSync(fullPath);
|
|
165
|
+
for (const doneEntry of doneEntries) {
|
|
166
|
+
if (doneEntry.endsWith(".md")) {
|
|
167
|
+
prds.push({
|
|
168
|
+
name: doneEntry.replace(/\.md$/, ""),
|
|
169
|
+
status: "done",
|
|
170
|
+
dependencies: [],
|
|
171
|
+
unmetDependencies: [],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Ignore errors
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
collectInDir(fullPath);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (entry.name.endsWith(".md")) {
|
|
185
|
+
const claimPath = path.join(dir, entry.name + CLAIM_FILE_EXTENSION);
|
|
186
|
+
let status = "ready";
|
|
187
|
+
if (fs.existsSync(claimPath)) {
|
|
188
|
+
try {
|
|
189
|
+
const content = fs.readFileSync(claimPath, "utf-8");
|
|
190
|
+
const claimData = JSON.parse(content);
|
|
191
|
+
const age = Math.floor(Date.now() / 1000) - claimData.timestamp;
|
|
192
|
+
status = age < maxRuntime ? "in-progress" : "ready";
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
status = "ready";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const dependencies = parsePrdDependencies(fullPath);
|
|
199
|
+
prds.push({
|
|
200
|
+
name: entry.name.replace(/\.md$/, ""),
|
|
201
|
+
status,
|
|
202
|
+
dependencies,
|
|
203
|
+
unmetDependencies: [],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
collectInDir(fullPrdPath);
|
|
209
|
+
// Compute unmet dependencies: a dependency is unmet if there's no "done" PRD with that name
|
|
210
|
+
const doneNames = new Set(prds.filter((p) => p.status === "done").map((p) => p.name));
|
|
211
|
+
for (const prd of prds) {
|
|
212
|
+
if (prd.dependencies.length > 0) {
|
|
213
|
+
prd.unmetDependencies = prd.dependencies.filter((dep) => !doneNames.has(dep));
|
|
214
|
+
// Mark PRDs with unmet dependencies as blocked (unless already done or in-progress)
|
|
215
|
+
if (prd.unmetDependencies.length > 0 && prd.status === "ready") {
|
|
216
|
+
prd.status = "blocked";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return prds;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Count open PRs on night-watch/ or feat/ branches using gh CLI
|
|
224
|
+
*/
|
|
225
|
+
export function countOpenPRs(projectDir, branchPatterns) {
|
|
226
|
+
try {
|
|
227
|
+
execSync("git rev-parse --git-dir", {
|
|
228
|
+
cwd: projectDir,
|
|
229
|
+
encoding: "utf-8",
|
|
230
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
231
|
+
});
|
|
232
|
+
try {
|
|
233
|
+
execSync("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
const output = execSync("gh pr list --state open --json headRefName,number", {
|
|
239
|
+
cwd: projectDir,
|
|
240
|
+
encoding: "utf-8",
|
|
241
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
242
|
+
});
|
|
243
|
+
const prs = JSON.parse(output);
|
|
244
|
+
const matchingPRs = prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern)));
|
|
245
|
+
return matchingPRs.length;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Derive CI status from gh statusCheckRollup data
|
|
253
|
+
*/
|
|
254
|
+
function deriveCiStatus(checks) {
|
|
255
|
+
if (!checks || checks.length === 0)
|
|
256
|
+
return "unknown";
|
|
257
|
+
const hasFailure = checks.some((c) => c.conclusion === "FAILURE" || c.conclusion === "ERROR" || c.conclusion === "CANCELLED");
|
|
258
|
+
if (hasFailure)
|
|
259
|
+
return "fail";
|
|
260
|
+
const allComplete = checks.every((c) => c.state === "COMPLETED");
|
|
261
|
+
if (allComplete)
|
|
262
|
+
return "pass";
|
|
263
|
+
return "pending";
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Derive review score from gh reviewDecision field
|
|
267
|
+
* Maps GitHub review decisions to a numeric score (0-100)
|
|
268
|
+
*/
|
|
269
|
+
function deriveReviewScore(reviewDecision) {
|
|
270
|
+
if (!reviewDecision)
|
|
271
|
+
return null;
|
|
272
|
+
switch (reviewDecision) {
|
|
273
|
+
case "APPROVED":
|
|
274
|
+
return 100;
|
|
275
|
+
case "CHANGES_REQUESTED":
|
|
276
|
+
return 0;
|
|
277
|
+
case "REVIEW_REQUIRED":
|
|
278
|
+
return null;
|
|
279
|
+
default:
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Collect open PR info using gh CLI
|
|
285
|
+
*/
|
|
286
|
+
export function collectPrInfo(projectDir, branchPatterns) {
|
|
287
|
+
try {
|
|
288
|
+
execSync("git rev-parse --git-dir", {
|
|
289
|
+
cwd: projectDir,
|
|
290
|
+
encoding: "utf-8",
|
|
291
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
292
|
+
});
|
|
293
|
+
try {
|
|
294
|
+
execSync("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
const output = execSync("gh pr list --state open --json headRefName,number,title,statusCheckRollup,reviewDecision", {
|
|
300
|
+
cwd: projectDir,
|
|
301
|
+
encoding: "utf-8",
|
|
302
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
303
|
+
});
|
|
304
|
+
const prs = JSON.parse(output);
|
|
305
|
+
return prs
|
|
306
|
+
.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern)))
|
|
307
|
+
.map((pr) => ({
|
|
308
|
+
number: pr.number,
|
|
309
|
+
title: pr.title,
|
|
310
|
+
branch: pr.headRefName,
|
|
311
|
+
ciStatus: deriveCiStatus(pr.statusCheckRollup),
|
|
312
|
+
reviewScore: deriveReviewScore(pr.reviewDecision),
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get last N lines from a log file
|
|
321
|
+
*/
|
|
322
|
+
export function getLastLogLines(logPath, lines) {
|
|
323
|
+
if (!fs.existsSync(logPath)) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const content = fs.readFileSync(logPath, "utf-8");
|
|
328
|
+
const allLines = content.trim().split("\n");
|
|
329
|
+
return allLines.slice(-lines);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get log file info
|
|
337
|
+
*/
|
|
338
|
+
export function getLogInfo(logPath, lastLines = 5) {
|
|
339
|
+
const exists = fs.existsSync(logPath);
|
|
340
|
+
return {
|
|
341
|
+
path: logPath,
|
|
342
|
+
lastLines: exists ? getLastLogLines(logPath, lastLines) : [],
|
|
343
|
+
exists,
|
|
344
|
+
size: exists ? fs.statSync(logPath).size : 0,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Collect log info as ILogInfo items
|
|
349
|
+
*/
|
|
350
|
+
export function collectLogInfo(projectDir) {
|
|
351
|
+
const logNames = ["executor", "reviewer"];
|
|
352
|
+
return logNames.map((name) => {
|
|
353
|
+
const logPath = path.join(projectDir, LOG_DIR, `${name}.log`);
|
|
354
|
+
const exists = fs.existsSync(logPath);
|
|
355
|
+
return {
|
|
356
|
+
name,
|
|
357
|
+
path: logPath,
|
|
358
|
+
exists,
|
|
359
|
+
size: exists ? fs.statSync(logPath).size : 0,
|
|
360
|
+
lastLines: exists ? getLastLogLines(logPath, 5) : [],
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Get crontab information for a project
|
|
366
|
+
*/
|
|
367
|
+
export function getCrontabInfo(projectName, projectDir) {
|
|
368
|
+
const marker = generateMarker(projectName);
|
|
369
|
+
const crontabEntries = Array.from(new Set([...getEntries(marker), ...getProjectEntries(projectDir)]));
|
|
370
|
+
return {
|
|
371
|
+
installed: crontabEntries.length > 0,
|
|
372
|
+
entries: crontabEntries,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Fetch a complete status snapshot for the given project
|
|
377
|
+
*/
|
|
378
|
+
export function fetchStatusSnapshot(projectDir, config) {
|
|
379
|
+
const projectName = getProjectName(projectDir);
|
|
380
|
+
const lockProjectName = path.basename(projectDir);
|
|
381
|
+
const executorLock = checkLockFile(`${LOCK_FILE_PREFIX}${lockProjectName}.lock`);
|
|
382
|
+
const reviewerLock = checkLockFile(`${LOCK_FILE_PREFIX}pr-reviewer-${lockProjectName}.lock`);
|
|
383
|
+
const processes = [
|
|
384
|
+
{ name: "executor", running: executorLock.running, pid: executorLock.pid },
|
|
385
|
+
{ name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
|
|
386
|
+
];
|
|
387
|
+
const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
|
|
388
|
+
const prs = collectPrInfo(projectDir, config.branchPatterns);
|
|
389
|
+
const logs = collectLogInfo(projectDir);
|
|
390
|
+
const crontab = getCrontabInfo(projectName, projectDir);
|
|
391
|
+
return {
|
|
392
|
+
projectName,
|
|
393
|
+
projectDir,
|
|
394
|
+
config,
|
|
395
|
+
prds,
|
|
396
|
+
processes,
|
|
397
|
+
prs,
|
|
398
|
+
logs,
|
|
399
|
+
crontab,
|
|
400
|
+
timestamp: new Date(),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
//# sourceMappingURL=status-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-data.js","sourceRoot":"","sources":["../../src/utils/status-data.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jonit-dev/night-watch-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Autonomous PRD execution using AI Provider CLIs + cron",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
"test": "vitest run",
|
|
19
19
|
"dev": "tsx src/cli.ts",
|
|
20
20
|
"prepublishOnly": "npm run build && npm test",
|
|
21
|
-
"publish:npm": "npm publish --access public"
|
|
21
|
+
"publish:npm": "npm publish --access public",
|
|
22
|
+
"lint": "eslint src/",
|
|
23
|
+
"verify": "tsc --noEmit && eslint src/",
|
|
24
|
+
"local": "tsc && yarn link"
|
|
22
25
|
},
|
|
23
26
|
"files": [
|
|
24
27
|
"dist/",
|
|
@@ -40,22 +43,27 @@
|
|
|
40
43
|
"license": "MIT",
|
|
41
44
|
"repository": {
|
|
42
45
|
"type": "git",
|
|
43
|
-
"url": "https://github.com/
|
|
46
|
+
"url": "https://github.com/jonit-dev/night-watch-cli.git"
|
|
44
47
|
},
|
|
45
|
-
"homepage": "https://github.com/
|
|
48
|
+
"homepage": "https://github.com/jonit-dev/night-watch-cli#readme",
|
|
46
49
|
"bugs": {
|
|
47
|
-
"url": "https://github.com/
|
|
50
|
+
"url": "https://github.com/jonit-dev/night-watch-cli/issues"
|
|
48
51
|
},
|
|
49
52
|
"dependencies": {
|
|
53
|
+
"blessed": "^0.1.81",
|
|
50
54
|
"chalk": "^5.6.2",
|
|
51
55
|
"cli-table3": "^0.6.5",
|
|
52
56
|
"commander": "^12.0.0",
|
|
53
57
|
"ora": "^9.3.0"
|
|
54
58
|
},
|
|
55
59
|
"devDependencies": {
|
|
60
|
+
"@eslint/js": "^10.0.1",
|
|
61
|
+
"@types/blessed": "^0.1.27",
|
|
56
62
|
"@types/node": "^20.11.0",
|
|
63
|
+
"eslint": "^10.0.0",
|
|
57
64
|
"tsx": "^4.7.0",
|
|
58
65
|
"typescript": "^5.3.0",
|
|
66
|
+
"typescript-eslint": "^8.56.0",
|
|
59
67
|
"vitest": "^1.2.0"
|
|
60
68
|
},
|
|
61
69
|
"engines": {
|
|
@@ -55,13 +55,19 @@ fi
|
|
|
55
55
|
|
|
56
56
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
57
57
|
|
|
58
|
-
ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}")
|
|
58
|
+
ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}" "${MAX_RUNTIME}")
|
|
59
59
|
|
|
60
60
|
if [ -z "${ELIGIBLE_PRD}" ]; then
|
|
61
61
|
log "SKIP: No eligible PRDs (all done, in-progress, or blocked)"
|
|
62
62
|
exit 0
|
|
63
63
|
fi
|
|
64
64
|
|
|
65
|
+
# Claim the PRD to prevent other runs from selecting it
|
|
66
|
+
claim_prd "${PRD_DIR}" "${ELIGIBLE_PRD}"
|
|
67
|
+
|
|
68
|
+
# Update EXIT trap to also release claim
|
|
69
|
+
trap "rm -f '${LOCK_FILE}'; release_claim '${PRD_DIR}' '${ELIGIBLE_PRD}'" EXIT
|
|
70
|
+
|
|
65
71
|
PRD_NAME="${ELIGIBLE_PRD%.md}"
|
|
66
72
|
BRANCH_NAME="night-watch/${PRD_NAME}"
|
|
67
73
|
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
@@ -146,6 +152,7 @@ esac
|
|
|
146
152
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
147
153
|
PR_EXISTS=$(gh pr list --state open --json headRefName --jq '.[].headRefName' 2>/dev/null | grep -cF "${BRANCH_NAME}" || echo "0")
|
|
148
154
|
if [ "${PR_EXISTS}" -gt 0 ]; then
|
|
155
|
+
release_claim "${PRD_DIR}" "${ELIGIBLE_PRD}"
|
|
149
156
|
mark_prd_done "${PRD_DIR}" "${ELIGIBLE_PRD}"
|
|
150
157
|
git -C "${PROJECT_DIR}" add -A docs/PRDs/night-watch/
|
|
151
158
|
git -C "${PROJECT_DIR}" commit -m "chore: mark ${ELIGIBLE_PRD} as done (PR opened on ${BRANCH_NAME})
|
|
@@ -118,10 +118,55 @@ detect_default_branch() {
|
|
|
118
118
|
echo "main"
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
# ── Claim management ─────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
claim_prd() {
|
|
124
|
+
local prd_dir="${1:?prd_dir required}"
|
|
125
|
+
local prd_file="${2:?prd_file required}"
|
|
126
|
+
local claim_file="${prd_dir}/${prd_file}.claim"
|
|
127
|
+
|
|
128
|
+
printf '{"timestamp":%d,"hostname":"%s","pid":%d}\n' \
|
|
129
|
+
"$(date +%s)" "$(hostname)" "$$" > "${claim_file}"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
release_claim() {
|
|
133
|
+
local prd_dir="${1:?prd_dir required}"
|
|
134
|
+
local prd_file="${2:?prd_file required}"
|
|
135
|
+
local claim_file="${prd_dir}/${prd_file}.claim"
|
|
136
|
+
|
|
137
|
+
rm -f "${claim_file}"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
is_claimed() {
|
|
141
|
+
local prd_dir="${1:?prd_dir required}"
|
|
142
|
+
local prd_file="${2:?prd_file required}"
|
|
143
|
+
local max_runtime="${3:-7200}"
|
|
144
|
+
local claim_file="${prd_dir}/${prd_file}.claim"
|
|
145
|
+
|
|
146
|
+
if [ ! -f "${claim_file}" ]; then
|
|
147
|
+
return 1
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
local claim_ts
|
|
151
|
+
claim_ts=$(grep -o '"timestamp":[0-9]*' "${claim_file}" 2>/dev/null | grep -o '[0-9]*' || echo "0")
|
|
152
|
+
local now
|
|
153
|
+
now=$(date +%s)
|
|
154
|
+
local age=$(( now - claim_ts ))
|
|
155
|
+
|
|
156
|
+
if [ "${age}" -lt "${max_runtime}" ]; then
|
|
157
|
+
return 0 # actively claimed
|
|
158
|
+
else
|
|
159
|
+
# Stale claim — remove it
|
|
160
|
+
rm -f "${claim_file}"
|
|
161
|
+
return 1
|
|
162
|
+
fi
|
|
163
|
+
}
|
|
164
|
+
|
|
121
165
|
# ── Find next eligible PRD ───────────────────────────────────────────────────
|
|
122
166
|
|
|
123
167
|
find_eligible_prd() {
|
|
124
168
|
local prd_dir="${1:?prd_dir required}"
|
|
169
|
+
local max_runtime="${2:-7200}"
|
|
125
170
|
local done_dir="${prd_dir}/done"
|
|
126
171
|
|
|
127
172
|
local prd_files
|
|
@@ -139,6 +184,12 @@ find_eligible_prd() {
|
|
|
139
184
|
prd_file=$(basename "${prd_path}")
|
|
140
185
|
local prd_name="${prd_file%.md}"
|
|
141
186
|
|
|
187
|
+
# Skip if claimed by another process
|
|
188
|
+
if is_claimed "${prd_dir}" "${prd_file}" "${max_runtime}"; then
|
|
189
|
+
log "SKIP-PRD: ${prd_file} — claimed by another process"
|
|
190
|
+
continue
|
|
191
|
+
fi
|
|
192
|
+
|
|
142
193
|
# Skip if a PR already exists for this PRD
|
|
143
194
|
if echo "${open_branches}" | grep -qF "${prd_name}"; then
|
|
144
195
|
log "SKIP-PRD: ${prd_file} — open PR already exists"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Tests for night-watch-helpers.sh claim functions
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
# Source the helpers
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME}")" && pwd)"
|
|
8
|
+
|
|
9
|
+
# Set required globals
|
|
10
|
+
export LOG_FILE="/tmp/night-watch-test-$$.log"
|
|
11
|
+
|
|
12
|
+
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
13
|
+
|
|
14
|
+
# Create temp PRD directory
|
|
15
|
+
TEST_PRD_DIR=$(mktemp -d)
|
|
16
|
+
echo "# Test PRD" > "${TEST_PRD_DIR}/01-test-prd.md"
|
|
17
|
+
echo "# Test PRD 2" > "${TEST_PRD_DIR}/02-test-prd.md"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
teardown() {
|
|
21
|
+
rm -rf "${TEST_PRD_DIR}"
|
|
22
|
+
rm -f "${LOG_FILE}"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@test "claim_prd creates .claim file with JSON" {
|
|
26
|
+
claim_prd "${TEST_PRD_DIR}" "01-test-prd.md"
|
|
27
|
+
|
|
28
|
+
[ -f "${TEST_PRD_DIR}/01-test-prd.md.claim" ]
|
|
29
|
+
|
|
30
|
+
local content
|
|
31
|
+
content=$(cat "${TEST_PRD_DIR}/01-test-prd.md.claim")
|
|
32
|
+
|
|
33
|
+
# Check JSON contains expected fields
|
|
34
|
+
echo "${content}" | grep -q '"timestamp":'
|
|
35
|
+
echo "${content}" | grep -q '"hostname":'
|
|
36
|
+
echo "${content}" | grep -q '"pid":'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@test "is_claimed returns 0 for active claim" {
|
|
40
|
+
claim_prd "${TEST_PRD_DIR}" "01-test-prd.md"
|
|
41
|
+
|
|
42
|
+
run is_claimed "${TEST_PRD_DIR}" "01-test-prd.md" 7200
|
|
43
|
+
[ "$status" -eq 0 ]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "is_claimed returns 1 for stale claim" {
|
|
47
|
+
# Write a claim with an old timestamp (1 second)
|
|
48
|
+
printf '{"timestamp":1000000000,"hostname":"test","pid":1}\n' \
|
|
49
|
+
> "${TEST_PRD_DIR}/01-test-prd.md.claim"
|
|
50
|
+
|
|
51
|
+
run is_claimed "${TEST_PRD_DIR}" "01-test-prd.md" 7200
|
|
52
|
+
[ "$status" -eq 1 ]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@test "is_claimed returns 1 for no claim" {
|
|
56
|
+
run is_claimed "${TEST_PRD_DIR}" "01-test-prd.md" 7200
|
|
57
|
+
[ "$status" -eq 1 ]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@test "release_claim removes .claim file" {
|
|
61
|
+
claim_prd "${TEST_PRD_DIR}" "01-test-prd.md"
|
|
62
|
+
[ -f "${TEST_PRD_DIR}/01-test-prd.md.claim" ]
|
|
63
|
+
|
|
64
|
+
release_claim "${TEST_PRD_DIR}" "01-test-prd.md"
|
|
65
|
+
[ ! -f "${TEST_PRD_DIR}/01-test-prd.md.claim" ]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@test "find_eligible_prd skips claimed PRD" {
|
|
69
|
+
# Claim the first PRD
|
|
70
|
+
claim_prd "${TEST_PRD_DIR}" "01-test-prd.md"
|
|
71
|
+
|
|
72
|
+
# find_eligible_prd should skip 01 and return 02
|
|
73
|
+
local result
|
|
74
|
+
result=$(find_eligible_prd "${TEST_PRD_DIR}" 7200)
|
|
75
|
+
|
|
76
|
+
[ "${result}" = "02-test-prd.md" ]
|
|
77
|
+
}
|
package/templates/prd.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# PRD: {{TITLE}}
|
|
2
|
+
|
|
3
|
+
{{DEPENDS_ON}}
|
|
4
|
+
|
|
5
|
+
**Complexity: {{COMPLEXITY_SCORE}} → {{COMPLEXITY_LEVEL}} mode**
|
|
6
|
+
{{COMPLEXITY_BREAKDOWN}}
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
<!-- What problem does this solve? Describe in 1-2 sentences. -->
|
|
13
|
+
|
|
14
|
+
## Solution
|
|
15
|
+
|
|
16
|
+
<!-- How will you solve it? 3-5 bullets explaining the approach. -->
|
|
17
|
+
|
|
18
|
+
## Phases
|
|
19
|
+
|
|
20
|
+
{{PHASES}}
|
|
21
|
+
|
|
22
|
+
## Acceptance Criteria
|
|
23
|
+
|
|
24
|
+
- [ ] All phases complete
|
|
25
|
+
- [ ] All tests pass
|
|
26
|
+
- [ ] Feature is reachable (not orphaned code)
|