wkappbot-code 0.3.25 → 0.3.26

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.
@@ -224,13 +224,19 @@ export function ensureLinks({ home, platform = process.platform, binDir, env = p
224
224
  // Windows runs the PATH logic even when both shims already existed: the doctor row reads the persistent PATH,
225
225
  // so a missing entry is a FAIL whether or not this run wrote a shim. POSIX keeps its old rule (a link was written).
226
226
  const pathWanted = needsPath && (created.length > 0 || platform === "win32");
227
+ // POSIX: note whether ~/.profile existed BEFORE addToPath, so uninstall deletes only a profile install created.
228
+ const profilePath = platform === "win32" ? "" : join(home, ".profile");
229
+ const profileExisted = !!profilePath && existsSync(profilePath);
227
230
  const pathAdded = pathWanted ? !!addToPath({ dir, home, platform, readRegPath }) : false;
228
- return { created, dir, pathAdded };
231
+ const profileCreated = pathAdded && !profileExisted && !!profilePath && existsSync(profilePath) ? profilePath : "";
232
+ return { created, dir, pathAdded, profileCreated };
229
233
  }
230
234
 
231
235
  // removeCreatedLinks(created, opts) -> the entries actually removed. Deletes only the listed entries, nothing else.
232
236
  // A "PATH:<folder>" entry is the PATH entry install added: it is removed by removePath (exactly that folder),
233
- // never by unlink. opts: platform, home, and an injectable removePath({dir, home, platform}) -> boolean.
237
+ // never by unlink. A "PROFILE:<path>" entry is the ~/.profile install created: it is deleted only when its text is
238
+ // blank again (trim() empty), so a profile the user wrote into is never removed. opts: platform, home, and an
239
+ // injectable removePath({dir, home, platform}) -> boolean.
234
240
  export function removeCreatedLinks(created = [], { platform = process.platform, home = process.env.HOME || process.env.USERPROFILE || "", removePath = defaultRemovePath } = {}) {
235
241
  const removed = [];
236
242
  for (const p of created) {
@@ -238,6 +244,13 @@ export function removeCreatedLinks(created = [], { platform = process.platform,
238
244
  try { if (removePath({ dir: p.slice(5), home, platform })) removed.push(p); } catch { /* not removed */ }
239
245
  continue;
240
246
  }
247
+ if (p.startsWith("PROFILE:")) {
248
+ try {
249
+ const f = p.slice(8);
250
+ if (existsSync(f) && readFileSync(f, "utf8").trim() === "") { unlinkSync(f); removed.push(p); }
251
+ } catch { /* not removed */ }
252
+ continue;
253
+ }
241
254
  try { unlinkSync(p); removed.push(p); } catch { /* already gone */ }
242
255
  }
243
256
  return removed;
@@ -259,6 +272,7 @@ export function installBothLinks({ dest, home, platform = process.platform, binD
259
272
  const r = ensureLinks({ home, platform, binDir: dir, env, targets, addToPath, readRegPath });
260
273
  const created = BINS.map(pathOf).filter((p) => r.created.includes(p) || ours.some((bin) => pathOf(bin) === p));
261
274
  if (r.pathAdded) created.push("PATH:" + dir); // recorded only when this install wrote the PATH entry
275
+ if (r.profileCreated) created.push("PROFILE:" + r.profileCreated); // right after PATH: the profile install made
262
276
  return { created, pathAdded: r.pathAdded, kept, dir };
263
277
  }
264
278
 
package/lib/hook-wire.js CHANGED
@@ -61,11 +61,19 @@ function route(cmd, lite) {
61
61
  return null;
62
62
  }
63
63
 
64
+ // A dev seat: a harness checkout beside this lite (its .claude/hooks/wkharness-run.sh exists) that is not a web user install
65
+ // (no .wk-user-install marker in the harness folder). Python --wire uses the same two tests (tools/wkharness-lite.sh).
66
+ export function isDevSeat(lite) {
67
+ const kih = dirname(dirname(lite));
68
+ return existsSync(join(kih, ".claude", "hooks", "wkharness-run.sh")) && !existsSync(join(kih, ".wk-user-install"));
69
+ }
70
+
64
71
  // The node port of --wire, plus the create step of the web installer. Throws on a settings file that is not JSON.
65
72
  export function wireSettings(path, lite) {
66
73
  const r = readSettings(path);
67
74
  if (!r.ok) throw new Error(path + " is not plain JSON (" + r.err + ")");
68
75
  const d = r.json || {};
76
+ const dev = isDevSeat(lite) && harnessHooks(d).some((h) => /wkharness-run\.sh/.test(h.command));
69
77
  const out = {};
70
78
  for (const [ev, groups] of Object.entries(d.hooks && typeof d.hooks === "object" ? d.hooks : {})) {
71
79
  const keep = [];
@@ -75,6 +83,8 @@ export function wireSettings(path, lite) {
75
83
  const seen = new Set(), hs = [];
76
84
  for (const h of asHooks(g)) {
77
85
  const cmd = String((h && h.command) || "");
86
+ // a dev seat runs its full harness and never also the lite one (owner 2026-11-12): lite entries go, the rest stay verbatim
87
+ if (dev) { if (!/wkharness-lite\.sh/.test(cmd)) hs.push(h); continue; }
78
88
  const c = HARNESS_HOOK.test(cmd) ? route(cmd, lite) : null; // only a harness hook is ever rewritten
79
89
  if (c && !seen.has(c)) { seen.add(c); hs.push({ type: "command", command: c, timeout: 10 }); }
80
90
  else if (!c && !HARNESS_HOOK.test(cmd)) hs.push(h); // a user's own hook stays verbatim
@@ -83,7 +93,8 @@ export function wireSettings(path, lite) {
83
93
  }
84
94
  if (keep.length) out[ev] = keep;
85
95
  }
86
- for (const [ev, arm, matcher] of ARMS) {
96
+ // a dev seat gets no lite arm added: its full harness is the only chain
97
+ if (!dev) for (const [ev, arm, matcher] of ARMS) {
87
98
  const c = '"' + lite + '" ' + arm;
88
99
  if (!(out[ev] || []).some((g) => (g.hooks || []).some((h) => h.command === c)))
89
100
  (out[ev] = out[ev] || []).push({ ...(matcher ? { matcher } : {}), hooks: [{ type: "command", command: c, timeout: 10 }] });
@@ -124,6 +135,16 @@ function wireClaudeHooksInner(o) {
124
135
  log("hooks: WARN " + o.settings + " is not plain JSON (" + before.err + "); left untouched -- please fix it, then run: wkappbot-code install");
125
136
  return res;
126
137
  }
138
+ // A dev seat with both its full hook and a lite entry (owner 2026-11-12: two Stop chains fired): drop the lite entries
139
+ // and keep the full hook, whatever state the settings reports as.
140
+ const liteOnDisk = join(o.kih, "tools", "wkharness-lite.sh");
141
+ if (before.hooks.some((h) => /wkharness-run\.sh/.test(h.command)) && before.hooks.some((h) => /wkharness-lite\.sh/.test(h.command)) && isDevSeat(liteOnDisk)) {
142
+ wireSettings(o.settings, liteOnDisk);
143
+ const dev = analyzeHooks(o.settings, o.kih);
144
+ res.after = dev.state; res.changed = true;
145
+ log("hooks: " + o.settings + " runs the full harness on this dev seat; its lite hook entries were removed");
146
+ return res;
147
+ }
127
148
  if (before.state === "ours") { log("hooks: " + o.settings + " already runs every harness hook from " + o.kih); return res; }
128
149
  // A FULL harness hook (wkharness-run.sh, the pwsh hub of a dev seat) is never rewritten into the lite one where pwsh
129
150
  // runs: the rewrite would downgrade the owner's own seats. Leave it and say what to do.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wkappbot-code",
3
- "version": "0.3.25",
3
+ "version": "0.3.26",
4
4
  "description": "wkappbot-code -- Wisdom Kernel: the pinned OpenCode bridge plus the reflex harness plugin, the wkappbot core, the web harness and the latest public skills in one install",
5
5
  "type": "module",
6
6
  "scripts": {