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.
Files changed (71) hide show
  1. package/ARCH.md +2 -1
  2. package/CHANGELOG.md +70 -0
  3. package/README.es.md +28 -6
  4. package/README.md +28 -6
  5. package/README.pt-BR.md +28 -6
  6. package/adapters/continue/render.js +2 -3
  7. package/adapters/copilot/render.js +3 -3
  8. package/adapters/cursor/render.js +2 -3
  9. package/adapters/generic/render.js +2 -3
  10. package/adapters/opencode/render.js +3 -4
  11. package/adapters/windsurf/render.js +2 -3
  12. package/docs/WORKTREES.md +150 -0
  13. package/docs/WORKTREES.pt-BR.md +150 -0
  14. package/docs/harnesses/antigravity.md +2 -0
  15. package/docs/harnesses/antigravity.pt-BR.md +2 -0
  16. package/docs/harnesses/claude-code.md +2 -0
  17. package/docs/harnesses/claude-code.pt-BR.md +2 -0
  18. package/docs/harnesses/codex.md +3 -1
  19. package/docs/harnesses/codex.pt-BR.md +3 -1
  20. package/docs/harnesses/continue.md +2 -0
  21. package/docs/harnesses/continue.pt-BR.md +2 -0
  22. package/docs/harnesses/copilot.md +2 -0
  23. package/docs/harnesses/copilot.pt-BR.md +2 -0
  24. package/docs/harnesses/cursor.md +2 -0
  25. package/docs/harnesses/cursor.pt-BR.md +2 -0
  26. package/docs/harnesses/generic.md +2 -0
  27. package/docs/harnesses/generic.pt-BR.md +2 -0
  28. package/docs/harnesses/hermes.md +2 -0
  29. package/docs/harnesses/hermes.pt-BR.md +2 -0
  30. package/docs/harnesses/kimi-code.md +2 -0
  31. package/docs/harnesses/kimi-code.pt-BR.md +2 -0
  32. package/docs/harnesses/kiro.md +2 -0
  33. package/docs/harnesses/kiro.pt-BR.md +2 -0
  34. package/docs/harnesses/opencode.md +2 -0
  35. package/docs/harnesses/opencode.pt-BR.md +2 -0
  36. package/docs/harnesses/windsurf.md +2 -0
  37. package/docs/harnesses/windsurf.pt-BR.md +2 -0
  38. package/package.json +1 -1
  39. package/src/orchestrator-skills/module.yaml +1 -1
  40. package/src/orchestrator-skills/wize/skill.md +17 -7
  41. package/src/orchestrator-skills/wize-help/skill.md +20 -7
  42. package/src/orchestrator-skills/wize-update/skill.md +33 -27
  43. package/src/security-overlay/skills/wize-sec-recon/scripts/run-recon.js +4 -4
  44. package/src/security-overlay/skills/wize-sec-recon/skill.md +1 -1
  45. package/tools/installer/commands/doctor.js +186 -40
  46. package/tools/installer/commands/document-project.js +13 -3
  47. package/tools/installer/commands/hooks.js +215 -0
  48. package/tools/installer/commands/hydrate.js +298 -0
  49. package/tools/installer/commands/sync.js +50 -23
  50. package/tools/installer/commands/update.js +596 -120
  51. package/tools/installer/commands/version-check.js +69 -10
  52. package/tools/installer/document-project/modes/full-rescan.js +1 -1
  53. package/tools/installer/document-project/modes/initial-scan.js +5 -4
  54. package/tools/installer/document-project/modes/quick.js +11 -4
  55. package/tools/installer/git-context.js +219 -0
  56. package/tools/installer/git-ops.js +211 -0
  57. package/tools/installer/install-plan.js +136 -0
  58. package/tools/installer/marker.js +67 -0
  59. package/tools/installer/project-root.js +68 -0
  60. package/tools/installer/project-status.js +171 -0
  61. package/tools/installer/render-shared.js +170 -4
  62. package/tools/installer/render-targets.js +112 -0
  63. package/tools/installer/semver.js +59 -0
  64. package/tools/installer/setup-helpers.js +89 -31
  65. package/tools/installer/templates/hydrate.sh +163 -0
  66. package/tools/installer/templates/wize-kit-update.yml +38 -0
  67. package/tools/installer/toml.js +133 -0
  68. package/tools/installer/util.js +16 -0
  69. package/tools/installer/version-check.js +167 -37
  70. package/tools/installer/wize-cli.js +216 -78
  71. package/tools/installer/sync.js +0 -29
