@jonit-dev/night-watch-cli 1.8.10-beta.3 → 1.8.10-beta.5
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.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +957 -1906
- package/dist/cli.js.map +1 -0
- package/dist/commands/analytics.d.ts +14 -0
- package/dist/commands/analytics.d.ts.map +1 -0
- package/dist/commands/analytics.js +69 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/commands/audit.d.ts +19 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +144 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/board.d.ts +9 -0
- package/dist/commands/board.d.ts.map +1 -0
- package/dist/commands/board.js +702 -0
- package/dist/commands/board.js.map +1 -0
- package/dist/commands/cancel.d.ts +46 -0
- package/dist/commands/cancel.d.ts.map +1 -0
- package/dist/commands/cancel.js +239 -0
- package/dist/commands/cancel.js.map +1 -0
- package/dist/commands/cron.d.ts +8 -0
- package/dist/commands/cron.d.ts.map +1 -0
- package/dist/commands/cron.js +134 -0
- package/dist/commands/cron.js.map +1 -0
- package/dist/commands/dashboard/tab-actions.d.ts +10 -0
- package/dist/commands/dashboard/tab-actions.d.ts.map +1 -0
- package/dist/commands/dashboard/tab-actions.js +247 -0
- package/dist/commands/dashboard/tab-actions.js.map +1 -0
- package/dist/commands/dashboard/tab-config.d.ts +21 -0
- package/dist/commands/dashboard/tab-config.d.ts.map +1 -0
- package/dist/commands/dashboard/tab-config.js +873 -0
- package/dist/commands/dashboard/tab-config.js.map +1 -0
- package/dist/commands/dashboard/tab-logs.d.ts +10 -0
- package/dist/commands/dashboard/tab-logs.d.ts.map +1 -0
- package/dist/commands/dashboard/tab-logs.js +202 -0
- package/dist/commands/dashboard/tab-logs.js.map +1 -0
- package/dist/commands/dashboard/tab-schedules.d.ts +21 -0
- package/dist/commands/dashboard/tab-schedules.d.ts.map +1 -0
- package/dist/commands/dashboard/tab-schedules.js +320 -0
- package/dist/commands/dashboard/tab-schedules.js.map +1 -0
- package/dist/commands/dashboard/tab-status.d.ts +32 -0
- package/dist/commands/dashboard/tab-status.d.ts.map +1 -0
- package/dist/commands/dashboard/tab-status.js +424 -0
- package/dist/commands/dashboard/tab-status.js.map +1 -0
- package/dist/commands/dashboard/types.d.ts +42 -0
- package/dist/commands/dashboard/types.d.ts.map +1 -0
- package/dist/commands/dashboard/types.js +5 -0
- package/dist/commands/dashboard/types.js.map +1 -0
- package/dist/commands/dashboard.d.ts +11 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +242 -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 +195 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/history.d.ts +7 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +49 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/init.d.ts +45 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +777 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install.d.ts +65 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +405 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/logs.d.ts +15 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +155 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/merge.d.ts +26 -0
- package/dist/commands/merge.d.ts.map +1 -0
- package/dist/commands/merge.js +159 -0
- package/dist/commands/merge.js.map +1 -0
- package/dist/commands/notify.d.ts +7 -0
- package/dist/commands/notify.d.ts.map +1 -0
- package/dist/commands/notify.js +43 -0
- package/dist/commands/notify.js.map +1 -0
- package/dist/commands/plan.d.ts +19 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +88 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/prd-state.d.ts +12 -0
- package/dist/commands/prd-state.d.ts.map +1 -0
- package/dist/commands/prd-state.js +47 -0
- package/dist/commands/prd-state.js.map +1 -0
- package/dist/commands/prd.d.ts +18 -0
- package/dist/commands/prd.d.ts.map +1 -0
- package/dist/commands/prd.js +363 -0
- package/dist/commands/prd.js.map +1 -0
- package/dist/commands/prds.d.ts +13 -0
- package/dist/commands/prds.d.ts.map +1 -0
- package/dist/commands/prds.js +194 -0
- package/dist/commands/prds.js.map +1 -0
- package/dist/commands/prs.d.ts +14 -0
- package/dist/commands/prs.d.ts.map +1 -0
- package/dist/commands/prs.js +104 -0
- package/dist/commands/prs.js.map +1 -0
- package/dist/commands/qa.d.ts +34 -0
- package/dist/commands/qa.d.ts.map +1 -0
- package/dist/commands/qa.js +214 -0
- package/dist/commands/qa.js.map +1 -0
- package/dist/commands/queue.d.ts +8 -0
- package/dist/commands/queue.d.ts.map +1 -0
- package/dist/commands/queue.js +378 -0
- package/dist/commands/queue.js.map +1 -0
- package/dist/commands/resolve.d.ts +26 -0
- package/dist/commands/resolve.d.ts.map +1 -0
- package/dist/commands/resolve.js +186 -0
- package/dist/commands/resolve.js.map +1 -0
- package/dist/commands/retry.d.ts +9 -0
- package/dist/commands/retry.d.ts.map +1 -0
- package/dist/commands/retry.js +71 -0
- package/dist/commands/retry.js.map +1 -0
- package/dist/commands/review.d.ts +82 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +479 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/run.d.ts +73 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +509 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/serve.d.ts +19 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +142 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/shared/env-builder.d.ts +49 -0
- package/dist/commands/shared/env-builder.d.ts.map +1 -0
- package/dist/commands/shared/env-builder.js +150 -0
- package/dist/commands/shared/env-builder.js.map +1 -0
- package/dist/commands/slice.d.ts +35 -0
- package/dist/commands/slice.d.ts.map +1 -0
- package/dist/commands/slice.js +316 -0
- package/dist/commands/slice.js.map +1 -0
- package/dist/commands/state.d.ts +8 -0
- package/dist/commands/state.d.ts.map +1 -0
- package/dist/commands/state.js +54 -0
- package/dist/commands/state.js.map +1 -0
- package/dist/commands/status.d.ts +14 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +297 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/summary.d.ts +14 -0
- package/dist/commands/summary.d.ts.map +1 -0
- package/dist/commands/summary.js +193 -0
- package/dist/commands/summary.js.map +1 -0
- package/dist/commands/uninstall.d.ts +25 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +134 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/update.d.ts +22 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +90 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/scripts/night-watch-cron.sh +9 -0
- package/dist/scripts/night-watch-helpers.sh +65 -7
- package/dist/web/assets/index-CgXj_KM1.css +1 -0
- package/dist/web/assets/index-Dr4zlyJf.js +406 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BUgI2S1s.js +0 -406
- package/dist/web/assets/index-CkdLFBd7.js +0 -406
- package/dist/web/assets/index-RMfswANB.css +0 -1
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import * as readline from 'readline';
|
|
8
|
+
import { BUILT_IN_PRESET_IDS, CONFIG_FILE_NAME, DEFAULT_PRD_DIR, LOG_DIR, checkGhCli, checkGitRepo, checkNodeVersion, checkProviderCli, createBoardProvider, createTable, detectProviders, getDefaultConfig, getProjectName, header, info, label, loadConfig, step, success, error as uiError, warn, } from '@night-watch/core';
|
|
9
|
+
// Get templates directory path.
|
|
10
|
+
// Walk up from __dirname to find the package root (the directory that contains
|
|
11
|
+
// a package.json AND a templates/ folder). This works whether the code runs
|
|
12
|
+
// from the TypeScript source tree (src/commands/), the compiled dist tree
|
|
13
|
+
// (dist/commands/), or as a single esbuild bundle (dist/).
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
function findTemplatesDir(startDir) {
|
|
17
|
+
let d = startDir;
|
|
18
|
+
for (let i = 0; i < 8; i++) {
|
|
19
|
+
const candidate = join(d, 'templates');
|
|
20
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
d = dirname(d);
|
|
24
|
+
}
|
|
25
|
+
return join(startDir, 'templates'); // fallback
|
|
26
|
+
}
|
|
27
|
+
const TEMPLATES_DIR = findTemplatesDir(__dirname);
|
|
28
|
+
const NW_SKILLS = [
|
|
29
|
+
'nw-create-prd',
|
|
30
|
+
'nw-add-issue',
|
|
31
|
+
'nw-run',
|
|
32
|
+
'nw-slice',
|
|
33
|
+
'nw-board-sync',
|
|
34
|
+
'nw-review',
|
|
35
|
+
];
|
|
36
|
+
function hasPlaywrightDependency(cwd) {
|
|
37
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
38
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
43
|
+
return Boolean(packageJson.dependencies?.['@playwright/test'] ||
|
|
44
|
+
packageJson.dependencies?.playwright ||
|
|
45
|
+
packageJson.devDependencies?.['@playwright/test'] ||
|
|
46
|
+
packageJson.devDependencies?.playwright);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function detectPlaywright(cwd) {
|
|
53
|
+
if (hasPlaywrightDependency(cwd)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (fs.existsSync(path.join(cwd, 'node_modules', '.bin', 'playwright'))) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
execSync('playwright --version', {
|
|
61
|
+
cwd,
|
|
62
|
+
encoding: 'utf-8',
|
|
63
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
|
+
timeout: 1500,
|
|
65
|
+
});
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function resolvePlaywrightInstallCommand(cwd) {
|
|
73
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) {
|
|
74
|
+
return 'pnpm add -D @playwright/test';
|
|
75
|
+
}
|
|
76
|
+
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) {
|
|
77
|
+
return 'yarn add -D @playwright/test';
|
|
78
|
+
}
|
|
79
|
+
return 'npm install -D @playwright/test';
|
|
80
|
+
}
|
|
81
|
+
function promptYesNo(question, defaultNo = true) {
|
|
82
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
83
|
+
return Promise.resolve(false);
|
|
84
|
+
}
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const rl = readline.createInterface({
|
|
87
|
+
input: process.stdin,
|
|
88
|
+
output: process.stdout,
|
|
89
|
+
});
|
|
90
|
+
const suffix = defaultNo ? ' [y/N]: ' : ' [Y/n]: ';
|
|
91
|
+
rl.question(`${question}${suffix}`, (answer) => {
|
|
92
|
+
rl.close();
|
|
93
|
+
const normalized = answer.trim().toLowerCase();
|
|
94
|
+
if (normalized === '') {
|
|
95
|
+
resolve(!defaultNo);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function isInteractiveInitSession() {
|
|
103
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
104
|
+
}
|
|
105
|
+
export function chooseProviderForNonInteractive(providers) {
|
|
106
|
+
if (providers.includes('claude')) {
|
|
107
|
+
return 'claude';
|
|
108
|
+
}
|
|
109
|
+
return providers[0];
|
|
110
|
+
}
|
|
111
|
+
export function getGitHubRemoteStatus(cwd) {
|
|
112
|
+
try {
|
|
113
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
114
|
+
cwd,
|
|
115
|
+
encoding: 'utf-8',
|
|
116
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
117
|
+
}).trim();
|
|
118
|
+
return {
|
|
119
|
+
hasGitHubRemote: remoteUrl.includes('github.com'),
|
|
120
|
+
remoteUrl: remoteUrl || null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return {
|
|
125
|
+
hasGitHubRemote: false,
|
|
126
|
+
remoteUrl: null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function installPlaywrightForQa(cwd) {
|
|
131
|
+
try {
|
|
132
|
+
const installCmd = resolvePlaywrightInstallCommand(cwd);
|
|
133
|
+
execSync(installCmd, {
|
|
134
|
+
cwd,
|
|
135
|
+
encoding: 'utf-8',
|
|
136
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
137
|
+
});
|
|
138
|
+
execSync('npx playwright install chromium', {
|
|
139
|
+
cwd,
|
|
140
|
+
encoding: 'utf-8',
|
|
141
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
142
|
+
});
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get the default branch name for the repository
|
|
151
|
+
*/
|
|
152
|
+
export function getDefaultBranch(cwd) {
|
|
153
|
+
const getRefTimestamp = (ref) => {
|
|
154
|
+
try {
|
|
155
|
+
const timestamp = execSync(`git log -1 --format=%ct ${ref}`, {
|
|
156
|
+
encoding: 'utf-8',
|
|
157
|
+
cwd,
|
|
158
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
159
|
+
}).trim();
|
|
160
|
+
const parsed = parseInt(timestamp, 10);
|
|
161
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const getBranchLatestTimestamp = (branch) => {
|
|
168
|
+
const refs = [`refs/remotes/origin/${branch}`, `refs/heads/${branch}`];
|
|
169
|
+
let latest = null;
|
|
170
|
+
for (const ref of refs) {
|
|
171
|
+
const timestamp = getRefTimestamp(ref);
|
|
172
|
+
if (timestamp !== null && (latest === null || timestamp > latest)) {
|
|
173
|
+
latest = timestamp;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return latest;
|
|
177
|
+
};
|
|
178
|
+
try {
|
|
179
|
+
// If both main and master exist, use whichever has the newest tip commit
|
|
180
|
+
const mainTimestamp = getBranchLatestTimestamp('main');
|
|
181
|
+
const masterTimestamp = getBranchLatestTimestamp('master');
|
|
182
|
+
if (mainTimestamp !== null && masterTimestamp !== null) {
|
|
183
|
+
return mainTimestamp >= masterTimestamp ? 'main' : 'master';
|
|
184
|
+
}
|
|
185
|
+
if (mainTimestamp !== null) {
|
|
186
|
+
return 'main';
|
|
187
|
+
}
|
|
188
|
+
if (masterTimestamp !== null) {
|
|
189
|
+
return 'master';
|
|
190
|
+
}
|
|
191
|
+
// Fallback to origin/HEAD when neither main nor master exists
|
|
192
|
+
const remoteRef = execSync('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo ""', {
|
|
193
|
+
encoding: 'utf-8',
|
|
194
|
+
cwd,
|
|
195
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
196
|
+
}).trim();
|
|
197
|
+
if (remoteRef) {
|
|
198
|
+
// Extract branch name from refs/remotes/origin/HEAD -> refs/remotes/origin/main
|
|
199
|
+
const match = remoteRef.match(/refs\/remotes\/origin\/(.+)/);
|
|
200
|
+
if (match) {
|
|
201
|
+
return match[1];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Default to main
|
|
205
|
+
return 'main';
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return 'main';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Prompt user to select a provider from available options
|
|
213
|
+
*/
|
|
214
|
+
function promptProviderSelection(providers) {
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
const rl = readline.createInterface({
|
|
217
|
+
input: process.stdin,
|
|
218
|
+
output: process.stdout,
|
|
219
|
+
});
|
|
220
|
+
console.log('\nMultiple AI providers detected:');
|
|
221
|
+
providers.forEach((p, i) => {
|
|
222
|
+
console.log(` ${i + 1}. ${p}`);
|
|
223
|
+
});
|
|
224
|
+
rl.question('\nSelect a provider (enter number): ', (answer) => {
|
|
225
|
+
rl.close();
|
|
226
|
+
const selection = parseInt(answer.trim(), 10);
|
|
227
|
+
if (isNaN(selection) || selection < 1 || selection > providers.length) {
|
|
228
|
+
reject(new Error('Invalid selection. Please run init again and select a valid number.'));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
resolve(providers[selection - 1]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Create directory if it doesn't exist
|
|
237
|
+
*/
|
|
238
|
+
function ensureDir(dirPath) {
|
|
239
|
+
if (!fs.existsSync(dirPath)) {
|
|
240
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export function buildInitConfig(params) {
|
|
244
|
+
const defaults = getDefaultConfig();
|
|
245
|
+
return {
|
|
246
|
+
$schema: 'https://json-schema.org/schema',
|
|
247
|
+
projectName: params.projectName,
|
|
248
|
+
defaultBranch: params.defaultBranch,
|
|
249
|
+
prdDir: params.prdDir,
|
|
250
|
+
maxRuntime: defaults.maxRuntime,
|
|
251
|
+
reviewerMaxRuntime: defaults.reviewerMaxRuntime,
|
|
252
|
+
branchPrefix: defaults.branchPrefix,
|
|
253
|
+
branchPatterns: [...defaults.branchPatterns],
|
|
254
|
+
minReviewScore: defaults.minReviewScore,
|
|
255
|
+
maxLogSize: defaults.maxLogSize,
|
|
256
|
+
cronSchedule: defaults.cronSchedule,
|
|
257
|
+
reviewerSchedule: defaults.reviewerSchedule,
|
|
258
|
+
scheduleBundleId: defaults.scheduleBundleId ?? 'always-on',
|
|
259
|
+
cronScheduleOffset: defaults.cronScheduleOffset,
|
|
260
|
+
schedulingPriority: defaults.schedulingPriority,
|
|
261
|
+
maxRetries: defaults.maxRetries,
|
|
262
|
+
reviewerMaxRetries: defaults.reviewerMaxRetries,
|
|
263
|
+
reviewerMaxPrsPerRun: defaults.reviewerMaxPrsPerRun,
|
|
264
|
+
reviewerRetryDelay: defaults.reviewerRetryDelay,
|
|
265
|
+
provider: params.provider,
|
|
266
|
+
providerLabel: '',
|
|
267
|
+
executorEnabled: defaults.executorEnabled ?? true,
|
|
268
|
+
reviewerEnabled: params.reviewerEnabled,
|
|
269
|
+
providerEnv: { ...defaults.providerEnv },
|
|
270
|
+
notifications: {
|
|
271
|
+
...defaults.notifications,
|
|
272
|
+
webhooks: [...(defaults.notifications?.webhooks ?? [])],
|
|
273
|
+
},
|
|
274
|
+
prdPriority: [...defaults.prdPriority],
|
|
275
|
+
roadmapScanner: { ...defaults.roadmapScanner },
|
|
276
|
+
templatesDir: defaults.templatesDir,
|
|
277
|
+
boardProvider: { ...defaults.boardProvider },
|
|
278
|
+
autoMerge: defaults.autoMerge,
|
|
279
|
+
autoMergeMethod: defaults.autoMergeMethod,
|
|
280
|
+
fallbackOnRateLimit: defaults.fallbackOnRateLimit,
|
|
281
|
+
claudeModel: defaults.claudeModel,
|
|
282
|
+
qa: {
|
|
283
|
+
...defaults.qa,
|
|
284
|
+
branchPatterns: [...defaults.qa.branchPatterns],
|
|
285
|
+
},
|
|
286
|
+
audit: { ...defaults.audit },
|
|
287
|
+
analytics: { ...defaults.analytics },
|
|
288
|
+
merger: { ...defaults.merger },
|
|
289
|
+
prResolver: { ...defaults.prResolver },
|
|
290
|
+
jobProviders: { ...defaults.jobProviders },
|
|
291
|
+
queue: {
|
|
292
|
+
...defaults.queue,
|
|
293
|
+
priority: { ...defaults.queue.priority },
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Resolve a template path with per-file fallback.
|
|
299
|
+
* If customTemplatesDir is non-null and the file exists there, return custom path.
|
|
300
|
+
* Otherwise return the bundled template path.
|
|
301
|
+
*/
|
|
302
|
+
export function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
|
|
303
|
+
if (customTemplatesDir !== null) {
|
|
304
|
+
const customPath = join(customTemplatesDir, templateName);
|
|
305
|
+
if (fs.existsSync(customPath)) {
|
|
306
|
+
return { path: customPath, source: 'custom' };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { path: join(bundledTemplatesDir, templateName), source: 'bundled' };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Copy and process template file
|
|
313
|
+
*/
|
|
314
|
+
function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
|
|
315
|
+
// Skip if exists and not forcing
|
|
316
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
317
|
+
console.log(` Skipped (exists): ${targetPath}`);
|
|
318
|
+
return { created: false, source: source ?? 'bundled' };
|
|
319
|
+
}
|
|
320
|
+
const templatePath = sourcePath ?? join(TEMPLATES_DIR, templateName);
|
|
321
|
+
const resolvedSource = source ?? 'bundled';
|
|
322
|
+
let content = fs.readFileSync(templatePath, 'utf-8');
|
|
323
|
+
// Replace placeholders
|
|
324
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
325
|
+
content = content.replaceAll(key, value);
|
|
326
|
+
}
|
|
327
|
+
fs.writeFileSync(targetPath, content);
|
|
328
|
+
console.log(` Created: ${targetPath} (${resolvedSource})`);
|
|
329
|
+
return { created: true, source: resolvedSource };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Ensure Night Watch entries are in .gitignore
|
|
333
|
+
*/
|
|
334
|
+
function addToGitignore(cwd) {
|
|
335
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
336
|
+
const entries = [
|
|
337
|
+
{
|
|
338
|
+
pattern: '/logs/',
|
|
339
|
+
label: '/logs/',
|
|
340
|
+
check: (c) => c.includes('/logs/') || /^logs\//m.test(c),
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
pattern: CONFIG_FILE_NAME,
|
|
344
|
+
label: CONFIG_FILE_NAME,
|
|
345
|
+
check: (c) => c.includes(CONFIG_FILE_NAME),
|
|
346
|
+
},
|
|
347
|
+
{ pattern: '*.claim', label: '*.claim', check: (c) => c.includes('*.claim') },
|
|
348
|
+
];
|
|
349
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
350
|
+
const lines = ['# Night Watch', ...entries.map((e) => e.pattern), ''];
|
|
351
|
+
fs.writeFileSync(gitignorePath, lines.join('\n'));
|
|
352
|
+
console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
356
|
+
const missing = entries.filter((e) => !e.check(content));
|
|
357
|
+
if (missing.length === 0) {
|
|
358
|
+
console.log(` Skipped (exists): Night Watch entries in .gitignore`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const additions = missing.map((e) => e.pattern).join('\n');
|
|
362
|
+
const newContent = content.trimEnd() + '\n\n# Night Watch\n' + additions + '\n';
|
|
363
|
+
fs.writeFileSync(gitignorePath, newContent);
|
|
364
|
+
console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(', ')})`);
|
|
365
|
+
}
|
|
366
|
+
function installSkills(cwd, provider, force, templatesDir) {
|
|
367
|
+
const skillsTemplatesDir = path.join(templatesDir, 'skills');
|
|
368
|
+
if (!fs.existsSync(skillsTemplatesDir)) {
|
|
369
|
+
return { location: '', installed: 0, skipped: 0, type: 'none' };
|
|
370
|
+
}
|
|
371
|
+
const isClaudeProvider = provider === 'claude' || provider.startsWith('claude');
|
|
372
|
+
const isCodexProvider = provider === 'codex';
|
|
373
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
374
|
+
if (isClaudeProvider || fs.existsSync(claudeDir)) {
|
|
375
|
+
ensureDir(claudeDir);
|
|
376
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
377
|
+
ensureDir(skillsDir);
|
|
378
|
+
let installed = 0;
|
|
379
|
+
let skipped = 0;
|
|
380
|
+
for (const skillName of NW_SKILLS) {
|
|
381
|
+
const templateFile = path.join(skillsTemplatesDir, `${skillName}.md`);
|
|
382
|
+
if (!fs.existsSync(templateFile))
|
|
383
|
+
continue;
|
|
384
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
385
|
+
ensureDir(skillDir);
|
|
386
|
+
const target = path.join(skillDir, 'SKILL.md');
|
|
387
|
+
if (fs.existsSync(target) && !force) {
|
|
388
|
+
skipped++;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
fs.copyFileSync(templateFile, target);
|
|
392
|
+
installed++;
|
|
393
|
+
}
|
|
394
|
+
return { location: '.claude/skills/', installed, skipped, type: 'claude' };
|
|
395
|
+
}
|
|
396
|
+
if (isCodexProvider) {
|
|
397
|
+
const agentsFile = path.join(cwd, 'AGENTS.md');
|
|
398
|
+
const blockFile = path.join(skillsTemplatesDir, '_codex-block.md');
|
|
399
|
+
if (!fs.existsSync(blockFile)) {
|
|
400
|
+
return { location: '', installed: 0, skipped: 0, type: 'none' };
|
|
401
|
+
}
|
|
402
|
+
const block = fs.readFileSync(blockFile, 'utf-8');
|
|
403
|
+
const marker = '## Night Watch Skills';
|
|
404
|
+
if (!fs.existsSync(agentsFile)) {
|
|
405
|
+
fs.writeFileSync(agentsFile, block);
|
|
406
|
+
return { location: 'AGENTS.md', installed: NW_SKILLS.length, skipped: 0, type: 'codex' };
|
|
407
|
+
}
|
|
408
|
+
const existing = fs.readFileSync(agentsFile, 'utf-8');
|
|
409
|
+
if (existing.includes(marker)) {
|
|
410
|
+
if (!force) {
|
|
411
|
+
return { location: 'AGENTS.md', installed: 0, skipped: NW_SKILLS.length, type: 'codex' };
|
|
412
|
+
}
|
|
413
|
+
const withoutSection = existing.replace(/\n\n## Night Watch Skills[\s\S]*$/, '');
|
|
414
|
+
fs.writeFileSync(agentsFile, withoutSection + '\n\n' + block);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
fs.appendFileSync(agentsFile, '\n\n' + block);
|
|
418
|
+
}
|
|
419
|
+
return { location: 'AGENTS.md', installed: NW_SKILLS.length, skipped: 0, type: 'codex' };
|
|
420
|
+
}
|
|
421
|
+
return { location: '', installed: 0, skipped: 0, type: 'none' };
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Main init command implementation
|
|
425
|
+
*/
|
|
426
|
+
export function initCommand(program) {
|
|
427
|
+
program
|
|
428
|
+
.command('init')
|
|
429
|
+
.description('Initialize night-watch in the current project')
|
|
430
|
+
.option('-f, --force', 'Overwrite existing configuration')
|
|
431
|
+
.option('-d, --prd-dir <path>', 'Path to PRD directory')
|
|
432
|
+
.option('-p, --provider <name>', 'AI provider to use (claude or codex)')
|
|
433
|
+
.option('--no-reviewer', 'Disable reviewer cron job')
|
|
434
|
+
.action(async (options) => {
|
|
435
|
+
const cwd = process.cwd();
|
|
436
|
+
const force = options.force || false;
|
|
437
|
+
const prdDir = options.prdDir || DEFAULT_PRD_DIR;
|
|
438
|
+
const totalSteps = 14;
|
|
439
|
+
const interactive = isInteractiveInitSession();
|
|
440
|
+
console.log();
|
|
441
|
+
header('Night Watch CLI - Initializing');
|
|
442
|
+
// Step 1: Verify Node.js version
|
|
443
|
+
step(1, totalSteps, 'Checking Node.js version...');
|
|
444
|
+
const nodeCheck = checkNodeVersion(22);
|
|
445
|
+
if (!nodeCheck.passed) {
|
|
446
|
+
uiError(nodeCheck.message);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
success(nodeCheck.message);
|
|
450
|
+
// Step 2: Verify git repository
|
|
451
|
+
step(2, totalSteps, 'Checking git repository...');
|
|
452
|
+
const gitCheck = checkGitRepo(cwd);
|
|
453
|
+
if (!gitCheck.passed) {
|
|
454
|
+
uiError(gitCheck.message);
|
|
455
|
+
console.log('Please run this command from the root of a git repository.');
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
success(gitCheck.message);
|
|
459
|
+
// Step 3: Detect AI providers
|
|
460
|
+
step(3, totalSteps, 'Detecting AI providers...');
|
|
461
|
+
let selectedProvider;
|
|
462
|
+
if (options.provider) {
|
|
463
|
+
// Validate provider flag
|
|
464
|
+
if (!BUILT_IN_PRESET_IDS.includes(options.provider)) {
|
|
465
|
+
uiError(`Invalid provider "${options.provider}".`);
|
|
466
|
+
console.log(`Valid providers: ${BUILT_IN_PRESET_IDS.join(', ')}`);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
selectedProvider = options.provider;
|
|
470
|
+
const providerCheck = checkProviderCli(selectedProvider);
|
|
471
|
+
if (!providerCheck.passed) {
|
|
472
|
+
uiError(providerCheck.message);
|
|
473
|
+
console.log(`Install the ${selectedProvider} CLI or rerun with --provider ${detectProviders()[0] ?? 'claude'}.`);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
info(`Using provider from flag: ${selectedProvider}`);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
// Auto-detect providers
|
|
480
|
+
const detectedProviders = detectProviders();
|
|
481
|
+
if (detectedProviders.length === 0) {
|
|
482
|
+
uiError('No AI provider CLI found.');
|
|
483
|
+
console.log('\nPlease install one of the following:');
|
|
484
|
+
console.log(' - Claude CLI: https://docs.anthropic.com/en/docs/claude-cli');
|
|
485
|
+
console.log(' - Codex CLI: https://github.com/openai/codex');
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
else if (detectedProviders.length === 1) {
|
|
489
|
+
selectedProvider = detectedProviders[0];
|
|
490
|
+
info(`Auto-detected provider: ${selectedProvider}`);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
if (!interactive) {
|
|
494
|
+
selectedProvider = chooseProviderForNonInteractive(detectedProviders);
|
|
495
|
+
info(`Multiple providers detected in a non-interactive shell; defaulting to ${selectedProvider}. Use --provider to override.`);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
try {
|
|
499
|
+
selectedProvider = await promptProviderSelection(detectedProviders);
|
|
500
|
+
info(`Selected provider: ${selectedProvider}`);
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
uiError(`${err instanceof Error ? err.message : String(err)}`);
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Step 4: Check optional GitHub integration prerequisites
|
|
510
|
+
step(4, totalSteps, 'Checking GitHub integration prerequisites...');
|
|
511
|
+
const remoteStatus = getGitHubRemoteStatus(cwd);
|
|
512
|
+
const ghCheck = checkGhCli();
|
|
513
|
+
const ghAuthenticated = ghCheck.passed;
|
|
514
|
+
if (!remoteStatus.hasGitHubRemote) {
|
|
515
|
+
info('No GitHub remote detected. Board setup will be skipped for now.');
|
|
516
|
+
}
|
|
517
|
+
else if (!ghAuthenticated) {
|
|
518
|
+
warn(`${ghCheck.message}. Board setup will be skipped during init.`);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
success(ghCheck.message);
|
|
522
|
+
}
|
|
523
|
+
// Step 5: Detect test frameworks for QA bootstrap
|
|
524
|
+
step(5, totalSteps, 'Detecting test frameworks...');
|
|
525
|
+
const playwrightDetected = detectPlaywright(cwd);
|
|
526
|
+
let playwrightStatus = playwrightDetected ? 'detected' : 'not installed';
|
|
527
|
+
if (playwrightDetected) {
|
|
528
|
+
info('Playwright: detected');
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
info('Playwright: not found');
|
|
532
|
+
const installPlaywright = await promptYesNo('Install Playwright for QA now?', true);
|
|
533
|
+
if (installPlaywright) {
|
|
534
|
+
if (installPlaywrightForQa(cwd)) {
|
|
535
|
+
playwrightStatus = 'installed during init';
|
|
536
|
+
success('Installed Playwright test runner and Chromium browser.');
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
playwrightStatus = 'install failed';
|
|
540
|
+
console.warn(' Warning: Failed to install Playwright automatically. You can install it later.');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
info('Skipping Playwright install. QA can auto-install during execution if enabled.');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Set reviewerEnabled from flag (default: true, --no-reviewer sets to false)
|
|
548
|
+
const reviewerEnabled = options.reviewer !== false;
|
|
549
|
+
// Gather project information
|
|
550
|
+
const projectName = getProjectName(cwd);
|
|
551
|
+
const defaultBranch = getDefaultBranch(cwd);
|
|
552
|
+
// Display project configuration
|
|
553
|
+
header('Project Configuration');
|
|
554
|
+
label('Project', projectName);
|
|
555
|
+
label('Default branch', defaultBranch);
|
|
556
|
+
label('Provider', selectedProvider);
|
|
557
|
+
label('Reviewer', reviewerEnabled ? 'Enabled' : 'Disabled');
|
|
558
|
+
console.log();
|
|
559
|
+
// Define replacements for templates
|
|
560
|
+
const replacements = {
|
|
561
|
+
'${PROJECT_DIR}': cwd,
|
|
562
|
+
'${PROJECT_NAME}': projectName,
|
|
563
|
+
'${DEFAULT_BRANCH}': defaultBranch,
|
|
564
|
+
};
|
|
565
|
+
// Step 6: Create PRD directory structure
|
|
566
|
+
step(6, totalSteps, 'Creating PRD directory structure...');
|
|
567
|
+
const prdDirPath = path.join(cwd, prdDir);
|
|
568
|
+
const doneDirPath = path.join(prdDirPath, 'done');
|
|
569
|
+
ensureDir(doneDirPath);
|
|
570
|
+
success(`Created ${prdDirPath}/`);
|
|
571
|
+
success(`Created ${doneDirPath}/`);
|
|
572
|
+
// Step 7: Create logs directory
|
|
573
|
+
step(7, totalSteps, 'Creating logs directory...');
|
|
574
|
+
const logsPath = path.join(cwd, LOG_DIR);
|
|
575
|
+
ensureDir(logsPath);
|
|
576
|
+
success(`Created ${logsPath}/`);
|
|
577
|
+
// Add /logs/ to .gitignore
|
|
578
|
+
addToGitignore(cwd);
|
|
579
|
+
// Step 8: Create instructions directory and copy templates
|
|
580
|
+
step(8, totalSteps, 'Creating instructions directory...');
|
|
581
|
+
const instructionsDir = path.join(cwd, 'instructions');
|
|
582
|
+
ensureDir(instructionsDir);
|
|
583
|
+
success(`Created ${instructionsDir}/`);
|
|
584
|
+
// Load existing config (if present) to get templatesDir
|
|
585
|
+
const existingConfig = loadConfig(cwd);
|
|
586
|
+
const customTemplatesDirPath = path.join(cwd, existingConfig.templatesDir);
|
|
587
|
+
const customTemplatesDir = fs.existsSync(customTemplatesDirPath)
|
|
588
|
+
? customTemplatesDirPath
|
|
589
|
+
: null;
|
|
590
|
+
// Track template sources for summary
|
|
591
|
+
const templateSources = [];
|
|
592
|
+
// Copy executor.md template
|
|
593
|
+
const nwResolution = resolveTemplatePath('executor.md', customTemplatesDir, TEMPLATES_DIR);
|
|
594
|
+
const nwResult = processTemplate('executor.md', path.join(instructionsDir, 'executor.md'), replacements, force, nwResolution.path, nwResolution.source);
|
|
595
|
+
templateSources.push({ name: 'executor.md', source: nwResult.source });
|
|
596
|
+
// Copy prd-executor.md template
|
|
597
|
+
const peResolution = resolveTemplatePath('prd-executor.md', customTemplatesDir, TEMPLATES_DIR);
|
|
598
|
+
const peResult = processTemplate('prd-executor.md', path.join(instructionsDir, 'prd-executor.md'), replacements, force, peResolution.path, peResolution.source);
|
|
599
|
+
templateSources.push({ name: 'prd-executor.md', source: peResult.source });
|
|
600
|
+
// Copy pr-reviewer.md template
|
|
601
|
+
const prResolution = resolveTemplatePath('pr-reviewer.md', customTemplatesDir, TEMPLATES_DIR);
|
|
602
|
+
const prResult = processTemplate('pr-reviewer.md', path.join(instructionsDir, 'pr-reviewer.md'), replacements, force, prResolution.path, prResolution.source);
|
|
603
|
+
templateSources.push({ name: 'pr-reviewer.md', source: prResult.source });
|
|
604
|
+
// Copy qa.md template
|
|
605
|
+
const qaResolution = resolveTemplatePath('qa.md', customTemplatesDir, TEMPLATES_DIR);
|
|
606
|
+
const qaResult = processTemplate('qa.md', path.join(instructionsDir, 'qa.md'), replacements, force, qaResolution.path, qaResolution.source);
|
|
607
|
+
templateSources.push({ name: 'qa.md', source: qaResult.source });
|
|
608
|
+
// Copy audit.md template
|
|
609
|
+
const auditResolution = resolveTemplatePath('audit.md', customTemplatesDir, TEMPLATES_DIR);
|
|
610
|
+
const auditResult = processTemplate('audit.md', path.join(instructionsDir, 'audit.md'), replacements, force, auditResolution.path, auditResolution.source);
|
|
611
|
+
templateSources.push({ name: 'audit.md', source: auditResult.source });
|
|
612
|
+
// Copy prd-creator.md template
|
|
613
|
+
const plannerResolution = resolveTemplatePath('prd-creator.md', customTemplatesDir, TEMPLATES_DIR);
|
|
614
|
+
const plannerResult = processTemplate('prd-creator.md', path.join(instructionsDir, 'prd-creator.md'), replacements, force, plannerResolution.path, plannerResolution.source);
|
|
615
|
+
templateSources.push({ name: 'prd-creator.md', source: plannerResult.source });
|
|
616
|
+
// Step 9: Create config file
|
|
617
|
+
step(9, totalSteps, 'Creating configuration file...');
|
|
618
|
+
const configPath = path.join(cwd, CONFIG_FILE_NAME);
|
|
619
|
+
if (fs.existsSync(configPath) && !force) {
|
|
620
|
+
console.log(` Skipped (exists): ${configPath}`);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
const config = buildInitConfig({
|
|
624
|
+
projectName,
|
|
625
|
+
defaultBranch,
|
|
626
|
+
provider: selectedProvider,
|
|
627
|
+
reviewerEnabled,
|
|
628
|
+
prdDir,
|
|
629
|
+
});
|
|
630
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
631
|
+
success(`Created ${configPath}`);
|
|
632
|
+
}
|
|
633
|
+
// Step 10: Create GitHub Project board (only when repo has a GitHub remote)
|
|
634
|
+
step(10, totalSteps, 'Setting up GitHub Project board...');
|
|
635
|
+
const existingRaw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
636
|
+
const existingBoard = existingRaw.boardProvider;
|
|
637
|
+
let boardSetupStatus = 'Skipped';
|
|
638
|
+
if (existingBoard?.projectNumber && !force) {
|
|
639
|
+
boardSetupStatus = `Already configured (#${existingBoard.projectNumber})`;
|
|
640
|
+
info(`Board already configured (#${existingBoard.projectNumber}), skipping.`);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
if (!remoteStatus.hasGitHubRemote) {
|
|
644
|
+
boardSetupStatus = 'Skipped (no GitHub remote)';
|
|
645
|
+
info('No GitHub remote detected — skipping board setup. Run `night-watch board setup` manually.');
|
|
646
|
+
}
|
|
647
|
+
else if (!ghAuthenticated) {
|
|
648
|
+
boardSetupStatus = 'Skipped (gh auth required)';
|
|
649
|
+
info('GitHub CLI is not authenticated — run `gh auth login`, then `night-watch board setup`.');
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
try {
|
|
653
|
+
const provider = createBoardProvider({ enabled: true, provider: 'github' }, cwd);
|
|
654
|
+
const boardTitle = `${projectName} Night Watch`;
|
|
655
|
+
const board = await provider.setupBoard(boardTitle);
|
|
656
|
+
// Update the config file with the projectNumber
|
|
657
|
+
const rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
658
|
+
rawConfig.boardProvider = {
|
|
659
|
+
enabled: true,
|
|
660
|
+
provider: 'github',
|
|
661
|
+
projectNumber: board.number,
|
|
662
|
+
};
|
|
663
|
+
fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + '\n');
|
|
664
|
+
boardSetupStatus = `Created (#${board.number})`;
|
|
665
|
+
success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
|
|
666
|
+
}
|
|
667
|
+
catch (boardErr) {
|
|
668
|
+
boardSetupStatus = 'Failed (manual setup required)';
|
|
669
|
+
console.warn(` Warning: Could not set up GitHub Project board: ${boardErr instanceof Error ? boardErr.message : String(boardErr)}`);
|
|
670
|
+
info('Run `night-watch board setup` to create the board manually.');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Step 11: Sync Night Watch labels to GitHub
|
|
675
|
+
step(11, totalSteps, 'Syncing Night Watch labels to GitHub...');
|
|
676
|
+
let labelSyncStatus = 'Skipped';
|
|
677
|
+
if (!remoteStatus.hasGitHubRemote || !ghAuthenticated) {
|
|
678
|
+
labelSyncStatus = !remoteStatus.hasGitHubRemote
|
|
679
|
+
? 'Skipped (no GitHub remote)'
|
|
680
|
+
: 'Skipped (gh auth required)';
|
|
681
|
+
info('Skipping label sync (no GitHub remote or gh not authenticated).');
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
try {
|
|
685
|
+
const { NIGHT_WATCH_LABELS } = await import('@night-watch/core');
|
|
686
|
+
let created = 0;
|
|
687
|
+
for (const label of NIGHT_WATCH_LABELS) {
|
|
688
|
+
try {
|
|
689
|
+
execSync(`gh label create "${label.name}" --description "${label.description}" --color "${label.color}" --force`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
690
|
+
created++;
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
// Label creation is best-effort
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
labelSyncStatus = `Synced ${created}/${NIGHT_WATCH_LABELS.length} labels`;
|
|
697
|
+
success(`Synced ${created}/${NIGHT_WATCH_LABELS.length} labels to GitHub`);
|
|
698
|
+
}
|
|
699
|
+
catch (labelErr) {
|
|
700
|
+
labelSyncStatus = 'Failed';
|
|
701
|
+
warn(`Could not sync labels: ${labelErr instanceof Error ? labelErr.message : String(labelErr)}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// Step 12: Register in global registry
|
|
705
|
+
step(12, totalSteps, 'Registering project in global registry...');
|
|
706
|
+
try {
|
|
707
|
+
const { registerProject } = await import('@night-watch/core');
|
|
708
|
+
const entry = registerProject(cwd);
|
|
709
|
+
success(`Registered as "${entry.name}" in global registry`);
|
|
710
|
+
}
|
|
711
|
+
catch (regErr) {
|
|
712
|
+
console.warn(` Warning: Could not register in global registry: ${regErr instanceof Error ? regErr.message : String(regErr)}`);
|
|
713
|
+
}
|
|
714
|
+
// Step 13: Install AI skills
|
|
715
|
+
step(13, totalSteps, 'Installing Night Watch skills...');
|
|
716
|
+
const skillsResult = installSkills(cwd, selectedProvider, force, TEMPLATES_DIR);
|
|
717
|
+
if (skillsResult.installed > 0) {
|
|
718
|
+
success(`Installed ${skillsResult.installed} skills to ${skillsResult.location}`);
|
|
719
|
+
for (const skillName of NW_SKILLS) {
|
|
720
|
+
console.log(` /${skillName}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else if (skillsResult.skipped > 0) {
|
|
724
|
+
info(`Skills already installed (use --force to overwrite)`);
|
|
725
|
+
}
|
|
726
|
+
else if (skillsResult.type === 'none') {
|
|
727
|
+
info('No compatible AI skills directory detected — skipping.');
|
|
728
|
+
}
|
|
729
|
+
// Print summary
|
|
730
|
+
step(14, totalSteps, 'Initialization complete!');
|
|
731
|
+
// Summary with table
|
|
732
|
+
header('Initialization Complete');
|
|
733
|
+
const filesTable = createTable({ head: ['Created Files', ''] });
|
|
734
|
+
filesTable.push(['PRD Directory', `${prdDir}/done/`]);
|
|
735
|
+
filesTable.push(['Logs Directory', `${LOG_DIR}/`]);
|
|
736
|
+
filesTable.push(['Instructions', `instructions/executor.md (${templateSources[0].source})`]);
|
|
737
|
+
filesTable.push(['', `instructions/prd-executor.md (${templateSources[1].source})`]);
|
|
738
|
+
filesTable.push(['', `instructions/pr-reviewer.md (${templateSources[2].source})`]);
|
|
739
|
+
filesTable.push(['', `instructions/qa.md (${templateSources[3].source})`]);
|
|
740
|
+
filesTable.push(['', `instructions/audit.md (${templateSources[4].source})`]);
|
|
741
|
+
filesTable.push(['', `instructions/prd-creator.md (${templateSources[5].source})`]);
|
|
742
|
+
filesTable.push(['Config File', CONFIG_FILE_NAME]);
|
|
743
|
+
filesTable.push(['Board Setup', boardSetupStatus]);
|
|
744
|
+
filesTable.push(['Label Sync', labelSyncStatus]);
|
|
745
|
+
filesTable.push(['Global Registry', '~/.night-watch/projects.json']);
|
|
746
|
+
let skillsSummary;
|
|
747
|
+
if (skillsResult.installed > 0) {
|
|
748
|
+
skillsSummary = `${skillsResult.installed} skills → ${skillsResult.location}`;
|
|
749
|
+
}
|
|
750
|
+
else if (skillsResult.skipped > 0) {
|
|
751
|
+
skillsSummary = `Already installed (${skillsResult.location})`;
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
skillsSummary = 'Skipped';
|
|
755
|
+
}
|
|
756
|
+
filesTable.push(['Skills', skillsSummary]);
|
|
757
|
+
console.log(filesTable.toString());
|
|
758
|
+
// Configuration summary
|
|
759
|
+
header('Configuration');
|
|
760
|
+
label('Provider', selectedProvider);
|
|
761
|
+
label('Reviewer', reviewerEnabled ? 'Enabled' : 'Disabled');
|
|
762
|
+
label('Playwright', playwrightStatus);
|
|
763
|
+
console.log();
|
|
764
|
+
// Next steps
|
|
765
|
+
header('Next Steps');
|
|
766
|
+
info(`1. Add your PRD files to ${prdDir}/`);
|
|
767
|
+
info('2. Run `night-watch install` to set up cron jobs');
|
|
768
|
+
info('3. Run `night-watch doctor` to verify the full setup');
|
|
769
|
+
info('4. Or run `night-watch run` to execute PRDs manually');
|
|
770
|
+
if (skillsResult.installed > 0) {
|
|
771
|
+
info(`5. Use /nw-create-prd, /nw-run, /nw-add-issue and more in your AI assistant`);
|
|
772
|
+
}
|
|
773
|
+
console.log();
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
export default initCommand;
|
|
777
|
+
//# sourceMappingURL=init.js.map
|