wize-dev-kit 0.26.1 → 0.27.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCH.md +2 -1
- package/CHANGELOG.md +64 -0
- package/README.es.md +28 -6
- package/README.md +28 -6
- package/README.pt-BR.md +28 -6
- package/adapters/continue/render.js +2 -3
- package/adapters/copilot/render.js +3 -3
- package/adapters/cursor/render.js +2 -3
- package/adapters/generic/render.js +2 -3
- package/adapters/opencode/render.js +3 -4
- package/adapters/windsurf/render.js +2 -3
- package/docs/WORKTREES.md +150 -0
- package/docs/WORKTREES.pt-BR.md +150 -0
- package/docs/harnesses/antigravity.md +2 -0
- package/docs/harnesses/antigravity.pt-BR.md +2 -0
- package/docs/harnesses/claude-code.md +2 -0
- package/docs/harnesses/claude-code.pt-BR.md +2 -0
- package/docs/harnesses/codex.md +3 -1
- package/docs/harnesses/codex.pt-BR.md +3 -1
- package/docs/harnesses/continue.md +2 -0
- package/docs/harnesses/continue.pt-BR.md +2 -0
- package/docs/harnesses/copilot.md +2 -0
- package/docs/harnesses/copilot.pt-BR.md +2 -0
- package/docs/harnesses/cursor.md +2 -0
- package/docs/harnesses/cursor.pt-BR.md +2 -0
- package/docs/harnesses/generic.md +2 -0
- package/docs/harnesses/generic.pt-BR.md +2 -0
- package/docs/harnesses/hermes.md +2 -0
- package/docs/harnesses/hermes.pt-BR.md +2 -0
- package/docs/harnesses/kimi-code.md +2 -0
- package/docs/harnesses/kimi-code.pt-BR.md +2 -0
- package/docs/harnesses/kiro.md +2 -0
- package/docs/harnesses/kiro.pt-BR.md +2 -0
- package/docs/harnesses/opencode.md +2 -0
- package/docs/harnesses/opencode.pt-BR.md +2 -0
- package/docs/harnesses/windsurf.md +2 -0
- package/docs/harnesses/windsurf.pt-BR.md +2 -0
- package/package.json +1 -1
- package/src/orchestrator-skills/module.yaml +1 -1
- package/src/orchestrator-skills/wize/skill.md +17 -7
- package/src/orchestrator-skills/wize-help/skill.md +20 -7
- package/src/orchestrator-skills/wize-update/skill.md +33 -27
- package/tools/installer/commands/doctor.js +186 -40
- package/tools/installer/commands/document-project.js +13 -3
- package/tools/installer/commands/hooks.js +215 -0
- package/tools/installer/commands/hydrate.js +298 -0
- package/tools/installer/commands/sync.js +50 -23
- package/tools/installer/commands/update.js +596 -120
- package/tools/installer/commands/version-check.js +69 -10
- package/tools/installer/document-project/modes/full-rescan.js +1 -1
- package/tools/installer/document-project/modes/initial-scan.js +5 -4
- package/tools/installer/document-project/modes/quick.js +11 -4
- package/tools/installer/git-context.js +219 -0
- package/tools/installer/git-ops.js +211 -0
- package/tools/installer/install-plan.js +136 -0
- package/tools/installer/marker.js +67 -0
- package/tools/installer/project-root.js +68 -0
- package/tools/installer/project-status.js +171 -0
- package/tools/installer/render-shared.js +170 -4
- package/tools/installer/render-targets.js +112 -0
- package/tools/installer/semver.js +59 -0
- package/tools/installer/setup-helpers.js +89 -31
- package/tools/installer/templates/hydrate.sh +163 -0
- package/tools/installer/templates/wize-kit-update.yml +38 -0
- package/tools/installer/toml.js +133 -0
- package/tools/installer/util.js +16 -0
- package/tools/installer/version-check.js +167 -37
- package/tools/installer/wize-cli.js +216 -78
- package/tools/installer/sync.js +0 -29
|
@@ -14,7 +14,12 @@ const fs = require('node:fs');
|
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const { loadProjectConfig, loadInstalledKitVersion } = require('./update.js');
|
|
16
16
|
const { detectHarnessCli } = require('../baseline.js');
|
|
17
|
-
const {
|
|
17
|
+
const { getLatestNoWait } = require('../version-check.js');
|
|
18
|
+
const { compareVersions, semverGreater, safePin } = require('../semver.js');
|
|
19
|
+
const { gitContext, defaultBranch, trackedUnder } = require('../git-context.js');
|
|
20
|
+
const { getProjectStatus, supportsHydrate } = require('../project-status.js');
|
|
21
|
+
const { missingTargets } = require('../render-targets.js');
|
|
22
|
+
const { ALIAS_PATHS, gitignoreBlockStatus } = require('../setup-helpers.js');
|
|
18
23
|
|
|
19
24
|
function fileExists(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
|
|
20
25
|
function dirExists(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
|
|
@@ -72,12 +77,16 @@ function adapterTargetPath(targetCode, projectRoot) {
|
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
// Adapters that emit into more than one directory.
|
|
81
|
+
const EXTRA_ADAPTER_DIRS = { opencode: ['.opencode/commands'] };
|
|
82
|
+
|
|
75
83
|
function countAdapterFiles(targetCode, projectRoot) {
|
|
76
84
|
const dir = adapterTargetPath(targetCode, projectRoot);
|
|
77
|
-
if (!dir
|
|
85
|
+
if (!dir) return 0;
|
|
86
|
+
const dirs = [dir, ...(EXTRA_ADAPTER_DIRS[targetCode] || []).map(d => path.join(projectRoot, d))];
|
|
78
87
|
// Be permissive on file extension; .mdc, .md, .prompt, and SKILL.md dirs all count.
|
|
79
88
|
let count = 0;
|
|
80
|
-
const stack =
|
|
89
|
+
const stack = dirs.filter(dirExists);
|
|
81
90
|
while (stack.length) {
|
|
82
91
|
const d = stack.pop();
|
|
83
92
|
let entries; try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
@@ -103,7 +112,7 @@ function detectPhase(projectRoot) {
|
|
|
103
112
|
listFilesUnder(path.join(projectRoot, '.wize/solutioning/stories'), /\.md$/).length === 0)
|
|
104
113
|
return '3-solutioning (stories pending)';
|
|
105
114
|
if (!has('.wize/implementation/tea/risk-profile.md')) return '3-closeout (risk profile pending)';
|
|
106
|
-
if (!has('.wize/implementation/sprint-status.md')) return '4-implementation (sprint planning pending)';
|
|
115
|
+
if (!has('.wize/implementation/sprint-status.yaml') && !has('.wize/implementation/sprint-status.md')) return '4-implementation (sprint planning pending)';
|
|
107
116
|
return '4-implementation';
|
|
108
117
|
}
|
|
109
118
|
|
|
@@ -150,19 +159,39 @@ function knowledgeStatus(projectRoot) {
|
|
|
150
159
|
return { exists: true, files: refreshed, pendingLines, toBeGeneratedMarkers, stateExists, stateAgeDays };
|
|
151
160
|
}
|
|
152
161
|
|
|
162
|
+
function readHeadFromFs(gitDir, commonDir) {
|
|
163
|
+
const HEAD = readSafe(path.join(gitDir, 'HEAD')).trim();
|
|
164
|
+
if (!HEAD) return { branch: null, head: null, detached: false };
|
|
165
|
+
if (!HEAD.startsWith('ref: ')) return { branch: null, head: HEAD.slice(0, 7), detached: true };
|
|
166
|
+
const ref = HEAD.slice(5).trim();
|
|
167
|
+
const branch = ref.replace(/^refs\/heads\//, '');
|
|
168
|
+
let sha = null;
|
|
169
|
+
for (const dir of [gitDir, commonDir].filter(Boolean)) {
|
|
170
|
+
const loose = readSafe(path.join(dir, ref)).trim();
|
|
171
|
+
if (loose) { sha = loose; break; }
|
|
172
|
+
}
|
|
173
|
+
return { branch, head: sha ? sha.slice(0, 7) : null, detached: false };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Uses git when it works; a `.git` file or directory is enough to count as a
|
|
177
|
+
// repository even when git itself cannot run (fake or broken `.git`).
|
|
153
178
|
function gitInfo(projectRoot) {
|
|
154
|
-
const
|
|
155
|
-
if (!
|
|
156
|
-
let branch
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
179
|
+
const ctx = gitContext(projectRoot);
|
|
180
|
+
if (!ctx.isRepo) return { isRepo: false };
|
|
181
|
+
let { branch, head, detached } = ctx;
|
|
182
|
+
if (!branch && !head && ctx.gitDir) {
|
|
183
|
+
const fromFs = readHeadFromFs(ctx.gitDir, ctx.commonDir);
|
|
184
|
+
branch = fromFs.branch; head = fromFs.head; detached = fromFs.detached;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
isRepo: true,
|
|
188
|
+
linked: !!ctx.linked,
|
|
189
|
+
primary: ctx.primary || null,
|
|
190
|
+
branch: branch || null,
|
|
191
|
+
detached: !!detached,
|
|
192
|
+
head: head || null,
|
|
193
|
+
defaultBranch: defaultBranch(projectRoot)
|
|
194
|
+
};
|
|
166
195
|
}
|
|
167
196
|
|
|
168
197
|
function severityIcon(level) {
|
|
@@ -170,7 +199,10 @@ function severityIcon(level) {
|
|
|
170
199
|
}
|
|
171
200
|
|
|
172
201
|
async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
|
|
173
|
-
const
|
|
202
|
+
const json = !!opts.json;
|
|
203
|
+
const env = opts.env || process.env;
|
|
204
|
+
const out = opts.log || console.log;
|
|
205
|
+
const log = (m) => { if (!json) out(m); };
|
|
174
206
|
const cwd = projectRoot;
|
|
175
207
|
const cfg = loadProjectConfig(cwd);
|
|
176
208
|
const installed = loadInstalledKitVersion(kitRoot);
|
|
@@ -181,25 +213,39 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
|
|
|
181
213
|
const targets = (cfg.install && cfg.install.ide_targets) || [];
|
|
182
214
|
const profiles = (cfg.install && cfg.install.profiles) || [];
|
|
183
215
|
const harnesses = detectHarnessCli({ preferIde: targets });
|
|
216
|
+
const pin = (cfg.project && cfg.project.kit_version) || null;
|
|
184
217
|
|
|
185
|
-
//
|
|
218
|
+
// Registry: cache only, never the network. WIZE_DISABLE_UPDATE_CHECK=1 skips it.
|
|
219
|
+
const updateCheckOff = env.WIZE_DISABLE_UPDATE_CHECK === '1';
|
|
186
220
|
let registryLatest = null;
|
|
187
|
-
|
|
221
|
+
if (!updateCheckOff) {
|
|
222
|
+
// Without a terminal (an agent running `doctor --json`) no background refresh is started.
|
|
223
|
+
const isTTY = opts.isTTY !== undefined ? opts.isTTY : process.stdout.isTTY;
|
|
224
|
+
const lookupEnv = isTTY ? env : { ...env, WIZE_NO_BG_REFRESH: '1' };
|
|
225
|
+
try { registryLatest = (opts.getLatestNoWait || getLatestNoWait)({ env: lookupEnv }) || null; } catch (_) { /* best effort */ }
|
|
226
|
+
}
|
|
188
227
|
|
|
228
|
+
const status = getProjectStatus({ kitRoot, cwd, latest: registryLatest, latestSource: 'cache', env });
|
|
189
229
|
const suggestions = [];
|
|
230
|
+
const suggest = (level, text, command = null) => suggestions.push(command ? { level, text, command } : { level, text });
|
|
190
231
|
|
|
191
232
|
// -- Section: Kit --
|
|
192
233
|
log('');
|
|
193
234
|
log('Wize Dev Kit — Doctor');
|
|
194
235
|
log('─────────────────────');
|
|
195
236
|
log(`Kit version (installed): ${installed || '?'}`);
|
|
196
|
-
log(`Kit version (project): ${
|
|
197
|
-
log(`Kit version (registry): ${registryLatest || '(offline or skipped)'}`);
|
|
198
|
-
if (
|
|
199
|
-
|
|
237
|
+
log(`Kit version (project): ${pin || '— (no install)'}`);
|
|
238
|
+
log(`Kit version (registry): ${registryLatest || (updateCheckOff ? '(update check disabled)' : '(offline or skipped)')}`);
|
|
239
|
+
if (pin && installed) {
|
|
240
|
+
const cmp = compareVersions(pin, installed);
|
|
241
|
+
if (cmp < 0) {
|
|
242
|
+
suggest('warn', `Project pinned ${pin}, this kit is ${installed}. Run \`npx wize-dev-kit@latest update\` to converge.`, 'npx wize-dev-kit@latest update');
|
|
243
|
+
} else if (cmp > 0 && safePin(pin)) {
|
|
244
|
+
suggest('info', `Project pinned ${pin} is newer than this kit (${installed}). Use the pinned kit: \`npx --yes wize-dev-kit@${pin} doctor\`.`, `npx --yes wize-dev-kit@${pin} doctor`);
|
|
245
|
+
}
|
|
200
246
|
}
|
|
201
247
|
if (registryLatest && installed && semverGreater(registryLatest, installed)) {
|
|
202
|
-
|
|
248
|
+
suggest('info', `Registry has ${registryLatest}; you're on ${installed}. Run \`npx wize-dev-kit@latest update\` to pick it up.`, 'npx wize-dev-kit@latest update');
|
|
203
249
|
}
|
|
204
250
|
|
|
205
251
|
// -- Section: Project --
|
|
@@ -207,8 +253,13 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
|
|
|
207
253
|
log('Project');
|
|
208
254
|
log('───────');
|
|
209
255
|
if (!cfg.project) {
|
|
210
|
-
|
|
211
|
-
|
|
256
|
+
if (status.reason === 'kit-not-on-branch') {
|
|
257
|
+
log(`No .wize/config/project.toml on this branch — the kit is installed on ${status.kit_at}'s branch.`);
|
|
258
|
+
suggest('warn', `The kit is installed on ${status.kit_at}'s branch but not on this one (kit-not-on-branch). Merge the kit into this branch, or pass --here to install here.`);
|
|
259
|
+
} else {
|
|
260
|
+
log('No .wize/config/project.toml — kit is not installed here.');
|
|
261
|
+
suggest('error', 'Run `npx wize-dev-kit install` to set the kit up in this repo.', 'npx wize-dev-kit install');
|
|
262
|
+
}
|
|
212
263
|
} else {
|
|
213
264
|
log(`Name: ${cfg.project.name || '?'}`);
|
|
214
265
|
log(`Profiles: ${profiles.join(', ') || '(none)'}`);
|
|
@@ -218,7 +269,46 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
|
|
|
218
269
|
log(`Current phase: ${phase}`);
|
|
219
270
|
}
|
|
220
271
|
|
|
272
|
+
// -- Section: Hydration --
|
|
273
|
+
let hydration = null;
|
|
274
|
+
if (cfg.project) {
|
|
275
|
+
const missing = status.missing_targets;
|
|
276
|
+
hydration = {
|
|
277
|
+
status: status.hydrated,
|
|
278
|
+
marker: status.marker_version,
|
|
279
|
+
pin: status.pin,
|
|
280
|
+
binary: installed,
|
|
281
|
+
user_toml: status.user_toml,
|
|
282
|
+
missing_targets: missing,
|
|
283
|
+
command: status.commands.hydrate
|
|
284
|
+
};
|
|
285
|
+
log('');
|
|
286
|
+
log('Hydration');
|
|
287
|
+
log('─────────');
|
|
288
|
+
log(` status: ${status.hydrated}`);
|
|
289
|
+
log(` marker: ${status.marker_version || '(none)'}`);
|
|
290
|
+
log(` pin: ${status.pin || '(none)'}`);
|
|
291
|
+
log(` this kit: ${installed || '?'}`);
|
|
292
|
+
log(` user.toml: ${status.user_toml}`);
|
|
293
|
+
for (const t of targets) log(` ${t.padEnd(14)} ${missing.includes(t) ? '✖ not rendered' : '✓ rendered'}`);
|
|
294
|
+
|
|
295
|
+
const cmd = status.commands.hydrate;
|
|
296
|
+
if (status.hydrated === 'missing') {
|
|
297
|
+
suggest('error', `This checkout is not hydrated (not rendered: ${missing.join(', ')}). Run \`${cmd}\`.`, cmd);
|
|
298
|
+
} else if (status.hydrated === 'stale') {
|
|
299
|
+
suggest('error', `Rendered output is stale: marker says kit ${status.marker_version}, project pins ${status.pin}. Run \`${cmd}\`.`, cmd);
|
|
300
|
+
}
|
|
301
|
+
if (status.user_toml === 'missing' && status.hydrated === 'ok') {
|
|
302
|
+
if (supportsHydrate(status.pin)) {
|
|
303
|
+
suggest('error', `.wize/config/user.toml is missing in this checkout. Run \`${cmd}\`.`, cmd);
|
|
304
|
+
} else {
|
|
305
|
+
suggest('warn', '.wize/config/user.toml is missing in this checkout. Copy it from your primary checkout, or run `npx wize-dev-kit@latest update` to adopt hydrate.', 'npx wize-dev-kit@latest update');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
221
310
|
// -- Section: Adapters --
|
|
311
|
+
const adapters = [];
|
|
222
312
|
if (cfg.install) {
|
|
223
313
|
log('');
|
|
224
314
|
log('IDE Adapters');
|
|
@@ -226,9 +316,33 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
|
|
|
226
316
|
for (const t of targets) {
|
|
227
317
|
const dir = adapterTargetPath(t, cwd);
|
|
228
318
|
const count = countAdapterFiles(t, cwd);
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
319
|
+
const rendered = missingTargets(cwd, [t]).length === 0;
|
|
320
|
+
adapters.push({ target: t, files: count, rendered });
|
|
321
|
+
const label = rendered && count > 0 ? `✓ ${count} files` : count > 0 ? `✖ alias only` : '✖ none';
|
|
322
|
+
log(` ${t.padEnd(14)} ${label.padEnd(15)} ${dir ? path.relative(cwd, dir) : '(no path)'}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// -- Section: Leaks (generated files that git would see) --
|
|
327
|
+
const leaks = { tracked_aliases: [], gitignore: null };
|
|
328
|
+
if (cfg.project && git.isRepo) {
|
|
329
|
+
leaks.tracked_aliases = trackedUnder(cwd, ALIAS_PATHS);
|
|
330
|
+
const giFile = path.join(cwd, '.gitignore');
|
|
331
|
+
leaks.gitignore = gitignoreBlockStatus(fs.existsSync(giFile) ? readSafe(giFile) : null);
|
|
332
|
+
log('');
|
|
333
|
+
log('Generated files and git');
|
|
334
|
+
log('───────────────────────');
|
|
335
|
+
log(` tracked alias files: ${leaks.tracked_aliases.length}`);
|
|
336
|
+
log(` .gitignore block: ${leaks.gitignore}`);
|
|
337
|
+
const fix = 'npx wize-dev-kit@latest update';
|
|
338
|
+
if (leaks.tracked_aliases.length) {
|
|
339
|
+
const shown = leaks.tracked_aliases.slice(0, 3).join(', ') + (leaks.tracked_aliases.length > 3 ? ', ...' : '');
|
|
340
|
+
suggest('warn', `${leaks.tracked_aliases.length} generated alias file(s) are tracked by git (${shown}). Run \`${fix}\` to untrack them.`, fix);
|
|
341
|
+
}
|
|
342
|
+
if (leaks.gitignore === 'drift') {
|
|
343
|
+
suggest('warn', `The managed .gitignore block is out of date; generated files can leak into git status. Run \`${fix}\`.`, fix);
|
|
344
|
+
} else if (leaks.gitignore === 'missing') {
|
|
345
|
+
suggest('warn', `.gitignore has no wize-dev-kit managed block; generated files can leak into git status. Run \`${fix}\`.`, fix);
|
|
232
346
|
}
|
|
233
347
|
}
|
|
234
348
|
|
|
@@ -242,8 +356,8 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
|
|
|
242
356
|
log(`CONCERNS: ${gates.CONCERNS}`);
|
|
243
357
|
log(`FAIL: ${gates.FAIL}`);
|
|
244
358
|
log(`WAIVED: ${gates.WAIVED}`);
|
|
245
|
-
if (gates.FAIL > 0)
|
|
246
|
-
if (gates.CONCERNS > 0)
|
|
359
|
+
if (gates.FAIL > 0) suggest('error', `${gates.FAIL} story gate(s) at FAIL. Stories must not merge until resolved (advisory mode) or are blocking (enforcing mode).`);
|
|
360
|
+
if (gates.CONCERNS > 0) suggest('warn', `${gates.CONCERNS} story gate(s) at CONCERNS. Review findings before next sprint.`);
|
|
247
361
|
}
|
|
248
362
|
|
|
249
363
|
// -- Section: Knowledge --
|
|
@@ -255,26 +369,26 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
|
|
|
255
369
|
const age = f.days == null ? 'no last_refreshed' : `${f.days}d ago`;
|
|
256
370
|
log(` ${f.name.padEnd(28)} ${age}`);
|
|
257
371
|
if (f.days != null && f.days > 60) {
|
|
258
|
-
|
|
372
|
+
suggest('warn', `\`${f.name}\` last refreshed ${f.days}d ago. Run \`/wize-refresh-knowledge\` after current sprint.`);
|
|
259
373
|
}
|
|
260
374
|
}
|
|
261
375
|
if (knowledge.pendingLines > 0) {
|
|
262
376
|
log(` _pending.md: ${knowledge.pendingLines} inline note(s) waiting consolidation`);
|
|
263
377
|
if (knowledge.pendingLines >= 5) {
|
|
264
|
-
|
|
378
|
+
suggest('info', `${knowledge.pendingLines} notes piled up in _pending.md. Time to run \`/wize-refresh-knowledge\`.`);
|
|
265
379
|
}
|
|
266
380
|
}
|
|
267
381
|
const markerText = knowledge.toBeGeneratedMarkers > 0 ? `${knowledge.toBeGeneratedMarkers} marker(s)` : 'none';
|
|
268
382
|
log(` To be generated markers: ${markerText}`);
|
|
269
383
|
if (knowledge.toBeGeneratedMarkers >= 5) {
|
|
270
|
-
|
|
384
|
+
suggest('warn', `${knowledge.toBeGeneratedMarkers} docs marked "To be generated" in index.md. Run \`wize-dev-kit document-project initial_scan deep\` to fill them.`);
|
|
271
385
|
}
|
|
272
386
|
const stateText = knowledge.stateExists
|
|
273
387
|
? (knowledge.stateAgeDays == null ? 'project-scan-report.json exists (age unknown)' : `project-scan-report.json ${knowledge.stateAgeDays}d old`)
|
|
274
388
|
: 'no project-scan-report.json';
|
|
275
389
|
log(` Scan state: ${stateText}`);
|
|
276
390
|
if (!knowledge.stateExists) {
|
|
277
|
-
|
|
391
|
+
suggest('info', 'No scan state file found. Run `wize-dev-kit document-project initial_scan` to create a baseline + state.');
|
|
278
392
|
}
|
|
279
393
|
}
|
|
280
394
|
|
|
@@ -284,7 +398,7 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
|
|
|
284
398
|
log('───────────────────────');
|
|
285
399
|
if (harnesses.length === 0) {
|
|
286
400
|
log(' (none detected)');
|
|
287
|
-
|
|
401
|
+
suggest('info', 'No harness CLI detected. Brownfield baseline + auto-run features only work in your IDE; the headless flow is unavailable.');
|
|
288
402
|
} else {
|
|
289
403
|
for (const h of harnesses) log(` ${h.binary.padEnd(10)} ${h.path}`);
|
|
290
404
|
}
|
|
@@ -295,10 +409,12 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
|
|
|
295
409
|
log('───');
|
|
296
410
|
if (!git.isRepo) {
|
|
297
411
|
log(' (not a git repo)');
|
|
298
|
-
|
|
412
|
+
suggest('warn', 'Not a git repository. Run `git init` before installing the kit so version history is recorded.');
|
|
299
413
|
} else {
|
|
300
|
-
log(`
|
|
301
|
-
log(`
|
|
414
|
+
log(` checkout: ${git.linked ? `linked worktree of ${git.primary || '?'}` : 'primary checkout'}`);
|
|
415
|
+
log(` branch: ${git.detached ? `detached @${git.head || '?'}` : (git.branch || '?')}`);
|
|
416
|
+
log(` head: ${git.head || '(no commits)'}`);
|
|
417
|
+
log(` default branch: ${git.defaultBranch || '(unknown)'}`);
|
|
302
418
|
}
|
|
303
419
|
|
|
304
420
|
// -- Section: Suggestions --
|
|
@@ -312,7 +428,37 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
|
|
|
312
428
|
}
|
|
313
429
|
log('');
|
|
314
430
|
|
|
315
|
-
|
|
431
|
+
const count = (level) => suggestions.filter(s => s.level === level).length;
|
|
432
|
+
const summary = { errors: count('error'), warnings: count('warn'), infos: count('info') };
|
|
433
|
+
const exitCode = opts.strict && summary.errors > 0 ? 1 : 0;
|
|
434
|
+
|
|
435
|
+
if (json) {
|
|
436
|
+
out(JSON.stringify({
|
|
437
|
+
schema: 1,
|
|
438
|
+
ok: summary.errors === 0,
|
|
439
|
+
exit_code: exitCode,
|
|
440
|
+
root: cwd,
|
|
441
|
+
kit: { installed, project: pin, registry: registryLatest, update_check: updateCheckOff ? 'disabled' : 'cache' },
|
|
442
|
+
project: cfg.project ? {
|
|
443
|
+
name: cfg.project.name || null,
|
|
444
|
+
profiles,
|
|
445
|
+
targets,
|
|
446
|
+
language: { communication: (cfg.language && cfg.language.communication) || null, document_output: (cfg.language && cfg.language.document_output) || null },
|
|
447
|
+
phase
|
|
448
|
+
} : null,
|
|
449
|
+
git,
|
|
450
|
+
hydration,
|
|
451
|
+
adapters,
|
|
452
|
+
leaks,
|
|
453
|
+
gates,
|
|
454
|
+
knowledge,
|
|
455
|
+
harnesses: harnesses.map(h => ({ binary: h.binary, path: h.path })),
|
|
456
|
+
suggestions,
|
|
457
|
+
summary
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return { suggestions, phase, gates, knowledge, targets, profiles, installed, registryLatest, status, hydration, leaks, git, summary, exitCode };
|
|
316
462
|
}
|
|
317
463
|
|
|
318
464
|
module.exports = {
|
|
@@ -21,6 +21,7 @@ function parseMode(args) {
|
|
|
21
21
|
let resume = false;
|
|
22
22
|
let scanLevel = 'quick';
|
|
23
23
|
let target = null;
|
|
24
|
+
let overwrite = false;
|
|
24
25
|
|
|
25
26
|
for (let i = 0; i < args.length; i++) {
|
|
26
27
|
const arg = args[i];
|
|
@@ -28,6 +29,10 @@ function parseMode(args) {
|
|
|
28
29
|
resume = true;
|
|
29
30
|
continue;
|
|
30
31
|
}
|
|
32
|
+
if (arg === '--force') {
|
|
33
|
+
overwrite = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
31
36
|
if (arg === '--target') {
|
|
32
37
|
target = args[i + 1];
|
|
33
38
|
i++;
|
|
@@ -53,7 +58,7 @@ function parseMode(args) {
|
|
|
53
58
|
return { mode: null, error: `unknown mode: ${mode}. Valid modes: ${[...VALID_MODES].join(', ')}` };
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
return { mode, resume, scanLevel, target };
|
|
61
|
+
return { mode, resume, scanLevel, target, overwrite };
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
function cmdDocumentProject({ kitRoot, projectRoot, args = [], opts = {} }) {
|
|
@@ -66,12 +71,17 @@ function cmdDocumentProject({ kitRoot, projectRoot, args = [], opts = {} }) {
|
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
if (parsed.mode === 'quick') {
|
|
69
|
-
const
|
|
74
|
+
const env = opts.env || process.env;
|
|
75
|
+
if (env.WIZE_SKIP_BASELINE === '1') {
|
|
76
|
+
log('WIZE_SKIP_BASELINE=1: skipped');
|
|
77
|
+
return { ok: true, mode: 'quick', changed: false, skipped: true, written: [] };
|
|
78
|
+
}
|
|
79
|
+
const r = runQuick(projectRoot, { log, overwrite: parsed.overwrite });
|
|
70
80
|
return { ok: true, mode: 'quick', ...r };
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
if (parsed.mode === 'initial_scan') {
|
|
74
|
-
const r = runInitialScan(projectRoot, { scanLevel: parsed.scanLevel, log });
|
|
84
|
+
const r = runInitialScan(projectRoot, { scanLevel: parsed.scanLevel, log, overwrite: parsed.overwrite });
|
|
75
85
|
return { ok: true, mode: 'initial_scan', ...r };
|
|
76
86
|
}
|
|
77
87
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// `wize-dev-kit hooks` — opt-in automation that hydrates a checkout on its own.
|
|
2
|
+
//
|
|
3
|
+
// hooks install [--claude] [--cursor] [--github-action]
|
|
4
|
+
// hooks print <orca|claude|cursor|codex|github-action>
|
|
5
|
+
//
|
|
6
|
+
// `install` always writes the tracked, managed .wize/hooks/hydrate.sh and then
|
|
7
|
+
// only the config files whose flag was passed. `print` writes nothing. Nothing
|
|
8
|
+
// here runs without an explicit flag, because every file it writes is tracked.
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
const HOOK_REL = '.wize/hooks/hydrate.sh';
|
|
16
|
+
const HOOK_HEADER = '# wize-dev-kit managed hook v1';
|
|
17
|
+
const TEMPLATES = path.resolve(__dirname, '..', 'templates');
|
|
18
|
+
const PRINT_TARGETS = ['orca', 'claude', 'cursor', 'codex', 'github-action'];
|
|
19
|
+
|
|
20
|
+
const readTemplate = (name) => fs.readFileSync(path.join(TEMPLATES, name), 'utf-8');
|
|
21
|
+
|
|
22
|
+
const CLAUDE_ENTRY = {
|
|
23
|
+
matcher: 'startup',
|
|
24
|
+
hooks: [{ type: 'command', command: 'sh "$CLAUDE_PROJECT_DIR"/.wize/hooks/hydrate.sh', timeout: 180 }]
|
|
25
|
+
};
|
|
26
|
+
const CURSOR_CONFIG = { 'setup-worktree': ['sh .wize/hooks/hydrate.sh'] };
|
|
27
|
+
|
|
28
|
+
// A hydrate.sh this kit wrote carries the header within its first lines. Any
|
|
29
|
+
// other file at that path belongs to the user.
|
|
30
|
+
function isManagedHook(content) {
|
|
31
|
+
return typeof content === 'string' && content.split('\n', 6).some(l => l.trim() === HOOK_HEADER);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The new content for an installed hook, or null when it is absent, not
|
|
35
|
+
// managed, or already current. Used by `update` to refresh the script.
|
|
36
|
+
function refreshedHook(current) {
|
|
37
|
+
if (!isManagedHook(current)) return null;
|
|
38
|
+
const next = readTemplate('hydrate.sh');
|
|
39
|
+
return next === current ? null : next;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Pure. `text` is the current .claude/settings.json (null when absent).
|
|
43
|
+
// -> { ok: true, changed, next } | { ok: false, error }. Every other key and
|
|
44
|
+
// hook is kept; an entry that already runs the managed script is not duplicated.
|
|
45
|
+
function mergeClaudeSettings(text) {
|
|
46
|
+
let settings = {};
|
|
47
|
+
if (text !== null && text.trim() !== '') {
|
|
48
|
+
try { settings = JSON.parse(text); }
|
|
49
|
+
catch (err) { return { ok: false, error: `.claude/settings.json is not valid JSON (${err.message})` }; }
|
|
50
|
+
}
|
|
51
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
52
|
+
return { ok: false, error: '.claude/settings.json must hold a JSON object' };
|
|
53
|
+
}
|
|
54
|
+
if (settings.hooks === undefined) settings.hooks = {};
|
|
55
|
+
if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) {
|
|
56
|
+
return { ok: false, error: '.claude/settings.json: "hooks" must be an object' };
|
|
57
|
+
}
|
|
58
|
+
if (settings.hooks.SessionStart === undefined) settings.hooks.SessionStart = [];
|
|
59
|
+
if (!Array.isArray(settings.hooks.SessionStart)) {
|
|
60
|
+
return { ok: false, error: '.claude/settings.json: hooks.SessionStart must be an array' };
|
|
61
|
+
}
|
|
62
|
+
if (JSON.stringify(settings.hooks.SessionStart).includes(HOOK_REL)) return { ok: true, changed: false, next: text };
|
|
63
|
+
settings.hooks.SessionStart.push(CLAUDE_ENTRY);
|
|
64
|
+
return { ok: true, changed: true, next: JSON.stringify(settings, null, 2) + '\n' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cursorJson = () => JSON.stringify(CURSOR_CONFIG, null, 2) + '\n';
|
|
68
|
+
|
|
69
|
+
function printSnippet(target) {
|
|
70
|
+
const claude = JSON.stringify({ hooks: { SessionStart: [CLAUDE_ENTRY] } }, null, 2);
|
|
71
|
+
const snippets = {
|
|
72
|
+
orca: [
|
|
73
|
+
'# orca.yaml (commit it at the repo root)',
|
|
74
|
+
'scripts:',
|
|
75
|
+
` setup: sh ${HOOK_REL}`,
|
|
76
|
+
'setupAgentStartupPolicy: wait-for-setup',
|
|
77
|
+
'',
|
|
78
|
+
'# A non-empty setup hook saved locally in Orca overrides orca.yaml. Clear it in the',
|
|
79
|
+
'# repository settings, or the line above never runs.',
|
|
80
|
+
'',
|
|
81
|
+
'# .worktreeinclude (gitignored files Orca copies into each new worktree)',
|
|
82
|
+
'.wize/config/user.toml'
|
|
83
|
+
].join('\n'),
|
|
84
|
+
claude: [`# .claude/settings.json (or run: wize-dev-kit hooks install --claude)`, claude].join('\n'),
|
|
85
|
+
cursor: ['# .cursor/worktrees.json (or run: wize-dev-kit hooks install --cursor)', cursorJson().trimEnd()].join('\n'),
|
|
86
|
+
codex: [
|
|
87
|
+
'Codex local environment: set the setup script for this project to',
|
|
88
|
+
'',
|
|
89
|
+
` sh ${HOOK_REL}`,
|
|
90
|
+
'',
|
|
91
|
+
'Codex stores it in its own local-environment config, so the kit does not write it.'
|
|
92
|
+
].join('\n'),
|
|
93
|
+
'github-action': [`# .github/workflows/wize-kit-update.yml (or run: wize-dev-kit hooks install --github-action)`, readTemplate('wize-kit-update.yml').trimEnd()].join('\n')
|
|
94
|
+
};
|
|
95
|
+
return snippets[target];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const USAGE = [
|
|
99
|
+
'Usage:',
|
|
100
|
+
' wize-dev-kit hooks install [--claude] [--cursor] [--github-action]',
|
|
101
|
+
' wize-dev-kit hooks print <orca|claude|cursor|codex|github-action>',
|
|
102
|
+
'',
|
|
103
|
+
'install always writes .wize/hooks/hydrate.sh; each flag adds one config file.'
|
|
104
|
+
].join('\n');
|
|
105
|
+
|
|
106
|
+
function writeText(file, content, mode) {
|
|
107
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
108
|
+
fs.writeFileSync(file, content, mode ? { encoding: 'utf-8', mode } : 'utf-8');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readOrNull(file) {
|
|
112
|
+
try { return fs.readFileSync(file, 'utf-8'); } catch { return null; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function install({ projectRoot, flags, log, errlog }) {
|
|
116
|
+
const want = { claude: !!flags.claude, cursor: !!flags.cursor, action: !!(flags['github-action'] || flags.githubAction) };
|
|
117
|
+
if (!want.claude && !want.cursor && !want.action) {
|
|
118
|
+
errlog('✖ hooks install needs at least one of --claude, --cursor, --github-action');
|
|
119
|
+
errlog(USAGE);
|
|
120
|
+
return { exitCode: 1, written: [], unchanged: [] };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const written = [];
|
|
124
|
+
const unchanged = [];
|
|
125
|
+
const note = (rel, changed) => {
|
|
126
|
+
(changed ? written : unchanged).push(rel);
|
|
127
|
+
log(`${changed ? '✓ wrote' : '· unchanged'} ${rel}`);
|
|
128
|
+
};
|
|
129
|
+
const abs = (rel) => path.join(projectRoot, rel);
|
|
130
|
+
const fail = (message) => {
|
|
131
|
+
errlog(`✖ ${message}`);
|
|
132
|
+
return { exitCode: 1, written, unchanged };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Validate everything that can refuse before writing anything.
|
|
136
|
+
const hookFile = abs(HOOK_REL);
|
|
137
|
+
const hookCurrent = readOrNull(hookFile);
|
|
138
|
+
if (hookCurrent !== null && !isManagedHook(hookCurrent)) {
|
|
139
|
+
return fail(`${HOOK_REL} exists and is not managed by wize-dev-kit; move it aside or merge by hand`);
|
|
140
|
+
}
|
|
141
|
+
let claudeMerge = null;
|
|
142
|
+
if (want.claude) {
|
|
143
|
+
claudeMerge = mergeClaudeSettings(readOrNull(abs('.claude/settings.json')));
|
|
144
|
+
if (!claudeMerge.ok) return fail(claudeMerge.error);
|
|
145
|
+
}
|
|
146
|
+
if (!fs.existsSync(abs('.wize/config/project.toml'))) {
|
|
147
|
+
errlog('warning: no .wize/config/project.toml here; the hook skips until the kit is installed');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const script = readTemplate('hydrate.sh');
|
|
151
|
+
if (hookCurrent !== script) writeText(hookFile, script);
|
|
152
|
+
fs.chmodSync(hookFile, 0o755);
|
|
153
|
+
note(HOOK_REL, hookCurrent !== script);
|
|
154
|
+
|
|
155
|
+
if (want.claude) {
|
|
156
|
+
if (claudeMerge.changed) writeText(abs('.claude/settings.json'), claudeMerge.next);
|
|
157
|
+
note('.claude/settings.json', claudeMerge.changed);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (want.cursor) {
|
|
161
|
+
const rel = '.cursor/worktrees.json';
|
|
162
|
+
const current = readOrNull(abs(rel));
|
|
163
|
+
if (current === null) {
|
|
164
|
+
writeText(abs(rel), cursorJson());
|
|
165
|
+
note(rel, true);
|
|
166
|
+
} else if (current.includes(HOOK_REL)) {
|
|
167
|
+
note(rel, false);
|
|
168
|
+
} else {
|
|
169
|
+
unchanged.push(rel);
|
|
170
|
+
log(`· ${rel} exists; add this line to its "setup-worktree" array:`);
|
|
171
|
+
log(` "sh ${HOOK_REL}"`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (want.action) {
|
|
176
|
+
const rel = '.github/workflows/wize-kit-update.yml';
|
|
177
|
+
if (fs.existsSync(abs(rel))) note(rel, false);
|
|
178
|
+
else {
|
|
179
|
+
writeText(abs(rel), readTemplate('wize-kit-update.yml'));
|
|
180
|
+
note(rel, true);
|
|
181
|
+
log(' enable "Allow GitHub Actions to create and approve pull requests" in the repository settings');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { exitCode: 0, written, unchanged };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function cmdHooks({ projectRoot, args = [], flags = {}, opts = {} } = {}) {
|
|
189
|
+
const log = opts.log || console.log;
|
|
190
|
+
const errlog = opts.errlog || console.error;
|
|
191
|
+
const [sub, target] = args;
|
|
192
|
+
|
|
193
|
+
if (sub === 'install') return install({ projectRoot, flags, log, errlog });
|
|
194
|
+
if (sub === 'print') {
|
|
195
|
+
if (!PRINT_TARGETS.includes(target)) {
|
|
196
|
+
errlog(`✖ ${target ? `unknown target "${target}"` : 'hooks print needs a target'}; available: ${PRINT_TARGETS.join(', ')}`);
|
|
197
|
+
return { exitCode: 1 };
|
|
198
|
+
}
|
|
199
|
+
log(printSnippet(target));
|
|
200
|
+
return { exitCode: 0 };
|
|
201
|
+
}
|
|
202
|
+
errlog(sub ? `✖ unknown hooks subcommand "${sub}"` : '✖ hooks needs a subcommand');
|
|
203
|
+
errlog(USAGE);
|
|
204
|
+
return { exitCode: 1 };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
cmdHooks,
|
|
209
|
+
mergeClaudeSettings,
|
|
210
|
+
isManagedHook,
|
|
211
|
+
refreshedHook,
|
|
212
|
+
HOOK_REL,
|
|
213
|
+
PRINT_TARGETS,
|
|
214
|
+
readTemplate
|
|
215
|
+
};
|