@@ -0,0 +1,298 @@
1
+ // `wize-dev-kit hydrate` — render this checkout from .wize/config/project.toml.
2
+ //
3
+ // Built for fresh worktrees (Orca, Claude Code, Codex, Cursor): no prompts,
4
+ // never reads stdin, never runs the baseline, never writes tracked files
5
+ // (project.toml, .gitignore, AGENTS.md, tea.toml, .wize/custom, artifacts,
6
+ // knowledge) and never runs a mutating git command. Always exits 0 so a
7
+ // failure cannot block worktree creation; --strict turns ok:false into 1.
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const { spawnSync } = require('node:child_process');
14
+ const { loadProjectConfig } = require('../toml.js');
15
+ const { compareVersions, semverGreater, parse, safePin } = require('../semver.js');
16
+ const { gitContext, trackedSet, statusPorcelain } = require('../git-context.js');
17
+ const { renderTargets, missingTargets } = require('../render-targets.js');
18
+ const { readMarker, writeMarker, markerPin, fallbackExpired } = require('../marker.js');
19
+ const { deriveUserToml } = require('../setup-helpers.js');
20
+ const { getLatestNoWait, isCI } = require('../version-check.js');
21
+ const { HYDRATE_MIN, supportsHydrate } = require('../project-status.js');
22
+ const { real, sameSet } = require('../util.js');
23
+
24
+ const DELEGATE_ATTEMPTS = 2;
25
+ // Per attempt. A dropped network never fails fast (npm's fetch timeout does not
26
+ // cap the TCP connect), so a killed attempt is not retried; the cold-cache race
27
+ // that the second attempt absorbs fails quickly. Two cold attempts stay well
28
+ // under the 180 s SessionStart budget.
29
+ const DELEGATE_TIMEOUT_MS = 45000;
30
+ const DELEGATE_BACKOFF_MS = 2000;
31
+
32
+ const isFile = (p) => { try { return fs.statSync(p).isFile(); } catch { return false; } };
33
+ const lexists = (p) => { try { fs.lstatSync(p); return true; } catch { return false; } };
34
+
35
+ function sleepSync(ms) {
36
+ if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
37
+ }
38
+
39
+ // user.toml is per-developer and git-ignored, so each checkout needs its own.
40
+ // Never overwrites. Sources in order: $ORCA_ROOT_PATH (only when it belongs to
41
+ // this repository), the primary checkout, then a file derived from git/$USER.
42
+ // -> 'present' | 'copied:<src>' | 'generated' | 'failed'
43
+ function ensureUserToml({ root, ctx, env }) {
44
+ const dest = path.join(root, '.wize', 'config', 'user.toml');
45
+ if (fs.existsSync(dest)) return 'present';
46
+
47
+ const candidates = [];
48
+ if (env.ORCA_ROOT_PATH && ctx.commonDir) {
49
+ const orcaRoot = real(env.ORCA_ROOT_PATH);
50
+ const theirs = gitContext(orcaRoot);
51
+ if (theirs.isRepo && theirs.commonDir && real(theirs.commonDir) === real(ctx.commonDir)) candidates.push(orcaRoot);
52
+ }
53
+ if (ctx.primary && real(ctx.primary) !== real(root)) candidates.push(ctx.primary);
54
+
55
+ try {
56
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
57
+ for (const dir of candidates) {
58
+ const src = path.join(dir, '.wize', 'config', 'user.toml');
59
+ if (!isFile(src) || real(src) === real(dest)) continue;
60
+ try {
61
+ fs.copyFileSync(src, dest, fs.constants.COPYFILE_EXCL);
62
+ return `copied:${src}`;
63
+ } catch (err) {
64
+ if (err.code === 'EEXIST') return 'present';
65
+ }
66
+ }
67
+ fs.writeFileSync(dest, deriveUserToml(root, env), { encoding: 'utf-8', flag: 'wx' });
68
+ return 'generated';
69
+ } catch (err) {
70
+ return err.code === 'EEXIST' ? 'present' : 'failed';
71
+ }
72
+ }
73
+
74
+ // No --root: the child runs with cwd = root, and a path with spaces or shell
75
+ // metacharacters would be split by cmd.exe on Windows (shell: true). --strict
76
+ // is always on, so a child that rendered nothing is not counted as success.
77
+ function forwardedFlags(flags) {
78
+ const out = ['--strict'];
79
+ for (const f of ['json', 'quiet', 'if-needed', 'no-notice']) if (flags[f]) out.push(`--${f}`);
80
+ return out;
81
+ }
82
+
83
+ // Runs `npx wize-dev-kit@<pin> hydrate` so the checkout renders exactly the
84
+ // pinned kit. Two attempts absorb the cold-cache ENOTEMPTY race. An exact
85
+ // version is immutable, so a cached copy is used without revalidating the
86
+ // packument (offline, revalidation alone costs about 70 s), and npm's own fetch
87
+ // retries are off because the loop retries.
88
+ function delegate({ pin, root, flags, env, spawn, backoffMs, warn }) {
89
+ const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
90
+ const args = ['--yes', '--prefer-offline', `wize-dev-kit@${pin}`, 'hydrate', ...forwardedFlags(flags)];
91
+ for (let attempt = 1; attempt <= DELEGATE_ATTEMPTS; attempt++) {
92
+ const r = spawn(npx, args, {
93
+ cwd: root,
94
+ env: {
95
+ npm_config_fetch_retries: '0',
96
+ npm_config_fetch_timeout: '20000',
97
+ ...env,
98
+ // The child must render this root, not whatever ORCA_WORKTREE_PATH names.
99
+ WIZE_PROJECT_ROOT: root,
100
+ WIZE_HYDRATE_DELEGATED: '1', WIZE_SKIP_BASELINE: '1', WIZE_DISABLE_UPDATE_CHECK: '1'
101
+ },
102
+ timeout: DELEGATE_TIMEOUT_MS,
103
+ // --json needs exactly one object on stdout, so keep the child's output
104
+ // until it is known to have succeeded. stderr is held back too: a failed
105
+ // attempt would print npm's raw ETARGET/ENOTFOUND block once per attempt,
106
+ // and the caller prints one line instead (WIZE_VERBOSE=1 shows it all).
107
+ stdio: ['ignore', flags.json ? 'pipe' : 'inherit', env.WIZE_VERBOSE === '1' ? 'inherit' : 'pipe'],
108
+ encoding: 'utf-8',
109
+ shell: process.platform === 'win32'
110
+ });
111
+ if (r && !r.error && r.status === 0) {
112
+ if (typeof r.stderr === 'string' && r.stderr) warn(r.stderr.trimEnd());
113
+ return { ok: true, stdout: typeof r.stdout === 'string' ? r.stdout : '' };
114
+ }
115
+ const timedOut = !!(r && ((r.error && r.error.code === 'ETIMEDOUT') || (r.signal && !r.error)));
116
+ if (timedOut) break;
117
+ if (attempt < DELEGATE_ATTEMPTS) sleepSync(backoffMs);
118
+ }
119
+ return { ok: false };
120
+ }
121
+
122
+ function cmdHydrate({ kitRoot, binary, projectRoot, flags = {}, env = process.env, log = console.log, warn = console.error, deps = {} } = {}) {
123
+ const spawn = deps.spawnSync || spawnSync;
124
+ const latestNoWait = deps.getLatestNoWait || getLatestNoWait;
125
+ const backoffMs = Number.isFinite(Number(env.WIZE_HYDRATE_BACKOFF_MS)) && env.WIZE_HYDRATE_BACKOFF_MS !== undefined
126
+ ? Number(env.WIZE_HYDRATE_BACKOFF_MS) : DELEGATE_BACKOFF_MS;
127
+ const root = projectRoot;
128
+ const result = {
129
+ ok: true, root, linked: false, primary: null, pin: null, binary,
130
+ delegated: false, user_toml: null, adapters: [], warnings: [], notice: null
131
+ };
132
+ const say = (line) => { if (!flags.json) log(line); };
133
+ const chatter = (line) => { if (!flags.json && !flags.quiet) log(line); };
134
+ const warning = (line) => { result.warnings.push(line); warn(line); };
135
+ const emit = () => {
136
+ if (flags.json) log(JSON.stringify(result));
137
+ result.exitCode = flags.strict && !result.ok ? 1 : 0;
138
+ return result;
139
+ };
140
+
141
+ try {
142
+ if (!isFile(path.join(root, '.wize', 'config', 'project.toml'))) {
143
+ const bare = gitContext(root);
144
+ result.linked = bare.linked;
145
+ result.primary = bare.primary;
146
+ say('wize: no .wize/config/project.toml here, skipping');
147
+ // A delegated child that finds nothing to render must not read as success.
148
+ if (env.WIZE_HYDRATE_DELEGATED) result.ok = false;
149
+ return emit();
150
+ }
151
+
152
+ const cfg = loadProjectConfig(root);
153
+ const targets = (cfg.install && cfg.install.ide_targets) || ['claude-code', 'generic'];
154
+ const profiles = (cfg.install && cfg.install.profiles) || ['core'];
155
+ const pin = (cfg.project && typeof cfg.project.kit_version === 'string' && cfg.project.kit_version) || null;
156
+ result.pin = pin;
157
+
158
+ const ctx = gitContext(root);
159
+ result.linked = ctx.linked;
160
+ result.primary = ctx.primary;
161
+
162
+ if (flags['if-needed']) {
163
+ const marker = readMarker(root);
164
+ const userToml = fs.existsSync(path.join(root, '.wize', 'config', 'user.toml'));
165
+ if (pin && marker && markerPin(marker) === pin && !fallbackExpired(marker)
166
+ && sameSet(marker.targets || [], targets) && sameSet(marker.profiles || [], profiles)
167
+ && userToml && missingTargets(root, targets).length === 0) {
168
+ result.user_toml = 'present';
169
+ result.adapters = [];
170
+ if (!flags.quiet) say(`wize: already hydrated (${pin})`);
171
+ return emit();
172
+ }
173
+ }
174
+
175
+ const before = statusPorcelain(root);
176
+
177
+ result.user_toml = ensureUserToml({ root, ctx, env });
178
+ if (result.user_toml === 'failed') warning('wize: could not create .wize/config/user.toml; copy it from your primary checkout');
179
+
180
+ const canDelegate = supportsHydrate(pin) && pin !== binary
181
+ && !flags['no-delegate'] && !env.WIZE_HYDRATE_DELEGATED;
182
+ if (pin && !parse(pin)) {
183
+ warning(`wize: kit_version "${pin}" is not a version; rendering with ${binary}`);
184
+ } else if (pin && compareVersions(pin, HYDRATE_MIN) < 0) {
185
+ warning(`wize: pin ${pin} predates hydrate; adopt it with: npx wize-dev-kit@latest update`);
186
+ } else if (!pin) {
187
+ warning('wize: kit_version missing in project.toml; run: npx wize-dev-kit@latest update');
188
+ }
189
+
190
+ let delegated = false;
191
+ if (canDelegate) {
192
+ const d = delegate({ pin, root, flags, env, spawn, backoffMs, warn });
193
+ if (d.ok) {
194
+ delegated = true;
195
+ result.delegated = true;
196
+ if (flags.json) {
197
+ try {
198
+ const child = JSON.parse(d.stdout.trim());
199
+ if (Array.isArray(child.adapters)) result.adapters = child.adapters;
200
+ if (child.ok === false) result.ok = false;
201
+ if (Array.isArray(child.warnings)) result.warnings.push(...child.warnings);
202
+ } catch {
203
+ result.warnings.push('wize: delegated hydrate finished but its JSON output was unreadable');
204
+ }
205
+ }
206
+ } else {
207
+ warning(`wize: could not run kit ${pin} (offline or unpublished); rendering with ${binary}`);
208
+ }
209
+ }
210
+
211
+ if (!delegated) {
212
+ // Fail closed: when git cannot say what is tracked (missing binary,
213
+ // "dubious ownership"), no existing file is overwritten.
214
+ const tracked = ctx.isRepo ? trackedSet(root) : new Set();
215
+ const failClosed = tracked === null;
216
+ // A write that lands outside the project (a symlinked directory leading
217
+ // into another directory or repository) is never allowed.
218
+ const rootReal = real(root);
219
+ const inside = (p) => [root, rootReal].some(r => p === r || p.startsWith(r + path.sep));
220
+ const guard = (abs) => !inside(abs) || (failClosed ? lexists(abs) : tracked.has(abs));
221
+ if (failClosed) warning('wize: git could not list tracked files (git missing or unsafe repository); existing files are left untouched');
222
+ const render = renderTargets({
223
+ kitRoot, projectRoot: root, targets, profiles,
224
+ guard,
225
+ skipAgentsMd: true
226
+ });
227
+ result.adapters = render.adapters;
228
+ for (const a of render.adapters) {
229
+ if (a.skipped) chatter(` - ${a.code}: skipped (${a.reason})`);
230
+ else if (a.error) chatter(` ✖ ${a.code}: ${a.error}`);
231
+ else chatter(` ✓ ${a.code}: ${a.written} written, ${a.unchanged} unchanged${a.skippedTracked.length ? `, ${a.skippedTracked.length} tracked skipped` : ''}`);
232
+ }
233
+ const failed = render.adapters.filter(a => a.error);
234
+ if (failed.length) {
235
+ result.ok = false;
236
+ for (const a of failed) result.warnings.push(`wize: adapter ${a.code} failed: ${a.error}`);
237
+ warn(`wize: hydrate: adapter ${failed.map(a => a.code).join(', ')} failed; run: npx --yes wize-dev-kit@${safePin(pin) || 'latest'} hydrate`);
238
+ }
239
+ const trackedSkipped = render.adapters.reduce((n, a) => n + a.skippedTracked.length, 0);
240
+ if (trackedSkipped && !failClosed) {
241
+ warning(`wize: ${trackedSkipped} generated file(s) are tracked by git or resolve outside the project and were left untouched; untrack them with: npx wize-dev-kit@latest update`);
242
+ }
243
+ if (!failed.length && !(failClosed && trackedSkipped)) {
244
+ writeMarker(root, {
245
+ kit_version: binary,
246
+ pin,
247
+ targets,
248
+ profiles,
249
+ counts: {
250
+ written: render.adapters.reduce((n, a) => n + a.written, 0),
251
+ unchanged: render.adapters.reduce((n, a) => n + a.unchanged, 0),
252
+ skipped_tracked: trackedSkipped
253
+ },
254
+ delegated: false,
255
+ rendered_with: binary,
256
+ fallback: canDelegate
257
+ });
258
+ }
259
+ }
260
+
261
+ // Safety net: whatever shows up in `git status` that was not there before
262
+ // means the .gitignore block is out of date. Nothing is reverted.
263
+ const after = statusPorcelain(root);
264
+ if (before && after) {
265
+ const known = new Set(before);
266
+ const fresh = after.filter(l => !known.has(l));
267
+ if (fresh.length) {
268
+ warning(`wize: hydrate left new or changed entries in git status: ${fresh.map(l => l.trim()).join(', ')}; your .gitignore block is outdated; run: npx wize-dev-kit@latest update`);
269
+ }
270
+ }
271
+
272
+ // Notice: cache only, never blocks.
273
+ const quietNotice = flags['no-notice'] || flags.quiet || env.WIZE_DISABLE_UPDATE_CHECK === '1' || isCI(env);
274
+ if (!quietNotice && pin) {
275
+ // Without a terminal (an agent, an Orca setup script) no background refresh is started.
276
+ const isTTY = deps.isTTY !== undefined ? deps.isTTY : process.stdout.isTTY;
277
+ const latest = latestNoWait({ env: isTTY ? env : { ...env, WIZE_NO_BG_REFRESH: '1' } });
278
+ if (latest && semverGreater(latest, pin)) {
279
+ result.notice = `wize: kit ${pin} -> ${latest} available; run: npx wize-dev-kit@latest update`;
280
+ say(result.notice);
281
+ }
282
+ }
283
+
284
+ if (!delegated) {
285
+ const via = pin && pin !== binary ? `, rendered with ${binary}` : '';
286
+ say(`wize: hydrated ${root} (kit_version ${pin || 'none'}${via}, ${result.adapters.length} adapters, user.toml: ${result.user_toml})`);
287
+ }
288
+ return emit();
289
+ } catch (err) {
290
+ result.ok = false;
291
+ const pin = result.pin;
292
+ warn(`wize: hydrate: ${err && err.message ? err.message : err}; run: npx --yes wize-dev-kit@${safePin(pin) || 'latest'} hydrate`);
293
+ result.warnings.push(`wize: hydrate: ${err && err.message ? err.message : err}`);
294
+ return emit();
295
+ }
296
+ }
297
+
298
+ module.exports = { cmdHydrate };
@@ -1,45 +1,72 @@
1
1
  // `wize-dev-kit sync` — regenerate adapter outputs for the IDE targets the
