@imfusion/web-ui 0.10.1-dev.60.g7891a716 → 0.10.1-dev.61.g9d1000b8

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/CHANGELOG.md CHANGED
@@ -57,6 +57,11 @@ never changes the character width, so columns stay aligned. CoFo Sans Mono still
57
57
  - Remove a header color set to match the branded navbar. A custom surface still goes in `--imf-ui-appshell-header-bg`, which is unset by default and falls back to the navbar variant's surface.
58
58
  - **Breaking:** `AppShell.Header` is a flex row, and it wraps its children in a container that aligns them to the inline end. [AppShell](src/components/app-shell/app-shell.mdx) — Nikolaus Brunner <brunner@imfusion.com>
59
59
  - Give your own row `width: 100%` to span the header.
60
+ - **Breaking:** `npx web-ui install hooks` removes `baseline-staleness.sh`, and the `SessionStart` hook reports installed
61
+ skills that differ from the package copy in `node_modules` instead. — Nikolaus Brunner <brunner@imfusion.com>
62
+ - Remove the `.agents/hooks/imf-web-ui/baseline-staleness.sh` line from the project's pre-commit hook before running
63
+ `install hooks`; a pre-commit that calls the missing script fails every commit.
64
+ - Keep `.agents/skills/imf-web-ui*` out of the project's formatter, or the reformatted skills read as stale.
60
65
 
61
66
  ### Added
62
67
 
@@ -121,6 +126,9 @@ never changes the character width, so columns stay aligned. CoFo Sans Mono still
121
126
  - The navbar rail, `AppShell.Main`, and the aside share `--imf-ui-appshell-padding` as their inline gutter, so their text lines up with no consumer CSS. [Docs](src/components/app-shell/app-shell.mdx)
122
127
  - `--imf-ui-appshell-navbar-padding` defaults to `--imf-ui-appshell-padding`, and `--imf-ui-appshell-aside-padding` defaults to `--imf-ui-appshell-padding`. Both stay overridable.
123
128
  - Agent skills — Nikolaus Brunner <brunner@imfusion.com>
129
+ - `npx web-ui install skills` writes no `.imf-web-ui-skill-version.json` marker into each skill and removes existing ones.
130
+ Both install commands read the harness back from the Claude skill links and the installer's hook registrations, and
131
+ take it from an old marker when one is still there.
124
132
  - `imf-web-ui-audit` checks that `@imfusion/web-ui` is installed before it dispatches, and asks whether to install first,
125
133
  since token, version, and type checks need the installed package. Its investigators search with whatever read-only
126
134
  file search the host offers.
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as e, r as t, t as n } from "./paths-B5Ht3ETc.js";
3
3
  import { chmodSync as r, cpSync as i, existsSync as a, lstatSync as o, mkdirSync as s, readFileSync as c, readdirSync as l, readlinkSync as u, realpathSync as d, rmSync as f, symlinkSync as p, writeFileSync as m } from "node:fs";
