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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// One runner for "render these adapters into this project", shared by
|
|
2
|
+
// install, update, sync and hydrate. Per-adapter failures are isolated, the
|
|
3
|
+
// write guard is always cleared, and counts come from render-shared's write
|
|
4
|
+
// stats rather than from each adapter's own return value.
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const shared = require('./render-shared.js');
|
|
11
|
+
const { ALIAS_BY_TARGET } = require('./setup-helpers.js');
|
|
12
|
+
|
|
13
|
+
// Where each adapter drops its `wize-*` entries. A configured target whose
|
|
14
|
+
// directory has no top-level `wize-` entry, or lacks its `wize` alias
|
|
15
|
+
// entrypoint, has not been rendered in this checkout, whatever else lives there.
|
|
16
|
+
const SENTINEL_DIRS = {
|
|
17
|
+
'claude-code': '.claude/skills',
|
|
18
|
+
copilot: '.github/skills',
|
|
19
|
+
antigravity: '.agent/skills',
|
|
20
|
+
codex: '.agents/skills',
|
|
21
|
+
'kimi-code': '.kimi/skills',
|
|
22
|
+
hermes: '.hermes/skills',
|
|
23
|
+
kiro: '.kiro/skills',
|
|
24
|
+
cursor: '.cursor/rules',
|
|
25
|
+
windsurf: '.windsurf/rules',
|
|
26
|
+
continue: '.continue/prompts',
|
|
27
|
+
opencode: '.opencode/agents',
|
|
28
|
+
generic: '.wize/agents'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Directories an adapter writes besides its sentinel dir (personas and commands).
|
|
32
|
+
const EXTRA_OUTPUT_DIRS = {
|
|
33
|
+
copilot: ['.github/agents'],
|
|
34
|
+
opencode: ['.opencode/commands']
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Every path an adapter's render can overwrite: sentinel dir, extra dirs and aliases.
|
|
38
|
+
function outputSpecs(code) {
|
|
39
|
+
return [SENTINEL_DIRS[code], ...(EXTRA_OUTPUT_DIRS[code] || []), ...(ALIAS_BY_TARGET[code] || [])].filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasSentinel(projectRoot, code) {
|
|
43
|
+
const rel = SENTINEL_DIRS[code];
|
|
44
|
+
if (!rel) return true; // unknown (custom) target: nothing to check
|
|
45
|
+
try {
|
|
46
|
+
if (!fs.readdirSync(path.join(projectRoot, rel)).some(n => n.startsWith('wize-'))) return false;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return (ALIAS_BY_TARGET[code] || []).every(a => fs.existsSync(path.join(projectRoot, a)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function missingTargets(projectRoot, targets) {
|
|
54
|
+
return (targets || []).map(t => (typeof t === 'string' ? t : t && t.code)).filter(c => c && !hasSentinel(projectRoot, c));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const codeOf = (x) => (typeof x === 'string' ? x : x && x.code);
|
|
58
|
+
|
|
59
|
+
function renderTargets({ kitRoot, projectRoot, targets, profiles, guard = null, probe = false, skipAgentsMd = false, log = () => {} }) {
|
|
60
|
+
const profileCodes = (profiles || ['core']).map(codeOf);
|
|
61
|
+
const adapters = [];
|
|
62
|
+
if (guard) shared.setWriteGuard(guard);
|
|
63
|
+
shared.setProbeMode(probe);
|
|
64
|
+
try {
|
|
65
|
+
for (const target of targets || []) {
|
|
66
|
+
const code = codeOf(target);
|
|
67
|
+
const entry = { code, written: 0, unchanged: 0, skippedTracked: [] };
|
|
68
|
+
adapters.push(entry);
|
|
69
|
+
const renderPath = path.join(kitRoot, 'adapters', code, 'render.js');
|
|
70
|
+
if (!fs.existsSync(renderPath)) {
|
|
71
|
+
entry.skipped = true;
|
|
72
|
+
entry.reason = 'adapter missing';
|
|
73
|
+
log(` - ${code}: skipped (adapter missing in this kit version)`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
delete require.cache[require.resolve(renderPath)]; // keep repeated runs in one process honest
|
|
77
|
+
shared.resetWriteStats();
|
|
78
|
+
try {
|
|
79
|
+
const mod = require(renderPath);
|
|
80
|
+
if (typeof mod.render !== 'function') {
|
|
81
|
+
entry.skipped = true;
|
|
82
|
+
entry.reason = 'no render() export';
|
|
83
|
+
log(` - ${code}: skipped (no render() export)`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const out = mod.render(kitRoot, projectRoot, { profiles: profileCodes, skipAgentsMd });
|
|
87
|
+
if (!out || !Array.isArray(out.written)) {
|
|
88
|
+
entry.skipped = true;
|
|
89
|
+
entry.reason = 'stub (no skills emitted)';
|
|
90
|
+
log(` - ${code}: skipped (stub, no skills emitted)`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const stats = shared.getWriteStats();
|
|
94
|
+
entry.written = stats.written;
|
|
95
|
+
entry.unchanged = stats.unchanged;
|
|
96
|
+
entry.skippedTracked = stats.skippedTracked;
|
|
97
|
+
const tracked = entry.skippedTracked.length ? `, ${entry.skippedTracked.length} tracked left alone` : '';
|
|
98
|
+
log(` ✓ ${code}: ${entry.written} written, ${entry.unchanged} unchanged${tracked}`);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
entry.error = err.message;
|
|
101
|
+
log(` ✖ ${code}: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
shared.clearWriteGuard();
|
|
106
|
+
shared.setProbeMode(false);
|
|
107
|
+
shared.resetWriteStats();
|
|
108
|
+
}
|
|
109
|
+
return { ok: adapters.every(a => !a.error), adapters };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { renderTargets, SENTINEL_DIRS, EXTRA_OUTPUT_DIRS, outputSpecs, missingTargets };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Single semver implementation for the installer: parse, compare, greater.
|
|
2
|
+
// Prerelease-aware (0.27.0-rc.1 < 0.27.0); build metadata is ignored.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
7
|
+
|
|
8
|
+
// Returns { major, minor, patch, pre: [] } or null when `v` is not semver.
|
|
9
|
+
function parse(v) {
|
|
10
|
+
if (typeof v !== 'string') return null;
|
|
11
|
+
const m = SEMVER_RE.exec(v.trim().replace(/^v/i, ''));
|
|
12
|
+
if (!m) return null;
|
|
13
|
+
const pre = m[4] ? m[4].split('.').map(id => (/^\d+$/.test(id) ? Number(id) : id)) : [];
|
|
14
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function comparePre(a, b) {
|
|
18
|
+
if (!a.length && !b.length) return 0;
|
|
19
|
+
if (!a.length) return 1; // a release outranks any prerelease
|
|
20
|
+
if (!b.length) return -1;
|
|
21
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
22
|
+
if (i >= a.length) return -1;
|
|
23
|
+
if (i >= b.length) return 1;
|
|
24
|
+
const x = a[i];
|
|
25
|
+
const y = b[i];
|
|
26
|
+
if (x === y) continue;
|
|
27
|
+
const xn = typeof x === 'number';
|
|
28
|
+
const yn = typeof y === 'number';
|
|
29
|
+
if (xn && yn) return x < y ? -1 : 1;
|
|
30
|
+
if (xn) return -1; // numeric identifiers sort below alphanumeric
|
|
31
|
+
if (yn) return 1;
|
|
32
|
+
return x < y ? -1 : 1;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// -1 | 0 | 1. An unparseable argument counts as 0.0.0, so garbage never
|
|
38
|
+
// outranks a real version.
|
|
39
|
+
function compareVersions(a, b) {
|
|
40
|
+
const zero = { major: 0, minor: 0, patch: 0, pre: [] };
|
|
41
|
+
const pa = parse(a) || zero;
|
|
42
|
+
const pb = parse(b) || zero;
|
|
43
|
+
for (const k of ['major', 'minor', 'patch']) {
|
|
44
|
+
if (pa[k] !== pb[k]) return pa[k] < pb[k] ? -1 : 1;
|
|
45
|
+
}
|
|
46
|
+
return comparePre(pa.pre, pb.pre);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A pin that is safe to put in a command line: plain semver, no `v` prefix,
|
|
50
|
+
// whitespace or build metadata (same rule as .wize/hooks/hydrate.sh).
|
|
51
|
+
const SAFE_PIN_RE = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
|
|
52
|
+
const isSafePin = (v) => typeof v === 'string' && SAFE_PIN_RE.test(v);
|
|
53
|
+
const safePin = (v) => (isSafePin(v) ? v : null);
|
|
54
|
+
|
|
55
|
+
function semverGreater(a, b) {
|
|
56
|
+
return compareVersions(a, b) > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { parse, compareVersions, semverGreater, isSafePin, safePin };
|
|
@@ -12,6 +12,45 @@ const path = require('node:path');
|
|
|
12
12
|
const GITIGNORE_BEGIN = '# >>> wize-dev-kit (managed) >>>';
|
|
13
13
|
const GITIGNORE_END = '# <<< wize-dev-kit (managed) <<<';
|
|
14
14
|
|
|
15
|
+
// The 12 entrypoints for the `wize` alias skill/command each adapter emits,
|
|
16
|
+
// per adapter target. `wize-*` globs do not match them (no hyphen), so each gets
|
|
17
|
+
// an explicit line. Never widen to `wize*`: that would swallow user files such
|
|
18
|
+
// as wizebar.
|
|
19
|
+
const ALIAS_BY_TARGET = {
|
|
20
|
+
'claude-code': ['.claude/skills/wize/'],
|
|
21
|
+
copilot: ['.github/skills/wize/'],
|
|
22
|
+
antigravity: ['.agent/skills/wize/'],
|
|
23
|
+
codex: ['.agents/skills/wize/'],
|
|
24
|
+
'kimi-code': ['.kimi/skills/wize/'],
|
|
25
|
+
hermes: ['.hermes/skills/wize/'],
|
|
26
|
+
kiro: ['.kiro/skills/wize/'],
|
|
27
|
+
cursor: ['.cursor/rules/wize.mdc'],
|
|
28
|
+
windsurf: ['.windsurf/rules/wize.md'],
|
|
29
|
+
continue: ['.continue/prompts/wize.prompt'],
|
|
30
|
+
opencode: ['.opencode/commands/wize.md'],
|
|
31
|
+
generic: ['.wize/agents/wize.md']
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const ALIAS_PATHS = Object.values(ALIAS_BY_TARGET).flat();
|
|
35
|
+
|
|
36
|
+
// Generated outputs: the `wize-*` glob per location plus its explicit alias line.
|
|
37
|
+
const GENERATED_PATHS = [
|
|
38
|
+
{ dir: '.claude/skills/', glob: 'wize-*', alias: 'wize/' },
|
|
39
|
+
{ dir: '.github/skills/', glob: 'wize-*', alias: 'wize/' },
|
|
40
|
+
{ dir: '.github/agents/', glob: 'wize-*.agent.md', alias: null },
|
|
41
|
+
{ dir: '.agent/skills/', glob: 'wize-*', alias: 'wize/' },
|
|
42
|
+
{ dir: '.agents/skills/', glob: 'wize-*', alias: 'wize/' },
|
|
43
|
+
{ dir: '.kimi/skills/', glob: 'wize-*', alias: 'wize/' },
|
|
44
|
+
{ dir: '.hermes/skills/', glob: 'wize-*', alias: 'wize/' },
|
|
45
|
+
{ dir: '.kiro/skills/', glob: 'wize-*', alias: 'wize/' },
|
|
46
|
+
{ dir: '.cursor/rules/', glob: 'wize-*.mdc', alias: 'wize.mdc' },
|
|
47
|
+
{ dir: '.windsurf/rules/', glob: 'wize-*.md', alias: 'wize.md' },
|
|
48
|
+
{ dir: '.continue/prompts/', glob: 'wize-*.prompt', alias: 'wize.prompt' },
|
|
49
|
+
{ dir: '.opencode/agents/', glob: 'wize-*.md', alias: null },
|
|
50
|
+
{ dir: '.opencode/commands/', glob: 'wize-*.md', alias: 'wize.md' },
|
|
51
|
+
{ dir: '.wize/agents/', glob: 'wize-*.md', alias: 'wize.md' }
|
|
52
|
+
];
|
|
53
|
+
|
|
15
54
|
const GITIGNORE_BODY = [
|
|
16
55
|
'# Personal / per-developer (do NOT commit)',
|
|
17
56
|
'.wize/config/user.toml',
|
|
@@ -19,27 +58,17 @@ const GITIGNORE_BODY = [
|
|
|
19
58
|
'.wize/.local/',
|
|
20
59
|
'.wize/implementation/quick-dev-log.md',
|
|
21
60
|
'',
|
|
22
|
-
'# Generated IDE adapter outputs (regenerate with `npx wize-dev-kit
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'.
|
|
27
|
-
'.agents/skills/wize-*',
|
|
28
|
-
'.kimi/skills/wize-*',
|
|
29
|
-
'.hermes/skills/wize-*',
|
|
30
|
-
'.kiro/skills/wize-*',
|
|
31
|
-
'.cursor/rules/wize-*.mdc',
|
|
32
|
-
'.windsurf/rules/wize-*.md',
|
|
33
|
-
'.continue/prompts/wize-*.prompt',
|
|
34
|
-
'.opencode/agents/wize-*.md',
|
|
35
|
-
'.opencode/commands/wize-*.md',
|
|
36
|
-
'.wize/agents/wize-*.md'
|
|
61
|
+
'# Generated IDE adapter outputs (regenerate with `npx wize-dev-kit hydrate`)',
|
|
62
|
+
...GENERATED_PATHS.flatMap(g => [g.dir + g.glob, ...(g.alias ? [g.dir + g.alias] : [])]),
|
|
63
|
+
'',
|
|
64
|
+
'# Worktrees created inside the repo (Claude Code)',
|
|
65
|
+
'.claude/worktrees/'
|
|
37
66
|
].join('\n');
|
|
38
67
|
|
|
39
68
|
function buildGitignoreBlock() {
|
|
40
69
|
return [
|
|
41
70
|
GITIGNORE_BEGIN,
|
|
42
|
-
'# Managed by
|
|
71
|
+
'# Managed by wize-dev-kit (install/update). Re-running either updates',
|
|
43
72
|
'# the lines between the markers below; lines outside are untouched.',
|
|
44
73
|
'',
|
|
45
74
|
GITIGNORE_BODY,
|
|
@@ -47,35 +76,51 @@ function buildGitignoreBlock() {
|
|
|
47
76
|
].join('\n');
|
|
48
77
|
}
|
|
49
78
|
|
|
50
|
-
//
|
|
51
|
-
// Returns { changed
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
// Pure: the .gitignore text after the managed block is applied to `current`
|
|
80
|
+
// (null/'' for no file). Returns { next, changed, mode } with mode
|
|
81
|
+
// 'created'|'replaced'|'appended'|'unchanged'.
|
|
82
|
+
function computeGitignore(current) {
|
|
83
|
+
const exists = current !== null && current !== undefined;
|
|
84
|
+
const text = exists ? current : '';
|
|
54
85
|
const block = buildGitignoreBlock();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (exists) current = fs.readFileSync(file, 'utf-8');
|
|
58
|
-
|
|
59
|
-
const startIdx = current.indexOf(GITIGNORE_BEGIN);
|
|
60
|
-
const endIdx = current.indexOf(GITIGNORE_END);
|
|
86
|
+
const startIdx = text.indexOf(GITIGNORE_BEGIN);
|
|
87
|
+
const endIdx = text.indexOf(GITIGNORE_END);
|
|
61
88
|
let next;
|
|
62
89
|
let mode;
|
|
63
90
|
|
|
64
91
|
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
65
|
-
const before =
|
|
66
|
-
const after =
|
|
92
|
+
const before = text.slice(0, startIdx).replace(/\n+$/, '');
|
|
93
|
+
const after = text.slice(endIdx + GITIGNORE_END.length).replace(/^\n+/, '');
|
|
67
94
|
next = [before, block, after].filter(Boolean).join('\n\n');
|
|
68
95
|
if (!next.endsWith('\n')) next += '\n';
|
|
69
|
-
mode =
|
|
96
|
+
mode = text.includes(block) ? 'unchanged' : 'replaced';
|
|
70
97
|
} else if (exists) {
|
|
71
|
-
next =
|
|
98
|
+
next = text.replace(/\n+$/, '') + '\n\n' + block + '\n';
|
|
72
99
|
mode = 'appended';
|
|
73
100
|
} else {
|
|
74
101
|
next = block + '\n';
|
|
75
102
|
mode = 'created';
|
|
76
103
|
}
|
|
104
|
+
return { next, changed: next !== text, mode };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 'ok' | 'drift' | 'missing': whether the managed block in `current` (null
|
|
108
|
+
// for no file) is byte-for-byte what this kit would write.
|
|
109
|
+
function gitignoreBlockStatus(current) {
|
|
110
|
+
if (current === null || current === undefined) return 'missing';
|
|
111
|
+
const text = String(current).replace(/\r\n/g, '\n');
|
|
112
|
+
const startIdx = text.indexOf(GITIGNORE_BEGIN);
|
|
113
|
+
const endIdx = text.indexOf(GITIGNORE_END);
|
|
114
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return 'missing';
|
|
115
|
+
return text.slice(startIdx, endIdx + GITIGNORE_END.length) === buildGitignoreBlock() ? 'ok' : 'drift';
|
|
116
|
+
}
|
|
77
117
|
|
|
78
|
-
|
|
118
|
+
// Idempotent: if the block exists, replace it; otherwise append.
|
|
119
|
+
// Returns { changed: boolean, mode: 'created'|'replaced'|'appended'|'unchanged' }.
|
|
120
|
+
function applyGitignore(projectRoot, { dryRun = false } = {}) {
|
|
121
|
+
const file = path.join(projectRoot, '.gitignore');
|
|
122
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : null;
|
|
123
|
+
const { next, changed, mode } = computeGitignore(current);
|
|
79
124
|
if (changed && !dryRun) fs.writeFileSync(file, next, 'utf-8');
|
|
80
125
|
return { changed, mode };
|
|
81
126
|
}
|
|
@@ -100,10 +145,23 @@ ${roleLine}
|
|
|
100
145
|
`;
|
|
101
146
|
}
|
|
102
147
|
|
|
148
|
+
// user.toml content for a checkout that has no copy to inherit: the name comes
|
|
149
|
+
// from `git config user.name`, then $USER, then "Developer".
|
|
150
|
+
function deriveUserToml(root, env = process.env) {
|
|
151
|
+
const { configUserName } = require('./git-context.js');
|
|
152
|
+
const name = configUserName(root) || env.USER || env.USERNAME || 'Developer';
|
|
153
|
+
return generateUserToml({ name });
|
|
154
|
+
}
|
|
155
|
+
|
|
103
156
|
module.exports = {
|
|
157
|
+
deriveUserToml,
|
|
104
158
|
GITIGNORE_BEGIN,
|
|
105
159
|
GITIGNORE_END,
|
|
160
|
+
ALIAS_PATHS,
|
|
161
|
+
ALIAS_BY_TARGET,
|
|
106
162
|
buildGitignoreBlock,
|
|
163
|
+
computeGitignore,
|
|
164
|
+
gitignoreBlockStatus,
|
|
107
165
|
applyGitignore,
|
|
108
166
|
generateUserToml
|
|
109
167
|
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# wize-dev-kit managed hook v1
|
|
3
|
+
# Hydrates this checkout from .wize/config/project.toml without touching tracked
|
|
4
|
+
# files. `wize-dev-kit update` refreshes this file; local edits are overwritten.
|
|
5
|
+
# Always exits 0 so a failure never blocks worktree creation or a session start.
|
|
6
|
+
# Messages go to stderr: a Claude Code SessionStart hook adds stdout to the context.
|
|
7
|
+
|
|
8
|
+
# Same precedence as the CLI's resolveRoot: CLAUDE_PROJECT_DIR (the Claude session's
|
|
9
|
+
# own checkout), then the project found from the current directory (up to the git
|
|
10
|
+
# boundary), and only then ORCA_WORKTREE_PATH, which may be inherited from an Orca
|
|
11
|
+
# terminal of another worktree. The child hydrates this directory whatever else the
|
|
12
|
+
# environment says.
|
|
13
|
+
root=""
|
|
14
|
+
if [ -n "$CLAUDE_PROJECT_DIR" ]; then
|
|
15
|
+
root=$CLAUDE_PROJECT_DIR
|
|
16
|
+
else
|
|
17
|
+
# The walk ends at the first directory holding .git; with none above, only $PWD counts.
|
|
18
|
+
boundary=""
|
|
19
|
+
d=$PWD
|
|
20
|
+
while :; do
|
|
21
|
+
if [ -e "$d/.git" ]; then boundary=$d; break; fi
|
|
22
|
+
parent=$(dirname "$d")
|
|
23
|
+
[ "$parent" = "$d" ] && break
|
|
24
|
+
d=$parent
|
|
25
|
+
done
|
|
26
|
+
d=$PWD
|
|
27
|
+
while :; do
|
|
28
|
+
if [ -f "$d/.wize/config/project.toml" ]; then root=$d; break; fi
|
|
29
|
+
{ [ -z "$boundary" ] || [ "$d" = "$boundary" ]; } && break
|
|
30
|
+
d=$(dirname "$d")
|
|
31
|
+
done
|
|
32
|
+
[ -n "$root" ] || root=${ORCA_WORKTREE_PATH:-$PWD}
|
|
33
|
+
fi
|
|
34
|
+
cd "$root" || exit 0
|
|
35
|
+
WIZE_PROJECT_ROOT=$PWD
|
|
36
|
+
export WIZE_PROJECT_ROOT
|
|
37
|
+
toml=.wize/config/project.toml
|
|
38
|
+
[ -f "$toml" ] || { echo "wize: no $toml here, skipping" >&2; exit 0; }
|
|
39
|
+
|
|
40
|
+
pin=$(sed -n 's/^kit_version *= *"\([^"]*\)".*/\1/p' "$toml" | head -1)
|
|
41
|
+
# Anchored: a glob would let "1.2.3.4:host/repo" through, which npm reads as a git spec.
|
|
42
|
+
printf '%s' "$pin" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' || pin=""
|
|
43
|
+
pkg="wize-dev-kit@${pin:-latest}"
|
|
44
|
+
|
|
45
|
+
# Where the first configured target keeps its wize-* entries and its `wize`
|
|
46
|
+
# entrypoint (mirrors ALIAS_BY_TARGET in setup-helpers.js).
|
|
47
|
+
target=$(sed -n 's/^ide_targets *= *\[ *"\([^"]*\)".*/\1/p' "$toml" | head -1)
|
|
48
|
+
case "${target:-claude-code}" in
|
|
49
|
+
claude-code) dir=.claude/skills; alias=.claude/skills/wize ;;
|
|
50
|
+
copilot) dir=.github/skills; alias=.github/skills/wize ;;
|
|
51
|
+
antigravity) dir=.agent/skills; alias=.agent/skills/wize ;;
|
|
52
|
+
codex) dir=.agents/skills; alias=.agents/skills/wize ;;
|
|
53
|
+
kimi-code) dir=.kimi/skills; alias=.kimi/skills/wize ;;
|
|
54
|
+
hermes) dir=.hermes/skills; alias=.hermes/skills/wize ;;
|
|
55
|
+
kiro) dir=.kiro/skills; alias=.kiro/skills/wize ;;
|
|
56
|
+
cursor) dir=.cursor/rules; alias=.cursor/rules/wize.mdc ;;
|
|
57
|
+
windsurf) dir=.windsurf/rules; alias=.windsurf/rules/wize.md ;;
|
|
58
|
+
continue) dir=.continue/prompts; alias=.continue/prompts/wize.prompt ;;
|
|
59
|
+
opencode) dir=.opencode/agents; alias=.opencode/commands/wize.md ;;
|
|
60
|
+
generic) dir=.wize/agents; alias=.wize/agents/wize.md ;;
|
|
61
|
+
*) dir=""; alias="" ;;
|
|
62
|
+
esac
|
|
63
|
+
|
|
64
|
+
# True when `dir` holds wize-* entries and the `wize` entrypoint exists.
|
|
65
|
+
has_wize() {
|
|
66
|
+
[ -n "$dir" ] && [ -n "$alias" ] && [ -e "$alias" ] || return 1
|
|
67
|
+
for f in "$dir"/wize-*; do [ -e "$f" ] && return 0; done
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# hydrate ships in 0.27.0; older pins only know sync.
|
|
72
|
+
legacy=0
|
|
73
|
+
if [ -n "$pin" ] && [ "$(printf '%s\n' 0.27.0 "$pin" | sort -V | head -1)" != 0.27.0 ]; then legacy=1; fi
|
|
74
|
+
# sort -V ranks 0.27.0-rc.1 after 0.27.0; semver ranks it before.
|
|
75
|
+
case "$pin" in 0.27.0-*) legacy=1 ;; esac
|
|
76
|
+
|
|
77
|
+
# The pin a marker records: its "pin" when present, else "kit_version".
|
|
78
|
+
marker=.wize/.local/hydrated.json
|
|
79
|
+
marker_matches() {
|
|
80
|
+
[ -f "$marker" ] || return 1
|
|
81
|
+
# A fallback render (the pinned kit could not be run) is current for an hour only.
|
|
82
|
+
if grep -q '"fallback": *true' "$marker" 2>/dev/null && [ -z "$(find "$marker" -mmin -60 2>/dev/null)" ]; then return 1; fi
|
|
83
|
+
if grep -q '"pin": *"' "$marker" 2>/dev/null; then
|
|
84
|
+
grep -Fq "\"pin\": \"$pin\"" "$marker"
|
|
85
|
+
else
|
|
86
|
+
grep -Fq "\"kit_version\": \"$pin\"" "$marker"
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Fast path: already hydrated for this pin. No npx, no network. A legacy pin's
|
|
91
|
+
# own `sync` writes no marker and no user.toml, and an exact pin is immutable,
|
|
92
|
+
# so its files being present is enough (a marker, when there is one, must match).
|
|
93
|
+
if [ -n "$pin" ] && has_wize; then
|
|
94
|
+
if [ "$legacy" = 1 ]; then
|
|
95
|
+
if [ ! -f "$marker" ] || marker_matches; then exit 0; fi
|
|
96
|
+
elif [ -f .wize/config/user.toml ] && marker_matches; then
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# user.toml is per developer and git-ignored: copy it from the primary checkout.
|
|
102
|
+
common_dir() { (cd "$1" 2>/dev/null && d=$(git rev-parse --git-common-dir 2>/dev/null) && cd "$d" 2>/dev/null && pwd -P); }
|
|
103
|
+
primary=""
|
|
104
|
+
if [ -n "$ORCA_ROOT_PATH" ] && [ -n "$(common_dir .)" ] && [ "$(common_dir "$ORCA_ROOT_PATH")" = "$(common_dir .)" ]; then
|
|
105
|
+
primary=$ORCA_ROOT_PATH
|
|
106
|
+
fi
|
|
107
|
+
[ -n "$primary" ] || primary=$(git worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p')
|
|
108
|
+
if [ ! -f .wize/config/user.toml ] && [ -n "$primary" ] && [ -f "$primary/.wize/config/user.toml" ]; then
|
|
109
|
+
cp "$primary/.wize/config/user.toml" .wize/config/user.toml && echo "wize: user.toml copied from $primary" >&2
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
if [ "$legacy" = 1 ]; then
|
|
113
|
+
cmd=sync
|
|
114
|
+
quiet=""
|
|
115
|
+
else
|
|
116
|
+
cmd=hydrate
|
|
117
|
+
quiet="--quiet"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Offline, npx spends about 70 s revalidating the packument even for a cached
|
|
121
|
+
# version, which would outlast the 180 s SessionStart timeout over 3 attempts.
|
|
122
|
+
# An exact pin is immutable, so use the cache as is; npm's own retries are off
|
|
123
|
+
# because this loop retries, and each attempt is capped by `timeout` when it
|
|
124
|
+
# exists. Without `timeout`, npm's fetch timeout (20 s per request) bounds a
|
|
125
|
+
# refused or slow answer, but not a registry that silently drops packets.
|
|
126
|
+
offline=""
|
|
127
|
+
[ -n "$pin" ] && offline="--prefer-offline"
|
|
128
|
+
cap=""
|
|
129
|
+
command -v timeout >/dev/null 2>&1 && cap="timeout 55"
|
|
130
|
+
|
|
131
|
+
backoff="${WIZE_HOOK_BACKOFF_S:-2}"
|
|
132
|
+
errf=$(mktemp 2>/dev/null) || errf=""
|
|
133
|
+
ok=0
|
|
134
|
+
i=1
|
|
135
|
+
while [ "$i" -le 3 ]; do
|
|
136
|
+
# shellcheck disable=SC2086
|
|
137
|
+
if [ -n "$errf" ]; then
|
|
138
|
+
WIZE_SKIP_BASELINE=1 npm_config_fetch_retries=0 npm_config_fetch_timeout=20000 $cap npx --yes $offline "$pkg" "$cmd" $quiet >&2 2>"$errf"
|
|
139
|
+
rc=$?
|
|
140
|
+
cat "$errf" >&2
|
|
141
|
+
else
|
|
142
|
+
WIZE_SKIP_BASELINE=1 npm_config_fetch_retries=0 npm_config_fetch_timeout=20000 $cap npx --yes $offline "$pkg" "$cmd" $quiet >&2
|
|
143
|
+
rc=$?
|
|
144
|
+
fi
|
|
145
|
+
if [ "$rc" = 0 ]; then ok=1; break; fi
|
|
146
|
+
# ETARGET / notarget: the pin is not on the registry; retrying gets the same answer.
|
|
147
|
+
if [ -n "$errf" ] && grep -Eq 'ETARGET|notarget' "$errf" 2>/dev/null; then break; fi
|
|
148
|
+
# 124: `timeout` killed the attempt (a network that drops packets); retrying only waits again.
|
|
149
|
+
[ "$rc" = 124 ] && break
|
|
150
|
+
[ "$i" -lt 3 ] && sleep $((i * backoff))
|
|
151
|
+
i=$((i + 1))
|
|
152
|
+
done
|
|
153
|
+
[ -z "$errf" ] || rm -f "$errf"
|
|
154
|
+
[ "$ok" = 1 ] || echo "wize: $cmd failed; run: npx $pkg $cmd" >&2
|
|
155
|
+
|
|
156
|
+
# hydrate prints its own update notice; older pins get one from the registry.
|
|
157
|
+
if [ "$cmd" = sync ] && [ "$WIZE_DISABLE_UPDATE_CHECK" != 1 ] && command -v npm >/dev/null 2>&1; then
|
|
158
|
+
if command -v timeout >/dev/null 2>&1; then latest=$(timeout 5 npm view wize-dev-kit version 2>/dev/null); else latest=""; fi
|
|
159
|
+
if [ -n "$latest" ] && [ "$latest" != "$pin" ]; then
|
|
160
|
+
echo "wize: project pinned to $pin, latest is $latest (update on the default branch: npx wize-dev-kit@latest update)" >&2
|
|
161
|
+
fi
|
|
162
|
+
fi
|
|
163
|
+
exit 0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Managed by `wize-dev-kit hooks install --github-action`; safe to edit.
|
|
2
|
+
# Opens a pull request that bumps the wize-dev-kit pin once a week.
|
|
3
|
+
#
|
|
4
|
+
# Pull requests created with GITHUB_TOKEN do not trigger other workflows, so
|
|
5
|
+
# required checks will not run on them until someone pushes to the branch.
|
|
6
|
+
# The repository setting "Allow GitHub Actions to create and approve pull
|
|
7
|
+
# requests" (Settings > Actions > General) must be enabled.
|
|
8
|
+
name: Wize kit update
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
schedule:
|
|
12
|
+
- cron: '17 6 * * 1'
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
pull-requests: write
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
update:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
with:
|
|
25
|
+
fetch-depth: 0
|
|
26
|
+
- uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: 20
|
|
29
|
+
- name: Set git identity
|
|
30
|
+
run: |
|
|
31
|
+
git config user.name "github-actions[bot]"
|
|
32
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
33
|
+
- name: Prepare the kit update PR
|
|
34
|
+
env:
|
|
35
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
36
|
+
BASE_BRANCH: ${{ github.event.repository.default_branch }}
|
|
37
|
+
# A fresh checkout has no origin/HEAD, so name the default branch explicitly.
|
|
38
|
+
run: npx --yes wize-dev-kit@latest update --pr --base "origin/$BASE_BRANCH"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Minimal TOML reader/writer for the subset the kit emits: `[section]`
|
|
2
|
+
// headers, `key = "value"` scalars and `key = ["a", "b"]` string arrays.
|
|
3
|
+
// The writer is surgical: it edits one string value in place and verifies the
|
|
4
|
+
// result, so a caller can never report a rewrite that did not happen.
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
|
|
11
|
+
// Drops a trailing `# comment`, but only when the `#` sits outside a string.
|
|
12
|
+
function stripComment(line) {
|
|
13
|
+
let quote = null;
|
|
14
|
+
for (let i = 0; i < line.length; i++) {
|
|
15
|
+
const ch = line[i];
|
|
16
|
+
if (quote === '"') {
|
|
17
|
+
if (ch === '\\') i++;
|
|
18
|
+
else if (ch === '"') quote = null;
|
|
19
|
+
} else if (quote === "'") {
|
|
20
|
+
if (ch === "'") quote = null;
|
|
21
|
+
} else if (ch === '"' || ch === "'") {
|
|
22
|
+
quote = ch;
|
|
23
|
+
} else if (ch === '#') {
|
|
24
|
+
return line.slice(0, i);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return line;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseTomlValue(raw) {
|
|
31
|
+
if (raw.startsWith('"') && raw.endsWith('"')) return raw.slice(1, -1);
|
|
32
|
+
if (raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
|
|
33
|
+
if (raw.startsWith('[') && raw.endsWith(']')) {
|
|
34
|
+
return raw.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
if (raw === 'true') return true;
|
|
37
|
+
if (raw === 'false') return false;
|
|
38
|
+
if (!isNaN(Number(raw))) return Number(raw);
|
|
39
|
+
return raw;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseToml(content) {
|
|
43
|
+
const out = {};
|
|
44
|
+
let section = null;
|
|
45
|
+
for (const raw of content.split('\n')) {
|
|
46
|
+
const line = stripComment(raw).trim();
|
|
47
|
+
if (!line) continue;
|
|
48
|
+
const head = line.match(/^\[([^\]]+)\]$/);
|
|
49
|
+
if (head) { section = head[1]; out[section] = out[section] || {}; continue; }
|
|
50
|
+
const kv = line.match(/^([a-zA-Z_][\w-]*)\s*=\s*(.+)$/);
|
|
51
|
+
if (!kv) continue;
|
|
52
|
+
const [, key, valRaw] = kv;
|
|
53
|
+
const v = parseTomlValue(valRaw.trim());
|
|
54
|
+
if (section) out[section][key] = v;
|
|
55
|
+
else out[key] = v;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readToml(file) {
|
|
61
|
+
let content;
|
|
62
|
+
try { content = fs.readFileSync(file, 'utf-8'); } catch { return {}; }
|
|
63
|
+
return parseToml(content);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadProjectConfig(projectRoot) {
|
|
67
|
+
return readToml(path.join(projectRoot, '.wize/config/project.toml'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function escapeRe(s) {
|
|
71
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Pure rewrite of `key = "value"` inside `[section]`: keeps a trailing
|
|
75
|
+
// comment, inserts the key right after the header when absent. Returns the new
|
|
76
|
+
// text (identical to `content` when nothing changes), or null when the value
|
|
77
|
+
// is unsafe to write, the section is missing, the key holds a non-string
|
|
78
|
+
// value, or the result does not parse back to the new value.
|
|
79
|
+
function rewriteTomlValue(content, section, key, value) {
|
|
80
|
+
const val = String(value);
|
|
81
|
+
if (/["\\\r\n]/.test(val)) return null;
|
|
82
|
+
|
|
83
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
|
84
|
+
const lines = content.split('\n');
|
|
85
|
+
const headRe = new RegExp('^\\s*\\[' + escapeRe(section) + '\\]\\s*(#.*)?\\r?$');
|
|
86
|
+
const start = lines.findIndex(l => headRe.test(l));
|
|
87
|
+
if (start === -1) return null;
|
|
88
|
+
let end = lines.length;
|
|
89
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
90
|
+
if (/^\s*\[/.test(lines[i])) { end = i; break; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const keyRe = new RegExp('^\\s*' + escapeRe(key) + '\\s*=');
|
|
94
|
+
const strRe = new RegExp('^(\\s*' + escapeRe(key) + '\\s*=\\s*)"[^"]*"(\\s*(#.*)?)$');
|
|
95
|
+
let idx = -1;
|
|
96
|
+
for (let i = start + 1; i < end; i++) {
|
|
97
|
+
if (keyRe.test(lines[i])) { idx = i; break; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (idx === -1) {
|
|
101
|
+
lines.splice(start + 1, 0, `${key} = "${val}"` + (eol === '\r\n' ? '\r' : ''));
|
|
102
|
+
} else {
|
|
103
|
+
if (!strRe.test(lines[idx])) return null;
|
|
104
|
+
lines[idx] = lines[idx].replace(strRe, (_, pre, post) => `${pre}"${val}"${post}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const next = lines.join('\n');
|
|
108
|
+
const parsed = parseToml(next);
|
|
109
|
+
return parsed[section] && parsed[section][key] === val ? next : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Sets `key = "value"` inside `[section]` on disk. Returns false when the
|
|
113
|
+
// rewrite is refused or the re-read file does not carry the new value.
|
|
114
|
+
function setTomlValue(file, section, key, value) {
|
|
115
|
+
let content;
|
|
116
|
+
try { content = fs.readFileSync(file, 'utf-8'); } catch { return false; }
|
|
117
|
+
const next = rewriteTomlValue(content, section, key, value);
|
|
118
|
+
if (next === null) return false;
|
|
119
|
+
if (next !== content) {
|
|
120
|
+
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
121
|
+
try {
|
|
122
|
+
fs.writeFileSync(tmp, next, 'utf-8');
|
|
123
|
+
fs.renameSync(tmp, file);
|
|
124
|
+
} catch {
|
|
125
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* ignore */ }
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const parsed = readToml(file);
|
|
130
|
+
return !!(parsed[section] && parsed[section][key] === String(value));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { readToml, parseToml, loadProjectConfig, rewriteTomlValue, setTomlValue };
|