2
2
  // project opted into. Useful after editing .wize/config/project.toml,
3
3
  // running `agent create`, or pulling kit changes that don't bump version.
4
+ //
5
+ // sync is the explicit "regenerate everything" command, so it renders
6
+ // unguarded. It never writes user.toml, .gitignore or kit_version. For a
7
+ // fresh checkout `hydrate` is the safe command; sync stays for old hook
8
+ // scripts and for pins that predate hydrate.
4
9
 
5
10
  'use strict';
6
11
 
7
- const fs = require('node:fs');
8
- const path = require('node:path');
9
- const { loadProjectConfig } = require('./update.js');
12
+ const { loadProjectConfig } = require('../toml.js');
13
+ const { renderTargets } = require('../render-targets.js');
14
+ const { writeMarker } = require('../marker.js');
15
+ const { compareVersions, parse, safePin } = require('../semver.js');
16
+ const { loadInstalledKitVersion } = require('./update.js');
17
+ const { supportsHydrate } = require('../project-status.js');
18
+
19
+ // -> 'none' | 'project-behind' | 'project-ahead'
20
+ function skewOf(pin, binary) {
21
+ if (!pin || !binary || !parse(pin) || !parse(binary)) return 'none';
22
+ const cmp = compareVersions(pin, binary);
23
+ return cmp < 0 ? 'project-behind' : cmp > 0 ? 'project-ahead' : 'none';
24
+ }
10
25
 