4
- import { dirname as h, relative as g, resolve as _ } from "node:path";
5
- import { Command as ee, Option as te } from "commander";
6
- import * as v from "@clack/prompts";
7
- var y = {
4
+ import { dirname as ee, relative as h, resolve as g } from "node:path";
5
+ import { Command as te, Option as ne } from "commander";
6
+ import * as _ from "@clack/prompts";
7
+ var v = {
8
8
  harnesses: {
9
9
  claude: {
10
10
  label: "Claude Code",
@@ -22,162 +22,160 @@ var y = {
22
22
  templatesDir: "src/llms/install-templates",
23
23
  skillsSource: "src/llms/skills",
24
24
  skillsDir: ".agents/skills",
25
- linkDir: ".claude/skills",
26
- versionMarker: ".imf-web-ui-skill-version.json"
27
- }, ne = `<!-- ${y.namespace}:begin`, b = `<!-- ${y.namespace}:end -->`, re = _(e, y.templatesDir, "AGENTS.md"), x = _(t, "AGENTS.md");
28
- function S(e) {
29
- let t = e.indexOf(ne), n = e.indexOf(b);
30
- return t === -1 || n === -1 ? null : e.slice(t, n + b.length);
25
+ linkDir: ".claude/skills"
26
+ }, re = `<!-- ${v.namespace}:begin`, y = `<!-- ${v.namespace}:end -->`, ie = g(e, v.templatesDir, "AGENTS.md"), b = g(t, "AGENTS.md");
27
+ function x(e) {
28
+ let t = e.indexOf(re), n = e.indexOf(y);
29
+ return t === -1 || n === -1 ? null : e.slice(t, n + y.length);
31
30
  }
32
- function ie() {
33
- if (!a(x)) return "absent";
34
- let e = S(c(re, "utf-8"));
31
+ function S() {
32
+ if (!a(b)) return "absent";
33
+ let e = x(c(ie, "utf-8"));
35
34
  if (!e) return "absent";
36
- let t = c(x, "utf-8"), n = S(t), r = n ? t.replace(n, e) : `${t.trimEnd()}\n\n${e}\n`;
37
- return r === t ? "unchanged" : (m(x, r), n ? "updated" : "added");
35
+ let t = c(b, "utf-8"), n = x(t), r = n ? t.replace(n, e) : `${t.trimEnd()}\n\n${e}\n`;
36
+ return r === t ? "unchanged" : (m(b, r), n ? "updated" : "added");
38
37
  }
39
38
  //#endregion
40
39
  //#region src/cli/skills.ts
41
- var C = Object.keys(y.harnesses).filter((e) => e in y.harnesses), w = [...C, "all"];
42
- function T(e) {
40
+ var C = Object.keys(v.harnesses).filter((e) => e in v.harnesses), ae = [...C, "all"];
41
+ function w(e) {
43
42
  return e === "all" ? C : [e];
44
43
  }
45
- function E(e) {
46
- return `./${g(t, e)}`;
44
+ function T(e) {
45
+ return `./${h(t, e)}`;
47
46
  }
48
- var D = y.namespace, O = y.versionMarker, k = _(e, y.skillsSource), A = _(t, y.skillsDir), j = _(t, y.linkDir), M = class extends Error {}, N = JSON.parse(c(_(e, "package.json"), "utf-8"));
49
- if (typeof N != "object" || !N || !("version" in N) || typeof N.version != "string") throw Error(`${_(e, "package.json")} has no string "version" field`);
50
- var P = N.version;
51
- function F(e) {
52
- return e === D || e.startsWith(`${D}-`);
47
+ var E = v.namespace, D = ".imf-web-ui-skill-version.json", O = g(e, v.skillsSource), k = g(t, v.skillsDir), A = g(t, v.linkDir), j = class extends Error {}, M = JSON.parse(c(g(e, "package.json"), "utf-8"));
48
+ if (typeof M != "object" || !M || !("version" in M) || typeof M.version != "string") throw Error(`${g(e, "package.json")} has no string "version" field`);
49
+ var oe = M.version;
50
+ function N(e) {
51
+ return e === E || e.startsWith(`${E}-`);
53
52
  }
54
- function I() {
55
- return a(k) ? l(k, { withFileTypes: !0 }).filter((e) => e.isDirectory() && F(e.name)).map((e) => e.name).sort() : [];
53
+ function P() {
54
+ return a(O) ? l(O, { withFileTypes: !0 }).filter((e) => e.isDirectory() && N(e.name)).map((e) => e.name).sort() : [];
56
55
  }
57
- function ae(e) {
58
- let t = _(A, e, O);
59
- if (!a(t)) return null;
56
+ function se(e) {
57
+ if (!a(e)) return null;
60
58
  try {
61
- let e = JSON.parse(c(t, "utf-8"));
62
- if (typeof e != "object" || !e) return null;
63
- let n = "harnesses" in e && Array.isArray(e.harnesses) ? e.harnesses : null;
59
+ let t = JSON.parse(c(e, "utf-8"));
60
+ if (typeof t != "object" || !t) return null;
61
+ let n = "harnesses" in t && Array.isArray(t.harnesses) ? t.harnesses : null;
64
62
  return n ? C.filter((e) => n.includes(e)) : null;
65
63
  } catch {
66
64
  return null;
67
65
  }
68
66
  }
69
- function L(e) {
67
+ function ce(e) {
70
68
  for (let t of e) {
71
- let e = ae(t);
69
+ let e = se(g(k, t, D));
72
70
  if (e && e.length > 0) return e;
73
71
  }
74
72
  return null;
75
73
  }
76
- function R(e, t, n) {
77
- if (a(e)) for (let r of l(e).filter(F)) t.has(r) || (f(_(e, r), {
74
+ function F(e) {
75
+ return e.some((e) => a(g(k, e)));
76
+ }
77
+ function I(e) {
78
+ return R() ? F(e) : e.some((e) => o(g(A, e), { throwIfNoEntry: !1 })?.isSymbolicLink() === !0);
79
+ }
80
+ function L(e, t, n) {
81
+ if (a(e)) for (let r of l(e).filter(N)) t.has(r) || (f(g(e, r), {
78
82
  recursive: !0,
79
83
  force: !0
80
- }), v.log.info(`Removed skill: ${r} (${n})`));
84
+ }), _.log.info(`Removed skill: ${r} (${n})`));
81
85
  }
82
- function oe() {
83
- return a(j) && a(A) && d(j) === d(A);
86
+ function R() {
87
+ return a(A) && a(k) && d(A) === d(k);
84
88
  }
85
- function z(e) {
86
- let t = _(j, e), n = g(j, _(A, e)), r = o(t, { throwIfNoEntry: !1 });
89
+ function le(e) {
90
+ let t = g(A, e), n = h(A, g(k, e)), r = o(t, { throwIfNoEntry: !1 });
87
91
  r?.isSymbolicLink() && u(t) === n || (r && f(t, {
88
92
  recursive: !0,
89
93
  force: !0
90
- }), s(j, { recursive: !0 }), p(n, t));
94
+ }), s(A, { recursive: !0 }), p(n, t));
91
95
  }
92
- function se(e, t) {
96
+ function ue(e, t) {
93
97
  try {
94
- s(A, { recursive: !0 });
95
- let r = [];
96
- for (let n of e) {
97
- let e = _(A, n);
98
+ s(k, { recursive: !0 });
99
+ for (let t of e) {
100
+ let e = g(k, t);
98
101
  f(e, {
99
102
  recursive: !0,
100
103
  force: !0
101
- }), i(_(k, n), e, { recursive: !0 });
102
- let a = _(e, O);
103
- m(a, JSON.stringify({
104
- version: P,
105
- harnesses: t
106
- }, null, 2) + "\n"), r.push(a);
104
+ }), i(g(O, t), e, { recursive: !0 });
107
105
  }
108
- if (n(r), R(A, new Set(e), "no longer in this bundle"), v.log.success(`Skills -> ${E(A)}/${D}*`), oe()) return;
106
+ if (L(k, new Set(e), "no longer in this bundle"), _.log.success(`Skills -> ${T(k)}/${E}*`), R()) return;
109
107
  if (t.includes("claude")) {
110
- for (let t of e) z(t);
111
- R(j, new Set(e), "no longer in this bundle"), v.log.success(`Claude Code -> ${E(j)}/${D}* (symlinks -> ${E(A)})`);
112
- } else R(j, /* @__PURE__ */ new Set(), "claude not selected");
108
+ for (let t of e) le(t);
109
+ L(A, new Set(e), "no longer in this bundle"), _.log.success(`Claude Code -> ${T(A)}/${E}* (symlinks -> ${T(k)})`);
110
+ } else L(A, /* @__PURE__ */ new Set(), "claude not selected");
113
111
  } catch (e) {
114
- throw e instanceof Error ? new M(e.message) : e;
112
+ throw e instanceof Error ? new j(e.message) : e;
115
113
  }
116
114
  }
117
115
  //#endregion
118
116
  //#region src/cli/hooks.ts
119
- var B = _(e, y.templatesDir), V = _(B, "hooks"), H = _(t, y.hooksDir), U = class extends Error {};
120
- function ce(e, t) {
121
- if (typeof e != "object" || !e || Array.isArray(e)) throw new U(`${t} is not a JSON object.`);
117
+ var z = g(e, v.templatesDir), B = g(z, "hooks"), V = g(t, v.hooksDir), H = class extends Error {};
118
+ function de(e, t) {
119
+ if (typeof e != "object" || !e || Array.isArray(e)) throw new H(`${t} is not a JSON object.`);
122
120
  let n = { ...e };
123
121
  if ("hooks" in n) {
124
- if (typeof n.hooks != "object" || n.hooks === null || Array.isArray(n.hooks)) throw new U(`${t}: "hooks" is not a JSON object.`);
125
- for (let [e, r] of Object.entries(n.hooks)) if (!Array.isArray(r)) throw new U(`${t}: "hooks.${e}" is not a JSON array.`);
122
+ if (typeof n.hooks != "object" || n.hooks === null || Array.isArray(n.hooks)) throw new H(`${t}: "hooks" is not a JSON object.`);
123
+ for (let [e, r] of Object.entries(n.hooks)) if (!Array.isArray(r)) throw new H(`${t}: "hooks.${e}" is not a JSON array.`);
126
124
  }
127
125
  return n;
128
126
  }
129
- function W(e) {
127
+ function U(e) {
130
128
  let t;
131
129
  try {
132
130
  t = JSON.parse(c(e, "utf-8"));
133
131
  } catch {
134
- throw new U(`${e} is not valid JSON.`);
132
+ throw new H(`${e} is not valid JSON.`);
135
133
  }
136
- return ce(t, e);
134
+ return de(t, e);
137
135
  }
138
- function le() {
139
- let e = l(V).filter((e) => e.endsWith(".sh"));
140
- s(H, { recursive: !0 });
136
+ function fe() {
137
+ let e = l(B).filter((e) => e.endsWith(".sh"));
138
+ s(V, { recursive: !0 });
141
139
  for (let t of e) {
142
- let e = _(H, t);
143
- i(_(V, t), e, { force: !0 }), r(e, 493);
140
+ let e = g(V, t);
141
+ i(g(B, t), e, { force: !0 }), r(e, 493);
144
142
  }
145
- let t = l(H).filter((t) => t.endsWith(".sh") && !e.includes(t));
146
- for (let e of t) f(_(H, e), { force: !0 });
143
+ let t = l(V).filter((t) => t.endsWith(".sh") && !e.includes(t));
144
+ for (let e of t) f(g(V, e), { force: !0 });
147
145
  return {
148
146
  installed: e,
149
147
  removed: t
150
148
  };
151
149
  }
152
- var G = `${y.hooksDir}/`;
153
- function K(e) {
150
+ var W = `${v.hooksDir}/`;
151
+ function G(e) {
154
152
  if (typeof e != "string") return null;
155
- let t = e.lastIndexOf(G);
153
+ let t = e.lastIndexOf(W);
156
154
  if (t === -1) return null;
157
- let n = /^([\w.-]+)["']?$/.exec(e.slice(t + G.length));
155
+ let n = /^([\w.-]+)["']?$/.exec(e.slice(t + W.length));
158
156
  return n ? n[1] : null;
159
157
  }
160
- function q(e) {
158
+ function K(e) {
161
159
  return typeof e == "object" && !!e && "hooks" in e && Array.isArray(e.hooks);
162
160
  }
163
- function J(e) {
164
- return q(e) ? e.hooks : null;
161
+ function q(e) {
162
+ return K(e) ? e.hooks : null;
165
163
  }
166
- function Y(e) {
164
+ function J(e) {
167
165
  return typeof e == "object" && e && "command" in e ? e.command : void 0;
168
166
  }
169
- function X(e) {
170
- let t = J(e);
171
- return t ? t.map((e) => K(Y(e))).filter((e) => e !== null) : [];
167
+ function Y(e) {
168
+ let t = q(e);
169
+ return t ? t.map((e) => G(J(e))).filter((e) => e !== null) : [];
172
170
  }
173
- function ue(e, t, n) {
171
+ function pe(e, t, n) {
174
172
  e.hooks ??= {};
175
173
  let r = 0, i = 0, a = [];
176
174
  for (let [r, o] of Object.entries(e.hooks)) {
177
175
  let s = 0, c = o.map((e) => {
178
- if (!q(e)) return e;
176
+ if (!K(e)) return e;
179
177
  let t = e.hooks, r = t.filter((e) => {
180
- let t = K(Y(e));
178
+ let t = G(J(e));
181
179
  return t === null || n.includes(t);
182
180
  });
183
181
  return s += t.length - r.length, r.length === t.length ? e : {
@@ -185,7 +183,7 @@ function ue(e, t, n) {
185
183
  hooks: r
186
184
  };
187
185
  }).filter((e) => {
188
- let t = J(e);
186
+ let t = q(e);
189
187
  return t === null || t.length > 0;
190
188
  });
191
189
  s !== 0 && (i += s, c.length === 0 && !(r in t) ? a.push(r) : e.hooks[r] = c);
@@ -194,8 +192,8 @@ function ue(e, t, n) {
194
192
  for (let [n, i] of Object.entries(t)) {
195
193
  e.hooks[n] ??= [];
196
194
  for (let t of i) {
197
- let i = X(t);
198
- e.hooks[n].some((e) => X(e).some((e) => i.includes(e))) || (e.hooks[n].push(t), r++);
195
+ let i = Y(t);
196
+ e.hooks[n].some((e) => Y(e).some((e) => i.includes(e))) || (e.hooks[n].push(t), r++);
199
197
  }
200
198
  }
201
199
  return {
@@ -203,99 +201,115 @@ function ue(e, t, n) {
203
201
  removed: i
204
202
  };
205
203
  }
206
- function de(e) {
204
+ function me(e) {
205
+ let n = g(t, v.harnesses[e].hookSettings);
206
+ if (!a(n)) return !1;
207
+ try {
208
+ return Object.values(U(n).hooks ?? {}).flat().some((e) => Y(e).length > 0);
209
+ } catch {
210
+ return !1;
211
+ }
212
+ }
213
+ function he(e) {
207
214
  let r = /* @__PURE__ */ new Map(), i = /* @__PURE__ */ new Map();
208
215
  for (let n of C) {
209
- let o = _(t, y.harnesses[n].hookSettings);
210
- if (a(o) && r.set(n, W(o)), e.includes(n)) {
211
- let e = _(B, y.harnesses[n].hookTemplate);
212
- i.set(n, W(e).hooks ?? {});
216
+ let o = g(t, v.harnesses[n].hookSettings);
217
+ if (a(o) && r.set(n, U(o)), e.includes(n)) {
218
+ let e = g(z, v.harnesses[n].hookTemplate);
219
+ i.set(n, U(e).hooks ?? {});
213
220
  }
214
221
  }
215
- let { installed: o, removed: c } = le();
216
- v.log.success(`Agent hooks -> ${E(H)} (${String(o.length)} scripts)`), c.length > 0 && v.log.info(`Removed stale hook script(s): ${c.join(", ")} (no longer shipped)`);
222
+ let { installed: o, removed: c } = fe();
223
+ _.log.success(`Agent hooks -> ${T(V)} (${String(o.length)} scripts)`), c.length > 0 && _.log.info(`Removed stale hook script(s): ${c.join(", ")} (no longer shipped)`);
217
224
  for (let a of C) {
218
225
  let c = e.includes(a);
219
226
  if (!c && !r.has(a)) continue;
220
- let l = _(t, y.harnesses[a].hookSettings), u = i.get(a) ?? {}, d = r.get(a) ?? {}, f = ue(d, u, c ? o : []);
227
+ let l = g(t, v.harnesses[a].hookSettings), u = i.get(a) ?? {}, d = r.get(a) ?? {}, f = pe(d, u, c ? o : []);
221
228
  if (f.added === 0 && f.removed === 0) {
222
- c && v.log.info(`Hook registrations already present in ${E(l)}`);
229
+ c && _.log.info(`Hook registrations already present in ${T(l)}`);
223
230
  continue;
224
231
  }
225
- s(h(l), { recursive: !0 }), m(l, JSON.stringify(d, null, 2) + "\n"), n([l]);
232
+ s(ee(l), { recursive: !0 }), m(l, JSON.stringify(d, null, 2) + "\n"), n([l]);
226
233
  let p = [f.added > 0 && `registered ${String(f.added)}`, f.removed > 0 && `removed ${String(f.removed)} stale`].filter(Boolean);
227
- v.log.success(`${p.join(", ")} hook registration(s) in ${E(l)}`);
234
+ _.log.success(`${p.join(", ")} hook registration(s) in ${T(l)}`);
228
235
  }
229
236
  }
230
237
  //#endregion
231
238
  //#region src/cli/index.ts
239
+ function X(e) {
240
+ let t = ce(e);
241
+ if (t) return t;
242
+ if (!F(e)) return null;
243
+ let n = C.filter((t) => me(t) || t === "claude" && I(e));
244
+ return n.length > 0 ? n : ["codex"];
245
+ }
232
246
  function Z() {
233
- return new te("--harness <harness>", "claude, codex, or all").choices(w);
247
+ return new ne("--harness <harness>", "claude, codex, or all").choices(ae);
234
248
  }
235
- async function fe(e, t, n) {
236
- if (t) return v.log.info(`Harness from --harness flag: ${t.join(", ")}`), t;
237
- let r = L(e);
238
- if (r && !n) return v.log.info(`Refreshing the existing install: ${r.join(", ")} — pass --reconfigure to choose differently.`), r;
249
+ async function ge(e, t, n) {
250
+ if (t) return _.log.info(`Harness from --harness flag: ${t.join(", ")}`), t;
251
+ let r = X(e);
252
+ if (r && !n) return _.log.info(`Refreshing the existing install: ${r.join(", ")} — pass --reconfigure to choose differently.`), r;
239
253
  if (!process.stdin.isTTY) return console.error("Pass --harness=claude|codex|all — no terminal to prompt in."), process.exitCode = 1, null;
240
- let i = await v.multiselect({
254
+ let i = await _.multiselect({
241
255
  message: "Install into which harness (or harnesses)?",
242
256
  options: C.map((e) => ({
243
257
  value: e,
244
- label: y.harnesses[e].label
258
+ label: v.harnesses[e].label
245
259
  })),
246
260
  required: !0
247
261
  });
248
- return v.isCancel(i) ? (v.cancel("Cancelled — nothing installed."), null) : i;
262
+ return _.isCancel(i) ? (_.cancel("Cancelled — nothing installed."), null) : i;
249
263
  }
250
- async function pe(e) {
251
- let t = e.harness ? T(e.harness) : null, n = I();
252
- if (v.intro(`@imfusion/web-ui install skills — ${String(n.length)} skills`), n.length === 0) {
253
- v.log.error("No skills found. Reinstall @imfusion/web-ui and try again."), v.outro("Nothing installed."), process.exitCode = 1;
264
+ async function _e(e) {
265
+ let t = e.harness ? w(e.harness) : null, n = P();
266
+ if (_.intro(`@imfusion/web-ui install skills — ${String(n.length)} skills`), n.length === 0) {
267
+ _.log.error("No skills found. Reinstall @imfusion/web-ui and try again."), _.outro("Nothing installed."), process.exitCode = 1;
254
268
  return;
255
269
  }
256
- let r = await fe(n, t, e.reconfigure === !0);
270
+ let r = await ge(n, t, e.reconfigure === !0);
257
271
  if (!r) return;
258
- v.log.message(`Skills in this bundle:\n${n.map((e) => ` - ${e}`).join("\n")}`);
272
+ _.log.message(`Skills in this bundle:\n${n.map((e) => ` - ${e}`).join("\n")}`);
259
273
  try {
260
- se(n, r);
274
+ ue(n, r);
261
275
  } catch (e) {
262
- if (e instanceof M) {
263
- v.log.error(e.message), v.outro("Install failed."), process.exitCode = 1;
276
+ if (e instanceof j) {
277
+ _.log.error(e.message), _.outro("Install failed."), process.exitCode = 1;
264
278
  return;
265
279
  }
266
280
  throw e;
267
281
  }
268
- let i = ie();
269
- (i === "updated" || i === "added") && v.log.success(`Refreshed the ${y.namespace} block in ./AGENTS.md`), v.outro(`Done — ${String(n.length)} skills installed.`);
282
+ let i = S();
283
+ (i === "updated" || i === "added") && _.log.success(`Refreshed the ${v.namespace} block in ./AGENTS.md`), _.outro(`Done — ${String(n.length)} skills installed.`);
270
284
  }
271
- function me(e) {
272
- let t = e.harness ? T(e.harness) : null, n = L(I());
273
- if (!t && !n) {
274
- console.error("No harness recorded. Pass --harness=claude|codex|all."), process.exitCode = 1;
285
+ function ve(e) {
286
+ let t = e.harness ? w(e.harness) : null, n = P(), r = X(n);
287
+ if (!t && !r) {
288
+ console.error("No harness detected. Pass --harness=claude|codex|all."), process.exitCode = 1;
275
289
  return;
276
290
  }
277
- let r = t ?? n ?? [], i = r.filter((e) => !(n ?? []).includes(e));
278
- if (i.length > 0) {
279
- for (let e of i) console.error(`No skills installed for ${e}. Run \`npx web-ui install skills --harness=${e}\` first.`);
291
+ let i = t ?? r ?? [], a = i.filter((e) => e === "claude" ? !I(n) : !F(n));
292
+ if (a.length > 0) {
293
+ for (let e of a) console.error(`No skills installed for ${e}. Run \`npx web-ui install skills --harness=${e}\` first.`);
280
294
  process.exitCode = 1;
281
295
  } else {
282
- v.intro("@imfusion/web-ui install hooks");
296
+ _.intro("@imfusion/web-ui install hooks");
283
297
  try {
284
- de(r);
298
+ he(i);
285
299
  } catch (e) {
286
- if (e instanceof U) {
300
+ if (e instanceof H) {
287
301
  console.error(e.message), process.exitCode = 1;
288
302
  return;
289
303
  }
290
304
  throw e;
291
305
  }
292
- v.outro("Done — agent hooks installed.");
306
+ _.outro("Done — agent hooks installed.");
293
307
  }
294
308
  }
295
- var Q = new ee();
296
- Q.name("web-ui").description("Command-line tools for @imfusion/web-ui").version(P);
309
+ var Q = new te();
310
+ Q.name("web-ui").description("Command-line tools for @imfusion/web-ui").version(oe);
297
311
  var $ = Q.command("install").description("Install @imfusion/web-ui agent skills or hooks into a project");
298
- $.command("skills").description("Install skills for the chosen harness").addOption(Z()).option("--reconfigure", "Re-open the harness selection prompt on an existing install").action(pe), $.command("hooks").description("Install agent lifecycle hooks (requires skills already installed for the harness)").addOption(Z()).action(me), $.action(() => {
312
+ $.command("skills").description("Install skills for the chosen harness").addOption(Z()).option("--reconfigure", "Re-open the harness selection prompt on an existing install").action(_e), $.command("hooks").description("Install agent lifecycle hooks (requires skills already installed for the harness)").addOption(Z()).action(ve), $.action(() => {
299
313
  $.help();
300
314
  }), Q.action(() => {
301
315
  Q.help();
@@ -40,8 +40,8 @@ npx web-ui install hooks --harness=all
40
40
  | `codex` | Codex, and any agent that reads `.agents/skills/` |
41
41
  | `all` | Both |
42
42
 
43
- Without `--harness`, `install skills` prompts once and records the choice, and later runs reuse it. `install hooks` needs the
44
- skills installed for the same harness first. Leave out the second command to install only the skills.
43
+ Without `--harness`, `install skills` prompts on a first install. Later runs read the harness from the installed skill links
44
+ and hook registrations. `install hooks` needs the skills installed first. Leave out the second command to install only the skills.
45
45
 
46
46
  `npx web-ui --help` lists the commands, and `--help` on any subcommand lists its options.
47
47
 
@@ -79,7 +79,7 @@ The hooks put the agent on the right skill at the right moment and ask it to ver
79
79
 
80
80
  | Event | What the hook does |
81
81
  | --- | --- |
82
- | `SessionStart` | Points the agent at the router once per session. |
82
+ | `SessionStart` | Points the agent at the router once per session, and tells it when the installed skills differ from the package. |
83
83
  | `SubagentStart` | Gives the same pointer to spawned agents. |
84
84
  | `UserPromptSubmit` | Names the companion skills relevant to the prompt. |
85
85
  | `Stop` | Asks for project verification after a turn edits source files. |
@@ -97,7 +97,7 @@ npx web-ui install skills
97
97
  npx web-ui install hooks
98
98
  ```
99
99
 
100
- Both reuse the recorded harness. Leave out the second command if the project skips hooks. Together they bring the installed
100
+ Both read the harness from the installed files. Leave out the second command if the project skips hooks. Together they bring the installed
101
101
  tooling in line with the package version and remove anything the package no longer ships.
102
102
 
103
103
  ## Reference
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imfusion/web-ui",
3
- "version": "0.10.1-dev.60.g7891a716",
3
+ "version": "0.10.1-dev.61.g9d1000b8",
4
4
  "description": "The official Web UI component library for ImFusion web apps",
5
5
  "author": "ImFusion GmbH",
6
6
  "homepage": "https://imfusion.com",
@@ -2,4 +2,18 @@
2
2
  # SessionStart: inject the web-ui router pointer once per session. Fixed string — the
3
3
  # hook-injection eval asserts on it verbatim.
4
4
  echo "This project depends on @imfusion/web-ui. Start at the imf-web-ui skill: it decides whether guidance is needed and routes to the right companion. Repo docs carry only what is unique to this repo."
5
+
6
+ # Installed skills that differ from the package's copy are stale. A symlinked checkout
7
+ # carries work-in-progress skills, so it never counts.
8
+ ROOT=$(cd "$(dirname "$0")/../../.." && pwd) || exit 0
9
+ PACKAGE="$ROOT/node_modules/@imfusion/web-ui"
10
+ [ -d "$PACKAGE/src/llms/skills" ] && [ ! -L "$PACKAGE" ] || exit 0
11
+ for dir in "$PACKAGE"/src/llms/skills/imf-web-ui* "$ROOT"/.agents/skills/imf-web-ui*; do
12
+ [ -d "$dir" ] || continue
13
+ name=$(basename "$dir")
14
+ if ! diff -rq "$PACKAGE/src/llms/skills/$name" "$ROOT/.agents/skills/$name" >/dev/null 2>&1; then
15
+ echo "The installed imf-web-ui skills differ from the @imfusion/web-ui package in node_modules. Tell the user and offer the imf-web-ui-update skill, or \`npx web-ui install skills\`."
16
+ break
17
+ fi
18
+ done
5
19
  exit 0
@@ -96,10 +96,10 @@ UI component or a hard-coded color waiting for a token, is evidence for that top
96
96
  ## Agent tooling checks
97
97
 
98
98
  For `agent-tooling`, compare the declared version in `package.json`, the installed
99
- `node_modules/@imfusion/web-ui/package.json`, every `.imf-web-ui-skill-version.json` marker, the `AGENTS.md` fence, hook
100
- scripts, and both host registrations with the shipped topic. Version drift, extra hooks, stale markers, and missing
101
- registrations are findings; point version drift to `imf-web-ui-update`. Registry availability is unverified. Read files as
102
- text; do not run installers or hooks.
99
+ `node_modules/@imfusion/web-ui/package.json`, the installed skills against `node_modules/@imfusion/web-ui/src/llms/skills`,
100
+ the `AGENTS.md` fence, hook scripts, and both host registrations with the shipped topic. Version drift, extra hooks, skills
101
+ that differ from the package copy, and missing registrations are findings; point version drift to `imf-web-ui-update`.
102
+ Registry availability is unverified. Read files as text; do not run installers or hooks.
103
103
 
104
104
  ## Safety
105
105
 
@@ -12,12 +12,13 @@ npx web-ui install skills --harness=claude
12
12
 
13
13
  It installs or refreshes the `imf-web-ui-*` skills, updates the managed `AGENTS.md` fence, and removes skills that the
14
14
  package no longer ships. The skills always live in `.agents/skills/`. `--harness` selects `claude`, which also links each
15
- skill into `.claude/skills/`, `codex`, or `all`; without it, the command prompts and remembers the choice. Pass
16
- `--reconfigure` to choose again. On a project that already has an install, re-running `install skills` without `--harness`
17
- reuses the remembered harness instead of narrowing it.
15
+ skill into `.claude/skills/`, `codex`, or `all`; without it, the command prompts on a first install. Pass `--reconfigure` to
16
+ choose again. On a project that already has an install, re-running `install skills` or `install hooks` without `--harness`
17
+ reads the harness back from the installed files: Claude skill links and installer-owned hook registrations, with Codex
18
+ assumed when skills leave no trace.
18
19
 
19
- Skill version markers (`.imf-web-ui-skill-version.json`) show which package version installed each skill. Refresh them with
20
- the installer; do not hand-edit the vendored files.
20
+ Refresh the skills with the installer; do not hand-edit them. Keep `.agents/skills/imf-web-ui*` out of the project's
21
+ formatter: a reformatted skill no longer matches the package copy, and the session-start check reports it as stale.
21
22
 
22
23
  ## The hooks
23
24
 
@@ -36,7 +37,8 @@ harness that is not selected.
36
37
 
37
38
  The shipped events are:
38
39
 
39
- - `SessionStart`: point the agent at the Web UI router once per session.
40
+ - `SessionStart`: point the agent at the Web UI router once per session, and tell it when the installed skills differ from
41
+ the copy in `node_modules/@imfusion/web-ui`.
40
42
  - `SubagentStart`: provide the same pointer to spawned agents.
41
43
  - `UserPromptSubmit`: name the companion skills relevant to the prompt.
42
44
  - `Stop`: ask for project verification after a turn edits source files.
@@ -44,9 +46,6 @@ The shipped events are:
44
46
  Read existing registrations first. If the project already covers an event, do not stack a second hook; adapt the existing
45
47
  registration and keep the missing behavior.
46
48
 
47
- `baseline-staleness.sh` is a pre-commit check, not an agent hook. It compares installed skill markers with the installed
48
- package version.
49
-
50
49
  ## Codex trust
51
50
 
52
51
  Codex runs project hooks only after the project is trusted and each script is approved through `/hooks`. Revisit that review
@@ -28,6 +28,6 @@ The `staged` block and the scripts under `scripts/verify/` own the step lists. H
28
28
 
29
29
  ## Staleness at commit time
30
30
 
31
- Pre-commit also runs advisory checks for vendored skill markers and documentation drift. If a staged change makes a doc
32
- stale, update the doc in the same commit. For an outdated installed skill, run `npx web-ui install skills` (also
33
- `npx web-ui install hooks` if the project uses hooks) with the project's existing `--harness` in the consumer project.
31
+ Pre-commit also runs an advisory check for documentation drift. If a staged change makes a doc stale, update the doc in the
32
+ same commit. Outdated installed skills surface at session start instead, as the `agent-tooling` topic describes; refresh them
33
+ with `npx web-ui install skills` (also `npx web-ui install hooks` if the project uses hooks).
@@ -79,8 +79,9 @@ npx web-ui install skills
79
79
  npx web-ui install hooks
80
80
  ```
81
81
 
82
- Both commands reuse the recorded harness. An install from the old `web-ui-install` has no record: pass the project's existing
83
- harness once, e.g. `--harness=all`. Stop on install failure. Do not use `--force` or `--legacy-peer-deps`.
82
+ Both commands read the harness back from the installed files and print it. Codex leaves no trace without hooks, so a Claude
83
+ and Codex install without hooks reads as Claude only; when the printed harness is wrong, pass the project's harness, e.g.
84
+ `--harness=all`. Stop on install failure. Do not use `--force` or `--legacy-peer-deps`.
84
85
 
85
86
  The installer owns the vendored skills, its `AGENTS.md` fence, and hook scripts. Run the installer commands after the package
86
87
  update even when the existing registrations look complete: they refresh scripts, prune retired registrations, and merge
@@ -88,8 +89,7 @@ installer-owned entries idempotently into the registration file for the chosen h
88
89
  `.codex/hooks.json`, or both). Stop on malformed settings or installer conflicts. Read existing host registrations first and
89
90
  do not stack an event the project already covers.
90
91
 
91
- After the installer command, inspect the installed skill diffs and briefly report any guidance changes; version-marker
92
- changes alone do not count.
92
+ After the installer command, inspect the installed skill diffs and briefly report any guidance changes.
93
93
 
94
94
  Then re-read, in full, every changed skill and reference the remaining work uses, starting with this update skill when it
95
95
  changed. A diff shows what moved, not the step order you continue under, so it does not replace the re-read. Continue from
@@ -117,7 +117,7 @@ Run the consumer's documented full verification. Read its scripts first; prefer
117
117
  non-watch format, lint, typecheck, test, and build commands that exist.
118
118
 
119
119
  Verify that the installed package, manifest, and lockfile match the recorded target version. A mismatch is a base-update
120
- conflict. Check skill markers, the `AGENTS.md` fence, hook registrations, and the final update-owned path list.
120
+ conflict. Check the `AGENTS.md` fence, hook registrations, and the final update-owned path list.
121
121
 
122
122
  Report before the first commit:
123
123
 
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env sh
2
- # Advisory baseline-staleness check, meant to be called from the repo's pre-commit hook.
3
- # Compares the installed imf-web-ui skill markers against the installed package version
4
- # and warns on drift. Always exits 0 — staleness never blocks a commit.
5
- PKG=node_modules/@imfusion/web-ui/package.json
6
- [ -f "$PKG" ] || exit 0
7
- # A symlinked checkout reports its own dev version.
8
- [ -L node_modules/@imfusion/web-ui ] && exit 0
9
- CURRENT=$(node -p "require('./$PKG').version" 2>/dev/null) || exit 0
10
-
11
- # .claude/skills may be a symlink to .agents/skills; skip markers already read.
12
- seen=""
13
- for marker in .agents/skills/imf-web-ui*/.imf-web-ui-skill-version.json .claude/skills/imf-web-ui*/.imf-web-ui-skill-version.json; do
14
- [ -f "$marker" ] || continue
15
- real=$(cd "$(dirname "$marker")" && pwd -P)/$(basename "$marker")
16
- case " $seen " in
17
- *" $real "*) continue ;;
18
- esac
19
- seen="$seen $real"
20
- INSTALLED=$(node -p "require('./$marker').version" 2>/dev/null) || continue
21
- if [ "$INSTALLED" != "$CURRENT" ]; then
22
- echo "imf-web-ui skills are stale ($INSTALLED installed, package is $CURRENT) — run /imf-web-ui-update, or: npx web-ui install skills && npx web-ui install hooks"
23
- break
24
- fi
25
- done
26
- exit 0