wize-dev-kit 0.26.0 → 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 +70 -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/src/security-overlay/skills/wize-sec-recon/scripts/run-recon.js +4 -4
- package/src/security-overlay/skills/wize-sec-recon/skill.md +1 -1
- 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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Pure decisions behind `wize-dev-kit install` / `uninstall`: where the kit
|
|
2
|
+
// may be installed, what an existing project.toml means for a re-run, how the
|
|
3
|
+
// project and user names are derived and when the baseline runs. wize-cli.js
|
|
4
|
+
// runs main() at load time, so everything testable lives here.
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const { gitContext, configUserName } = require('./git-context.js');
|
|
11
|
+
const { findProjectRoot, findKitElsewhere } = require('./project-root.js');
|
|
12
|
+
const { loadProjectConfig } = require('./toml.js');
|
|
13
|
+
const { real, sameSet } = require('./util.js');
|
|
14
|
+
|
|
15
|
+
const hasProjectToml = (dir) => fs.existsSync(path.join(dir, '.wize', 'config', 'project.toml'));
|
|
16
|
+
|
|
17
|
+
// -> { status, ctx, installed, root?, toplevel?, message? }
|
|
18
|
+
// installed cwd already holds project.toml
|
|
19
|
+
// nested an ancestor (inside the git checkout) holds it
|
|
20
|
+
// subdir cwd is inside a git checkout but is not its top level
|
|
21
|
+
// primary linked worktree whose branch lacks project.toml while the
|
|
22
|
+
// default-branch (or any other non-bare) worktree has it (merge the kit, or pass --here)
|
|
23
|
+
// ok fresh install at cwd
|
|
24
|
+
// `--here` skips the nested, subdir and primary checks.
|
|
25
|
+
function checkInstallRoot({ cwd, flags = {} }) {
|
|
26
|
+
const here = real(cwd);
|
|
27
|
+
const ctx = gitContext(here);
|
|
28
|
+
if (hasProjectToml(here)) return { status: 'installed', installed: true, ctx, root: here };
|
|
29
|
+
if (flags.here) return { status: 'ok', installed: false, ctx };
|
|
30
|
+
|
|
31
|
+
const found = findProjectRoot(here);
|
|
32
|
+
if (found && found !== here) {
|
|
33
|
+
return { status: 'nested', installed: false, ctx, root: found, message: `kit already installed at ${found}; run from there` };
|
|
34
|
+
}
|
|
35
|
+
const primary = findKitElsewhere(here, ctx);
|
|
36
|
+
if (primary) {
|
|
37
|
+
return {
|
|
38
|
+
status: 'primary', installed: false, ctx, root: primary,
|
|
39
|
+
message: `kit is installed on ${primary}'s branch; merge it into this branch, or pass --here to install here`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (ctx.isRepo && ctx.toplevel && real(ctx.toplevel) !== here) {
|
|
43
|
+
return {
|
|
44
|
+
status: 'subdir', installed: false, ctx, toplevel: real(ctx.toplevel),
|
|
45
|
+
message: `run install from the repo root ${real(ctx.toplevel)} (or pass --here)`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { status: 'ok', installed: false, ctx };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const csv = (v) => (typeof v === 'string' ? v.split(',').map(s => s.trim()).filter(Boolean) : []);
|
|
52
|
+
|
|
53
|
+
// Flags that would change what an installed project is configured for.
|
|
54
|
+
// Returns the list of conflicting flag names (empty = nothing to reject).
|
|
55
|
+
function conflictingFlags({ flags = {}, root }) {
|
|
56
|
+
let cfg;
|
|
57
|
+
try { cfg = loadProjectConfig(root); } catch { cfg = {}; }
|
|
58
|
+
const install = (cfg && cfg.install) || {};
|
|
59
|
+
const out = [];
|
|
60
|
+
if (typeof flags.profiles === 'string') {
|
|
61
|
+
const want = [...csv(flags.profiles), 'core'];
|
|
62
|
+
if (!sameSet(want, [...(install.profiles || ['core']), 'core'])) out.push('--profiles');
|
|
63
|
+
}
|
|
64
|
+
if (typeof flags.targets === 'string') {
|
|
65
|
+
if (!sameSet(csv(flags.targets), install.ide_targets || [])) out.push('--targets');
|
|
66
|
+
}
|
|
67
|
+
if (typeof flags.lang === 'string') {
|
|
68
|
+
const lang = cfg && cfg.language && cfg.language.communication;
|
|
69
|
+
if (flags.lang.trim() !== lang) out.push('--lang');
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function installedPin(root) {
|
|
75
|
+
try {
|
|
76
|
+
const cfg = loadProjectConfig(root);
|
|
77
|
+
return (cfg.project && cfg.project.kit_version) || 'unknown';
|
|
78
|
+
} catch { return 'unknown'; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default project name: the primary checkout's name in a linked worktree (the
|
|
82
|
+
// worktree directory is a throwaway), else the directory being installed.
|
|
83
|
+
function defaultProjectName({ cwd, ctx }) {
|
|
84
|
+
const c = ctx || gitContext(cwd);
|
|
85
|
+
if (c.isRepo && c.linked && c.primary) return path.basename(c.primary);
|
|
86
|
+
return path.basename(real(cwd));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const clean = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
|
90
|
+
|
|
91
|
+
// specific flag > legacy --name > derived
|
|
92
|
+
function resolveProjectName({ flags = {}, cwd, ctx }) {
|
|
93
|
+
return clean(flags['project-name']) || clean(flags.name) || defaultProjectName({ cwd, ctx });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// specific flag > legacy --name > git user.name > $USER > Developer. Without
|
|
97
|
+
// --yes and without a flag the caller prompts, so this returns null.
|
|
98
|
+
function resolveUserName({ flags = {}, cwd, env = process.env, nonInteractive = false }) {
|
|
99
|
+
const given = clean(flags['user-name']) || clean(flags.name);
|
|
100
|
+
if (given) return given;
|
|
101
|
+
if (!nonInteractive) return null;
|
|
102
|
+
return configUserName(cwd) || env.USER || env.USERNAME || 'Developer';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function baselineDocsExist(root) {
|
|
106
|
+
try {
|
|
107
|
+
return fs.readdirSync(path.join(root, '.wize', 'knowledge', 'document-project')).some(f => f.endsWith('.md'));
|
|
108
|
+
} catch { return false; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// -> { run, reason }. WIZE_SKIP_BASELINE=1 always wins; an existing baseline
|
|
112
|
+
// blocks the run unless --baseline forces it; otherwise brownfield only.
|
|
113
|
+
function baselineDecision({ root, flags = {}, env = process.env, brownfield }) {
|
|
114
|
+
if (env.WIZE_SKIP_BASELINE === '1') return { run: false, reason: 'env' };
|
|
115
|
+
if (flags.baseline) return { run: true, reason: 'forced' };
|
|
116
|
+
if (!brownfield) return { run: false, reason: 'greenfield' };
|
|
117
|
+
if (baselineDocsExist(root)) return { run: false, reason: 'exists' };
|
|
118
|
+
return { run: true, reason: 'brownfield' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Uninstall target: an explicit --root / WIZE_PROJECT_ROOT, else the project
|
|
122
|
+
// root found from cwd (cwd itself when none). `refused` when cwd is not the
|
|
123
|
+
// root and nothing explicit was given.
|
|
124
|
+
function resolveUninstallRoot({ cwd, flags = {}, env = process.env }) {
|
|
125
|
+
const here = real(cwd);
|
|
126
|
+
const explicit = clean(flags.root) || clean(env.WIZE_PROJECT_ROOT);
|
|
127
|
+
if (explicit) return { root: real(path.resolve(here, explicit)), explicit: true, refused: false };
|
|
128
|
+
const found = findProjectRoot(here);
|
|
129
|
+
const root = found || here;
|
|
130
|
+
return { root, explicit: false, refused: root !== here };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
checkInstallRoot, conflictingFlags, installedPin, defaultProjectName,
|
|
135
|
+
resolveProjectName, resolveUserName, baselineDecision, resolveUninstallRoot
|
|
136
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Hydration marker: .wize/.local/hydrated.json records which kit version
|
|
2
|
+
// last rendered this checkout. .wize/.local/ is git-ignored, so each worktree
|
|
3
|
+
// owns its own marker. Reads never throw.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
|
|
10
|
+
const SCHEMA = 1;
|
|
11
|
+
|
|
12
|
+
function markerPath(root) {
|
|
13
|
+
return path.join(root, '.wize', '.local', 'hydrated.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readMarker(root) {
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(fs.readFileSync(markerPath(root), 'utf-8'));
|
|
19
|
+
return data && typeof data === 'object' && !Array.isArray(data) ? data : null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The pin a render targeted. Fallback renders record it next to the binary that
|
|
26
|
+
// did the work, so a pin the binary cannot match does not read as stale. Older
|
|
27
|
+
// markers carry only kit_version.
|
|
28
|
+
const markerPin = (marker) => (typeof marker.pin === 'string' ? marker.pin : marker.kit_version);
|
|
29
|
+
|
|
30
|
+
// A fallback render stands in for the pinned kit while it cannot be run (offline,
|
|
31
|
+
// unpublished). It counts as current for FALLBACK_TTL_MS, then the next
|
|
32
|
+
// hydrate tries the exact pin again.
|
|
33
|
+
const FALLBACK_TTL_MS = 60 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
function fallbackExpired(marker, now = Date.now()) {
|
|
36
|
+
if (!marker || marker.fallback !== true) return false;
|
|
37
|
+
const at = Date.parse(marker.rendered_at);
|
|
38
|
+
return !Number.isFinite(at) || now - at > FALLBACK_TTL_MS;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeMarker(root, { kit_version, pin, targets, profiles, counts, delegated, rendered_with, fallback } = {}) {
|
|
42
|
+
const file = markerPath(root);
|
|
43
|
+
const payload = {
|
|
44
|
+
schema: SCHEMA,
|
|
45
|
+
kit_version: kit_version ?? null,
|
|
46
|
+
pin: pin ?? null,
|
|
47
|
+
targets: targets || [],
|
|
48
|
+
profiles: profiles || [],
|
|
49
|
+
counts: counts || {},
|
|
50
|
+
delegated: !!delegated,
|
|
51
|
+
rendered_with: rendered_with ?? null,
|
|
52
|
+
...(fallback ? { fallback: true } : {}),
|
|
53
|
+
rendered_at: new Date().toISOString()
|
|
54
|
+
};
|
|
55
|
+
const tmp = path.join(path.dirname(file), `.hydrated.json.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
56
|
+
try {
|
|
57
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
58
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
|
|
59
|
+
fs.renameSync(tmp, file);
|
|
60
|
+
return payload;
|
|
61
|
+
} catch {
|
|
62
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* ignore */ }
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { markerPath, readMarker, writeMarker, markerPin, fallbackExpired, FALLBACK_TTL_MS };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Project-root discovery. A command run from a subdirectory or from a linked
|
|
2
|
+
// worktree must land on the directory that owns `.wize/config/project.toml`,
|
|
3
|
+
// without ever climbing out of the enclosing git checkout.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const { real } = require('./util.js');
|
|
10
|
+
const { worktreeList, defaultBranch } = require('./git-context.js');
|
|
11
|
+
|
|
12
|
+
const PROJECT_TOML = path.join('.wize', 'config', 'project.toml');
|
|
13
|
+
|
|
14
|
+
function exists(p) {
|
|
15
|
+
try { fs.statSync(p); return true; } catch { return false; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Walks up from `start` and returns the first directory holding
|
|
19
|
+
// .wize/config/project.toml. The walk ends at the first directory that
|
|
20
|
+
// contains `.git` (file or dir); with no `.git` anywhere above, only `start`
|
|
21
|
+
// is checked, so the walk never reaches $HOME.
|
|
22
|
+
function findProjectRoot(start) {
|
|
23
|
+
const first = real(start);
|
|
24
|
+
const chain = [];
|
|
25
|
+
let boundary = null;
|
|
26
|
+
for (let dir = first; ; dir = path.dirname(dir)) {
|
|
27
|
+
chain.push(dir);
|
|
28
|
+
if (exists(path.join(dir, '.git'))) { boundary = dir; break; }
|
|
29
|
+
if (path.dirname(dir) === dir) break;
|
|
30
|
+
}
|
|
31
|
+
const candidates = boundary ? chain : [first];
|
|
32
|
+
for (const dir of candidates) {
|
|
33
|
+
if (exists(path.join(dir, PROJECT_TOML))) return dir;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// In a linked worktree whose branch lacks the kit, the other checkout that has
|
|
39
|
+
// it: the default-branch worktree first, else any non-bare worktree (a bare
|
|
40
|
+
// repository + worktrees layout has no primary checkout). `ctx` is the
|
|
41
|
+
// gitContext of `here`. Null when this is not a linked worktree or none has it.
|
|
42
|
+
function findKitElsewhere(here, ctx) {
|
|
43
|
+
if (!ctx || !ctx.isRepo || !ctx.linked) return null;
|
|
44
|
+
const self = real(ctx.toplevel || here);
|
|
45
|
+
const list = worktreeList(here) || [];
|
|
46
|
+
const def = defaultBranch(here);
|
|
47
|
+
const others = list.filter(w => !w.bare && real(w.path) !== self && real(w.path) !== real(here));
|
|
48
|
+
others.sort((a, b) => Number(b.branch === def) - Number(a.branch === def));
|
|
49
|
+
for (const w of others) {
|
|
50
|
+
if (exists(path.join(real(w.path), PROJECT_TOML))) return real(w.path);
|
|
51
|
+
}
|
|
52
|
+
if (!list.length && ctx.primary && real(ctx.primary) !== self && exists(path.join(real(ctx.primary), PROJECT_TOML))) return real(ctx.primary);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Precedence: --root > WIZE_PROJECT_ROOT > findProjectRoot(cwd) > (hydrate
|
|
57
|
+
// only) ORCA_WORKTREE_PATH > cwd. A project found from cwd beats an inherited
|
|
58
|
+
// ORCA_WORKTREE_PATH, so a shell in worktree B never renders worktree A.
|
|
59
|
+
function resolveRoot({ flags = {}, env = process.env, cwd = process.cwd(), forCommand = null } = {}) {
|
|
60
|
+
const pick = (v) => (typeof v === 'string' && v.trim() ? real(path.resolve(cwd, v.trim())) : null);
|
|
61
|
+
return pick(flags.root)
|
|
62
|
+
|| pick(env.WIZE_PROJECT_ROOT)
|
|
63
|
+
|| findProjectRoot(cwd)
|
|
64
|
+
|| (forCommand === 'hydrate' ? pick(env.ORCA_WORKTREE_PATH) : null)
|
|
65
|
+
|| real(cwd);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { findProjectRoot, findKitElsewhere, resolveRoot };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Local project status for `version-check` and the CLI hints: what this
|
|
2
|
+
// checkout is pinned to, whether it is hydrated, how that compares with the
|
|
3
|
+
// running kit and the registry, and the one action to take next. Reads files
|
|
4
|
+
// and git only; the registry answer comes in as `latest`.
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const { loadProjectConfig } = require('./toml.js');
|
|
11
|
+
const { compareVersions, parse, isSafePin } = require('./semver.js');
|
|
12
|
+
const { findProjectRoot, findKitElsewhere } = require('./project-root.js');
|
|
13
|
+
const { real } = require('./util.js');
|
|
14
|
+
const { gitContext, defaultBranch } = require('./git-context.js');
|
|
15
|
+
const { missingTargets } = require('./render-targets.js');
|
|
16
|
+
const { readMarker, markerPin, fallbackExpired } = require('./marker.js');
|
|
17
|
+
|
|
18
|
+
// First kit version that ships `hydrate`; older pins only know `sync`.
|
|
19
|
+
const HYDRATE_MIN = '0.27.0';
|
|
20
|
+
|
|
21
|
+
const ACTION_EXIT_CODES = { none: 0, hydrate: 10, update: 11, 'use-pin': 12, install: 13 };
|
|
22
|
+
|
|
23
|
+
function kitVersionOf(kitRoot) {
|
|
24
|
+
try { return require(path.join(kitRoot, 'package.json')).version; }
|
|
25
|
+
catch { return null; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function supportsHydrate(pin) {
|
|
29
|
+
return isSafePin(pin) && !!parse(pin) && compareVersions(pin, HYDRATE_MIN) >= 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hydrateCommand(pin) {
|
|
33
|
+
if (!isSafePin(pin)) return 'npx --yes wize-dev-kit hydrate';
|
|
34
|
+
return supportsHydrate(pin)
|
|
35
|
+
? `npx --yes wize-dev-kit@${pin} hydrate`
|
|
36
|
+
: `npx --yes wize-dev-kit@${pin} sync`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function gitFacts(root) {
|
|
40
|
+
const ctx = gitContext(root);
|
|
41
|
+
return {
|
|
42
|
+
repo: ctx.isRepo,
|
|
43
|
+
linked: ctx.linked,
|
|
44
|
+
primary: ctx.primary,
|
|
45
|
+
branch: ctx.branch,
|
|
46
|
+
detached: ctx.isRepo ? ctx.detached : false,
|
|
47
|
+
default_branch: ctx.isRepo ? defaultBranch(root) : null
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// `latestSource` says where `latest` came from. Only a registry answer fetched
|
|
52
|
+
// just now (`'network'`) can prove a pin is unpublished: a cached answer may be
|
|
53
|
+
// an hour old, or older offline, and a newer release would read as a downgrade.
|
|
54
|
+
// `published` is the packument's list of every published version; a pin above
|
|
55
|
+
// `latest` is unpublished only when that list is known and lacks it (a
|
|
56
|
+
// prerelease published under another dist-tag is ahead of `latest` too).
|
|
57
|
+
function getProjectStatus({ kitRoot, cwd = process.cwd(), latest = null, latestSource = null, published = null, env = process.env } = {}) {
|
|
58
|
+
const installed = kitVersionOf(kitRoot);
|
|
59
|
+
const root = findProjectRoot(cwd);
|
|
60
|
+
const commands = {
|
|
61
|
+
hydrate: hydrateCommand(null),
|
|
62
|
+
update: 'npx --yes wize-dev-kit@latest update',
|
|
63
|
+
pr: 'npx --yes wize-dev-kit@latest update --pr',
|
|
64
|
+
install: 'npx --yes wize-dev-kit@latest install'
|
|
65
|
+
};
|
|
66
|
+
const base = {
|
|
67
|
+
root,
|
|
68
|
+
pin: null,
|
|
69
|
+
installed,
|
|
70
|
+
latest: latest || null,
|
|
71
|
+
pin_status: 'missing',
|
|
72
|
+
hydrated: 'ok',
|
|
73
|
+
missing_targets: [],
|
|
74
|
+
marker_version: null,
|
|
75
|
+
user_toml: 'missing',
|
|
76
|
+
git: gitFacts(root || cwd),
|
|
77
|
+
skew: 'none',
|
|
78
|
+
action: 'none',
|
|
79
|
+
reason: '',
|
|
80
|
+
commands
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (!root) {
|
|
84
|
+
// A linked worktree whose branch predates the kit install: installing would
|
|
85
|
+
// fork the pin, so the answer is to merge, not to install.
|
|
86
|
+
const kitAt = findKitElsewhere(real(cwd), gitContext(real(cwd)));
|
|
87
|
+
if (kitAt) return { ...base, action: 'none', reason: 'kit-not-on-branch', kit_at: kitAt, commands: { ...commands, install: `${commands.install} --here` } };
|
|
88
|
+
return { ...base, action: 'install', reason: 'no-project', pin_status: 'missing', user_toml: 'missing' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const cfg = loadProjectConfig(root);
|
|
92
|
+
const pin = (cfg.project && typeof cfg.project.kit_version === 'string' && cfg.project.kit_version) || null;
|
|
93
|
+
const targets = (cfg.install && cfg.install.ide_targets) || ['claude-code', 'generic'];
|
|
94
|
+
const marker = readMarker(root);
|
|
95
|
+
const missing = missingTargets(root, targets);
|
|
96
|
+
const userToml = fs.existsSync(path.join(root, '.wize', 'config', 'user.toml')) ? 'present' : 'missing';
|
|
97
|
+
const known = !!latest;
|
|
98
|
+
|
|
99
|
+
let pinStatus = 'ok';
|
|
100
|
+
if (!pin) pinStatus = 'missing';
|
|
101
|
+
else if (!isSafePin(pin)) pinStatus = 'invalid';
|
|
102
|
+
else if (installed && compareVersions(pin, installed) > 0) {
|
|
103
|
+
const proven = known && latestSource === 'network' && compareVersions(pin, latest) > 0
|
|
104
|
+
&& Array.isArray(published) && !published.includes(pin);
|
|
105
|
+
pinStatus = proven ? 'unpublished' : 'ahead-of-binary';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let hydrated = 'ok';
|
|
109
|
+
if (missing.length) hydrated = 'missing';
|
|
110
|
+
else if (supportsHydrate(pin) && marker && (markerPin(marker) !== pin || fallbackExpired(marker))) hydrated = 'stale';
|
|
111
|
+
|
|
112
|
+
let skew = 'none';
|
|
113
|
+
if (pin && installed) {
|
|
114
|
+
const cmp = compareVersions(pin, installed);
|
|
115
|
+
if (cmp < 0) skew = 'project-behind';
|
|
116
|
+
else if (cmp > 0) skew = 'project-ahead';
|
|
117
|
+
else if (known && compareVersions(latest, pin) > 0) skew = 'registry-ahead';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
commands.hydrate = hydrateCommand(pin);
|
|
121
|
+
|
|
122
|
+
let action = 'none';
|
|
123
|
+
let reason = 'up-to-date';
|
|
124
|
+
if (hydrated !== 'ok') {
|
|
125
|
+
action = 'hydrate';
|
|
126
|
+
reason = hydrated === 'missing' ? 'unhydrated' : 'stale-marker';
|
|
127
|
+
} else if (userToml === 'missing' && supportsHydrate(pin)) {
|
|
128
|
+
action = 'hydrate';
|
|
129
|
+
reason = 'user-toml-missing';
|
|
130
|
+
} else if (pinStatus === 'unpublished') {
|
|
131
|
+
action = 'update';
|
|
132
|
+
reason = 'pin-unpublished';
|
|
133
|
+
commands.update = 'npx --yes wize-dev-kit@latest update --allow-downgrade';
|
|
134
|
+
} else if (pinStatus === 'ahead-of-binary') {
|
|
135
|
+
action = 'use-pin';
|
|
136
|
+
reason = 'pin-ahead-of-binary';
|
|
137
|
+
} else if (pinStatus === 'missing' || pinStatus === 'invalid') {
|
|
138
|
+
action = 'update';
|
|
139
|
+
reason = pinStatus === 'missing' ? 'pin-missing' : 'pin-invalid';
|
|
140
|
+
} else if (skew === 'project-behind') {
|
|
141
|
+
action = 'update';
|
|
142
|
+
reason = 'project-behind';
|
|
143
|
+
} else if (skew === 'registry-ahead') {
|
|
144
|
+
action = 'update';
|
|
145
|
+
reason = 'registry-ahead';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// WIZE_DISABLE_UPDATE_CHECK=1 is the opt-out of auto-update: the skew stays
|
|
149
|
+
// visible, the update action does not.
|
|
150
|
+
if (env.WIZE_DISABLE_UPDATE_CHECK === '1' && action === 'update') {
|
|
151
|
+
action = 'none';
|
|
152
|
+
reason = 'update-disabled';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
...base,
|
|
157
|
+
root,
|
|
158
|
+
pin,
|
|
159
|
+
pin_status: pinStatus,
|
|
160
|
+
hydrated,
|
|
161
|
+
missing_targets: missing,
|
|
162
|
+
marker_version: marker ? marker.kit_version ?? null : null,
|
|
163
|
+
user_toml: userToml,
|
|
164
|
+
skew,
|
|
165
|
+
action,
|
|
166
|
+
reason,
|
|
167
|
+
commands
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { getProjectStatus, kitVersionOf, hydrateCommand, supportsHydrate, ACTION_EXIT_CODES, HYDRATE_MIN };
|
|
@@ -105,10 +105,100 @@ function clipOneLine(str, max = 240) {
|
|
|
105
105
|
return flat.slice(0, max - 1).trimEnd() + '…';
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// Where `dir` would be created: the nearest existing ancestor resolved through
|
|
109
|
+
// symlinks, plus the part that does not exist yet.
|
|
110
|
+
function landingDir(dir) {
|
|
111
|
+
const rest = [];
|
|
112
|
+
let cur = path.resolve(dir);
|
|
113
|
+
for (;;) {
|
|
114
|
+
try { return path.join(fs.realpathSync(cur), ...rest.reverse()); } catch { /* not there yet */ }
|
|
115
|
+
const parent = path.dirname(cur);
|
|
116
|
+
if (parent === cur) return path.resolve(dir);
|
|
117
|
+
rest.push(path.basename(cur));
|
|
118
|
+
cur = parent;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// With a write guard set, a directory the guard vetoes is not created, so a
|
|
123
|
+
// symlinked directory cannot be filled with empty folders behind the guard.
|
|
108
124
|
function ensureDir(dir) {
|
|
125
|
+
if (probeOnly) return;
|
|
126
|
+
if (writeGuard && (writeGuard(path.resolve(dir)) || writeGuard(landingDir(dir)))) return;
|
|
109
127
|
fs.mkdirSync(dir, { recursive: true });
|
|
110
128
|
}
|
|
111
129
|
|
|
130
|
+
// ---- Write safety -------------------------------------------------------
|
|
131
|
+
// Every file a renderer emits goes through writeFileIfChanged /
|
|
132
|
+
// copyFileIfChanged: identical bytes are left alone (mtime untouched), writes
|
|
133
|
+
// are atomic (tmp + rename, safe for concurrent hydrates of one tree), and an
|
|
134
|
+
// optional guard can veto a path (hydrate uses it to never touch tracked files).
|
|
135
|
+
let writeGuard = null;
|
|
136
|
+
// Probe mode: every write still goes through the guard (so vetoes are
|
|
137
|
+
// recorded) but nothing touches the disk.
|
|
138
|
+
let probeOnly = false;
|
|
139
|
+
const writeStats = { written: 0, unchanged: 0, skippedTracked: [] };
|
|
140
|
+
|
|
141
|
+
function setWriteGuard(fn) { writeGuard = typeof fn === 'function' ? fn : null; }
|
|
142
|
+
function clearWriteGuard() { writeGuard = null; }
|
|
143
|
+
function setProbeMode(on) { probeOnly = !!on; }
|
|
144
|
+
function getWriteStats() {
|
|
145
|
+
return { written: writeStats.written, unchanged: writeStats.unchanged, skippedTracked: writeStats.skippedTracked.slice() };
|
|
146
|
+
}
|
|
147
|
+
function resetWriteStats() {
|
|
148
|
+
writeStats.written = 0;
|
|
149
|
+
writeStats.unchanged = 0;
|
|
150
|
+
writeStats.skippedTracked = [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function atomicWrite(abs, buf, mode) {
|
|
154
|
+
const tmp = path.join(path.dirname(abs), `.${path.basename(abs)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
155
|
+
try {
|
|
156
|
+
fs.writeFileSync(tmp, buf);
|
|
157
|
+
if (mode !== undefined) fs.chmodSync(tmp, mode);
|
|
158
|
+
fs.renameSync(tmp, abs);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* ignore */ }
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The path a write really lands on: the parent directory is resolved through
|
|
166
|
+
// symlinks, so a tracked `.claude/skills -> ../skills` link cannot hide the
|
|
167
|
+
// tracked file behind it from the guard.
|
|
168
|
+
function landingPath(abs) {
|
|
169
|
+
return path.join(landingDir(path.dirname(abs)), path.basename(abs));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function emit(file, buf, mode) {
|
|
173
|
+
const abs = path.resolve(file);
|
|
174
|
+
if (writeGuard && (writeGuard(abs) || writeGuard(landingPath(abs)))) {
|
|
175
|
+
writeStats.skippedTracked.push(abs);
|
|
176
|
+
return 'skipped-tracked';
|
|
177
|
+
}
|
|
178
|
+
if (probeOnly) {
|
|
179
|
+
writeStats.unchanged++;
|
|
180
|
+
return 'unchanged';
|
|
181
|
+
}
|
|
182
|
+
let existing = null;
|
|
183
|
+
try { existing = fs.readFileSync(abs); } catch { /* absent */ }
|
|
184
|
+
if (existing && existing.equals(buf)) {
|
|
185
|
+
writeStats.unchanged++;
|
|
186
|
+
return 'unchanged';
|
|
187
|
+
}
|
|
188
|
+
atomicWrite(abs, buf, mode);
|
|
189
|
+
writeStats.written++;
|
|
190
|
+
return 'written';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// -> 'written' | 'unchanged' | 'skipped-tracked'
|
|
194
|
+
function writeFileIfChanged(file, content) {
|
|
195
|
+
return emit(file, Buffer.isBuffer(content) ? content : Buffer.from(String(content), 'utf-8'));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function copyFileIfChanged(src, dst) {
|
|
199
|
+
return emit(dst, fs.readFileSync(src), fs.statSync(src).mode & 0o777);
|
|
200
|
+
}
|
|
201
|
+
|
|
112
202
|
// Companion files (steps/, templates/, data/, *.csv, *-template.md,
|
|
113
203
|
// customize.toml, research.template.md, etc.) that micro-file workflows
|
|
114
204
|
// reference from their body. We must copy these alongside the SKILL.md so
|
|
@@ -139,7 +229,7 @@ function copyCompanionFiles(srcDir, destDir, written, relPrefix = '') {
|
|
|
139
229
|
copyCompanionFiles(s, d, written, rel);
|
|
140
230
|
} else if (entry.isFile()) {
|
|
141
231
|
ensureDir(path.dirname(d));
|
|
142
|
-
|
|
232
|
+
copyFileIfChanged(s, d);
|
|
143
233
|
written.push(rel);
|
|
144
234
|
}
|
|
145
235
|
}
|
|
@@ -244,7 +334,7 @@ function renderAnthropicSkills(kitRoot, targetDir, opts = {}) {
|
|
|
244
334
|
} else {
|
|
245
335
|
const skillDir = path.join(targetDir, a.code);
|
|
246
336
|
ensureDir(skillDir);
|
|
247
|
-
|
|
337
|
+
writeFileIfChanged(path.join(skillDir, 'SKILL.md'), content);
|
|
248
338
|
written.push(`${a.code}/SKILL.md`);
|
|
249
339
|
// Copy companion files (steps/, templates/, data/, *.csv, customize.toml,
|
|
250
340
|
// *-template.md, research.template.md, etc.) so micro-file workflows can
|
|
@@ -272,7 +362,69 @@ const AGENT_ORDER = [
|
|
|
272
362
|
'wize-sec-red-teamer'
|
|
273
363
|
];
|
|
274
364
|
|
|
365
|
+
// ---- managed bootstrap block ---------------------------------------------------
|
|
366
|
+
//
|
|
367
|
+
// AGENTS.md is the one tracked file that Codex, Cursor, Windsurf, Antigravity
|
|
368
|
+
// and others read at session start. The block below tells the agent to run
|
|
369
|
+
// `version-check --json` and follow its `action`. The text names no version,
|
|
370
|
+
// so `update` only rewrites it when the wording itself changes.
|
|
371
|
+
|
|
372
|
+
const AGENTS_TITLE = '# Agents — Wize Development Kit';
|
|
373
|
+
const BOOTSTRAP_BEGIN = '<!-- wize:bootstrap:begin (managed by wize-dev-kit; edits inside are replaced by update) -->';
|
|
374
|
+
const BOOTSTRAP_END = '<!-- wize:bootstrap:end -->';
|
|
375
|
+
const BOOTSTRAP_BEGIN_RE = /<!-- wize:bootstrap:begin[^\n]*?-->/;
|
|
376
|
+
|
|
377
|
+
const BOOTSTRAP_LINES = [
|
|
378
|
+
BOOTSTRAP_BEGIN,
|
|
379
|
+
'## Kit bootstrap',
|
|
380
|
+
'',
|
|
381
|
+
'Once per session, if you can run shell commands, run `npx --yes --fetch-retries=0 wize-dev-kit version-check --json`',
|
|
382
|
+
'(prefix it with `timeout 20` where that exists) and follow its `action` field (if npx fails or times out,',
|
|
383
|
+
'for example offline, skip this):',
|
|
384
|
+
'',
|
|
385
|
+
'- `hydrate`: run `commands.hydrate` now. It writes only git-ignored files. If the `/wize-*`',
|
|
386
|
+
' commands are still missing afterwards, tell the user to restart the harness.',
|
|
387
|
+
'- `update`: run `commands.update`. It prepares a `chore/wize-kit-<version>` branch in a temporary',
|
|
388
|
+
' worktree and never touches this checkout. Then ask once before running `commands.pr`, which',
|
|
389
|
+
' pushes the branch and opens a PR. Exception: when `reason` is `pin-unpublished`, `commands.update`',
|
|
390
|
+
' lowers the project pin; ask the user first and run it only on a yes.',
|
|
391
|
+
'- `use-pin`: tell the user to run kit commands as `npx wize-dev-kit@<pin>`, with `<pin>` taken from',
|
|
392
|
+
' `project.pin` in the JSON.',
|
|
393
|
+
'- `none` with `reason` `kit-not-on-branch`: tell the user to merge the kit into this branch, or install here with `install --here`.',
|
|
394
|
+
'- `install`, other `none`: do nothing.',
|
|
395
|
+
'',
|
|
396
|
+
'If the `/wize-*` commands are missing, run `npx --yes wize-dev-kit hydrate`.',
|
|
397
|
+
'Never commit a kit update on the default branch.',
|
|
398
|
+
BOOTSTRAP_END
|
|
399
|
+
];
|
|
400
|
+
const BOOTSTRAP_BLOCK = BOOTSTRAP_LINES.join('\n');
|
|
401
|
+
|
|
402
|
+
// True only for an AGENTS.md this kit generated. A foreign file is never edited.
|
|
403
|
+
function isKitAgentsMd(content) {
|
|
404
|
+
if (typeof content !== 'string') return false;
|
|
405
|
+
return content.replace(/^/, '').split(/\r?\n/, 1)[0] === AGENTS_TITLE;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Pure: returns `content` with exactly one managed bootstrap block. An existing
|
|
409
|
+
// block is replaced in place; otherwise the block goes before the first `## `
|
|
410
|
+
// heading, or at the end when there is none. A half-written block (one marker
|
|
411
|
+
// without the other) is left alone rather than guessed at.
|
|
412
|
+
function upsertBootstrapBlock(content) {
|
|
413
|
+
const begin = BOOTSTRAP_BEGIN_RE.exec(content);
|
|
414
|
+
const endIdx = content.indexOf(BOOTSTRAP_END);
|
|
415
|
+
if (begin || endIdx !== -1) {
|
|
416
|
+
if (!begin || endIdx === -1 || endIdx < begin.index) return content;
|
|
417
|
+
return content.slice(0, begin.index) + BOOTSTRAP_BLOCK + content.slice(endIdx + BOOTSTRAP_END.length);
|
|
418
|
+
}
|
|
419
|
+
const heading = /^## /m.exec(content);
|
|
420
|
+
if (heading) {
|
|
421
|
+
return content.slice(0, heading.index) + BOOTSTRAP_BLOCK + '\n\n' + content.slice(heading.index);
|
|
422
|
+
}
|
|
423
|
+
return content.replace(/\s*$/, '') + '\n\n' + BOOTSTRAP_BLOCK + '\n';
|
|
424
|
+
}
|
|
425
|
+
|
|
275
426
|
function renderAgentsMd(kitRoot, projectRoot, opts = {}) {
|
|
427
|
+
if (opts.skipAgentsMd) return { written: [], skipped: ['AGENTS.md (skipAgentsMd)'] };
|
|
276
428
|
const assets = collectAssets(kitRoot, opts);
|
|
277
429
|
const rank = (code) => {
|
|
278
430
|
const i = AGENT_ORDER.indexOf(code);
|
|
@@ -286,12 +438,14 @@ function renderAgentsMd(kitRoot, projectRoot, opts = {}) {
|
|
|
286
438
|
return { written: [], skipped: ['AGENTS.md (already present; not overwritten)'] };
|
|
287
439
|
}
|
|
288
440
|
const lines = [
|
|
289
|
-
|
|
441
|
+
AGENTS_TITLE,
|
|
290
442
|
'',
|
|
291
443
|
'This repo is wired with the [`wize-dev-kit`](https://www.npmjs.com/package/wize-dev-kit).',
|
|
292
444
|
'Detailed artifacts live under `.wize/`. The agents below are activated through your AI IDE',
|
|
293
445
|
'using slash commands (Claude Code, Codex, Cursor, Windsurf, Antigravity, Hermes all read this file).',
|
|
294
446
|
'',
|
|
447
|
+
...BOOTSTRAP_LINES,
|
|
448
|
+
'',
|
|
295
449
|
'## Operating context',
|
|
296
450
|
'',
|
|
297
451
|
'Treat `.wize/`, this `AGENTS.md`, and the installed `wize-*` skills as your operating',
|
|
@@ -351,7 +505,7 @@ function renderAgentsMd(kitRoot, projectRoot, opts = {}) {
|
|
|
351
505
|
lines.push('', '## Where to start', '', 'Activate the orchestrator: `/wize` (or `/wize-help`). Wizer will guide you.', '');
|
|
352
506
|
|
|
353
507
|
if (opts.dryRun) return { written: ['[dry-run] AGENTS.md'], skipped: [] };
|
|
354
|
-
|
|
508
|
+
writeFileIfChanged(file, lines.join('\n'));
|
|
355
509
|
return { written: ['AGENTS.md'], skipped: [] };
|
|
356
510
|
}
|
|
357
511
|
|
|
@@ -359,7 +513,19 @@ module.exports = {
|
|
|
359
513
|
collectAssets,
|
|
360
514
|
renderAnthropicSkills,
|
|
361
515
|
renderAgentsMd,
|
|
516
|
+
upsertBootstrapBlock,
|
|
517
|
+
isKitAgentsMd,
|
|
518
|
+
BOOTSTRAP_BEGIN,
|
|
519
|
+
BOOTSTRAP_END,
|
|
520
|
+
BOOTSTRAP_BLOCK,
|
|
362
521
|
copyCompanionFiles,
|
|
522
|
+
writeFileIfChanged,
|
|
523
|
+
copyFileIfChanged,
|
|
524
|
+
setWriteGuard,
|
|
525
|
+
clearWriteGuard,
|
|
526
|
+
setProbeMode,
|
|
527
|
+
getWriteStats,
|
|
528
|
+
resetWriteStats,
|
|
363
529
|
// Re-exports for adapter authors who need to roll their own format:
|
|
364
530
|
readFrontmatter,
|
|
365
531
|
bodyAfterFrontmatter,
|