11
26
  function cmdSync({ kitRoot, projectRoot, opts = {} } = {}) {
12
27
  const log = opts.log || console.log;
28
+ const warn = opts.warn || console.error;
13
29
  const cfg = loadProjectConfig(projectRoot);
14
30
  if (!cfg.project) {
15
31
  log('No .wize/config/project.toml found here. Run `wize-dev-kit install` first.');
16
- return { changed: false };
32
+ return { changed: false, adapters: [], skew: 'none' };
17
33
  }
18
34
  const ideTargets = (cfg.install && cfg.install.ide_targets) || ['claude-code', 'generic'];
19
35
  const profiles = (cfg.install && cfg.install.profiles) || ['core'];
36
+ const pin = (typeof cfg.project.kit_version === 'string' && cfg.project.kit_version) || null;
37
+ const binary = loadInstalledKitVersion(kitRoot);
38
+
39
+ const skew = skewOf(pin, binary);
40
+ if (skew === 'project-behind') {
41
+ warn(`wize: project pinned ${pin}, running ${binary}; for an exact render run: npx --yes wize-dev-kit@${safePin(pin) || 'latest'} sync (or update the pin: npx wize-dev-kit@latest update)`);
42
+ } else if (skew === 'project-ahead') {
43
+ warn(`wize: project pinned ${pin} is newer than this kit ${binary}; run: npx --yes wize-dev-kit@${safePin(pin) || 'latest'} sync`);
44
+ }
45
+ if (supportsHydrate(pin)) warn('prefer: npx wize-dev-kit hydrate');
46
+
20
47
  log(`Syncing ${ideTargets.length} adapter(s) for profile(s) ${profiles.join(' + ')}:`);
21
48
 
22
- const results = [];
23
- for (const code of ideTargets) {
24
- const renderPath = path.join(kitRoot, 'adapters', code, 'render.js');
25
- if (!fs.existsSync(renderPath)) {
26
- log(` - ${code}: skipped (adapter missing in installed kit)`);
27
- results.push({ code, skipped: true });
28
- continue;
29
- }
30
- delete require.cache[require.resolve(renderPath)];
31
- try {
32
- const out = require(renderPath).render(kitRoot, projectRoot, { profiles });
33
- const n = out && Array.isArray(out.written) ? out.written.length : 0;
34
- log(` ✓ ${code}: ${n} file(s) emitted`);
35
- results.push({ code, written: n });
36
- } catch (err) {
37
- log(` ✖ ${code}: ${err.message}`);
38
- results.push({ code, error: err.message });
39
- }
49
+ const { adapters: results } = renderTargets({ kitRoot, projectRoot, targets: ideTargets, profiles });
50
+ for (const a of results) {
51
+ if (a.skipped) log(` - ${a.code}: skipped (${a.reason === 'adapter missing' ? 'adapter missing in installed kit' : a.reason})`);
52
+ else if (a.error) log(` ✖ ${a.code}: ${a.error}`);
53
+ else log(` ✓ ${a.code}: ${a.written + a.unchanged} file(s) emitted`);
54
+ }
55
+
56
+ const written = results.reduce((n, a) => n + (a.written || 0), 0);
57
+ if (!results.some(a => a.error)) {
58
+ writeMarker(projectRoot, {
59
+ kit_version: binary,
60
+ targets: ideTargets,
61
+ profiles,
62
+ counts: { written, unchanged: results.reduce((n, a) => n + (a.unchanged || 0), 0), skipped_tracked: 0 },
63
+ delegated: false,
64
+ rendered_with: binary
65
+ });
40
66
  }
67
+
41
68
  log('\nDone. Restart your IDE if it caches skills at startup.');
42
- return { changed: true, adapters: results };
69
+ return { changed: written > 0, adapters: results, skew };
43
70
  }
44
71
 
45
72
  module.exports = { cmdSync };