@imfusion/web-ui 0.5.0 → 0.5.1-dev.11.g2e949f6d

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 (40) hide show
  1. package/README.md +175 -40
  2. package/bin/install.js +319 -0
  3. package/bin/install.test.ts +139 -0
  4. package/dist/components/logo/logo.d.ts +1 -1
  5. package/dist/index.js +3 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +30 -22
  8. package/src/docgen/doc.gen.json +1 -1
  9. package/src/llms/skills/imf-web-ui/SKILL.md +17 -8
  10. package/src/llms/skills/imf-web-ui-agent-setup/SKILL.md +46 -0
  11. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/baseline-staleness.sh +17 -0
  12. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/post-tool-use.sh +21 -0
  13. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/session-start.sh +4 -0
  14. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/user-prompt-submit.sh +4 -0
  15. package/src/llms/skills/imf-web-ui-agent-setup/templates/settings.json +37 -0
  16. package/src/llms/skills/imf-web-ui-components/SKILL.md +1 -1
  17. package/src/llms/skills/imf-web-ui-frontend-conventions/SKILL.md +44 -0
  18. package/src/llms/skills/imf-web-ui-frontend-conventions/references/assets.md +23 -0
  19. package/src/llms/skills/imf-web-ui-frontend-conventions/references/components.md +63 -0
  20. package/src/llms/skills/imf-web-ui-frontend-conventions/references/data.md +130 -0
  21. package/src/llms/skills/imf-web-ui-frontend-conventions/references/docs-structure.md +44 -0
  22. package/src/llms/skills/imf-web-ui-frontend-conventions/references/git.md +33 -0
  23. package/src/llms/skills/imf-web-ui-frontend-conventions/references/library-boundary.md +37 -0
  24. package/src/llms/skills/imf-web-ui-frontend-conventions/references/npm-project.md +57 -0
  25. package/src/llms/skills/imf-web-ui-frontend-conventions/references/project-structure.md +21 -0
  26. package/src/llms/skills/{imf-web-ui-frontend-patterns/references/react-patterns.md → imf-web-ui-frontend-conventions/references/react.md} +26 -12
  27. package/src/llms/skills/imf-web-ui-frontend-conventions/references/stack.md +39 -0
  28. package/src/llms/skills/imf-web-ui-frontend-conventions/references/styling.md +91 -0
  29. package/src/llms/skills/imf-web-ui-frontend-conventions/references/testing.md +16 -0
  30. package/src/llms/skills/imf-web-ui-frontend-conventions/references/tooling.md +65 -0
  31. package/src/llms/skills/imf-web-ui-frontend-conventions/references/typescript.md +45 -0
  32. package/src/llms/skills/imf-web-ui-frontend-setup/SKILL.md +83 -0
  33. package/src/llms/skills/imf-web-ui-frontend-setup/templates/AGENTS.md +34 -0
  34. package/src/llms/skills/imf-web-ui-frontend-setup/templates/README.md +27 -0
  35. package/src/llms/skills/imf-web-ui-library-setup/SKILL.md +56 -0
  36. package/src/llms/skills/imf-web-ui-ux/SKILL.md +4 -4
  37. package/src/llms/skills/imf-web-ui-ux/references/forms.md +4 -2
  38. package/bin/install-skill.js +0 -180
  39. package/src/llms/skills/imf-web-ui-frontend-patterns/SKILL.md +0 -67
  40. package/src/llms/skills/imf-web-ui-setup/SKILL.md +0 -30
package/README.md CHANGED
@@ -1,60 +1,195 @@
1
1
  # Web UI
2
2
 
3
- `@imfusion/web-ui` — the official shared Web UI library for ImFusion web apps, built on Base UI (selected through the
4
- company's Component Library ADR). It will be published to ImFusion's private npm registry for internal use (publishing isn't
5
- wired up yet — the package is still `private`).
3
+ `@imfusion/web-ui` — the official shared Web UI library for ImFusion web apps, built on Base UI.
6
4
 
7
- This repo is **AI-first**: it's designed to be worked on by driving an agent through a set of skills, and that's the
8
- smoothest path — but hand-writing code is perfectly welcome too. (How the agent config is wired is described under
9
- [Working with an AI agent](#working-with-an-ai-agent).)
5
+ - [Usage](#usage) — install, wire up, and keep the version current
6
+ - [Documentation](#documentation) — Storybook and the developer docs
7
+ - [Development](#development) — working on the library itself, including the Agent Skills
8
+ - [Releasing](#releasing) — how versions are derived and how to cut one
10
9
 
11
- Either way, the fastest way in is the **`/web-ui-dev-getting-started`** skill: a short interactive intro that figures out
12
- what you want to do and points you at the right skill and docs, walking you through setup and the commands you'll need.
10
+ ## Usage
13
11
 
14
- For the full workflow, see [`CONTRIBUTING.md`](CONTRIBUTING.md); for the runnable scripts, [`package.json`](package.json) is
15
- the source of truth.
12
+ The packages are private on npmjs.com.
13
+
14
+ To install them, get the Web SDK npm token from the
15
+ [Test Licenses](https://imfusion.atlassian.net/wiki/spaces/DEV/pages/255590402/Test+Licenses) page on Confluence and add it
16
+ to your user-level `.npmrc`:
17
+
18
+ ```bash
19
+ npm config set //registry.npmjs.org/:_authToken=<token> --location=user
20
+ ```
21
+
22
+ Then:
23
+
24
+ ```bash
25
+ npm install @imfusion/web-ui
26
+ ```
27
+
28
+ Or if you want to install the latest CI build from master:
29
+
30
+ ```bash
31
+ npm install @imfusion/web-ui@dev
32
+ ```
33
+
34
+ Import the stylesheet and wrap your app root once:
35
+
36
+ ```tsx
37
+ import "@imfusion/web-ui/styles.css";
38
+ import { WebUIProvider } from "@imfusion/web-ui";
39
+ ```
40
+
41
+ ### LLM integration
42
+
43
+ The `web-ui` package does offer first-class support for LLMs, but it's not wired up by default.
44
+
45
+ To install the skills, run this command after the package is installed:
46
+
47
+ ```bash
48
+ npx web-ui-install
49
+ ```
50
+
51
+ The binary asks which LLM client you use and installs everything it needs. Run it again after a version bump and it refreshes
52
+ the directories it already installed into, without asking again — `--reconfigure` re-opens that choice.
53
+
54
+ **Available consumer skills - `/imf-web-ui-*`**
55
+
56
+ | Skill | What it does |
57
+ | ---------------------------------- | ----------------------------------------------------------------------------- |
58
+ | `/imf-web-ui` | The router: decides whether guidance is needed, then which companion to open. |
59
+ | `/imf-web-ui-library-setup` | One-time wiring in a consumer project — the styles import and the provider. |
60
+ | `/imf-web-ui-components` | Component reference — what exists and how it's meant to be used. |
61
+ | `/imf-web-ui-ux` | UX guidance for building interfaces with the library. |
62
+ | `/imf-web-ui-frontend-conventions` | The frontend conventions baseline, including the sanctioned styling seams. |
63
+ | `/imf-web-ui-frontend-setup` | Set up or audit an ImFusion frontend's tooling against the house baseline. |
64
+ | `/imf-web-ui-agent-setup` | Install or update the vendored skills and agent hooks in a consumer repo. |
65
+
66
+ Start at `/imf-web-ui` — it routes to the rest. Storybook's **User Guide → AI Agents** page covers the whole family.
67
+
68
+ ### Versions and updates
69
+
70
+ Every merge to master publishes a new build under the `dev` dist-tag. Which spec you install decides how you pick those up:
71
+
72
+ | Spec in `package.json` | Gets | Update with |
73
+ | ---------------------------------- | -------------------------- | ------------------------------ |
74
+ | `"@imfusion/web-ui": "^0.5.0"` | matching releases | `npm update @imfusion/web-ui` |
75
+ | `"@imfusion/web-ui": "latest"` | the newest release | `npm update @imfusion/web-ui` |
76
+ | `"@imfusion/web-ui": "dev"` | the newest build of master | `npm update @imfusion/web-ui` |
77
+ | `"@imfusion/web-ui": "file:….tgz"` | a packed tarball, frozen | re-pack and re-install by hand |
78
+
79
+ - **Two dist-tags.** `latest` moves when a release is tagged, `dev` on every build of master (and onto the release when one
80
+ is cut).
81
+ - **A range like `^0.5.0` is the usual choice.** It tracks releases and states which major you expect; bare `latest` follows
82
+ releases across majors, breaking changes included.
83
+ - **Dist-tag specs stay literal.** npm keeps `dev` or `latest` as-is in `package.json` and re-resolves on every `npm update`.
84
+ The concrete version lands in `package-lock.json`, so builds stay reproducible until you update.
85
+ - **Track `dev` while building against the library**, to get new components as they land. Use a release spec for anything you
86
+ cut a production release from, since `dev` moves whenever someone merges.
87
+ - **A `file:` tarball never updates.** npm copies the archive into `node_modules` and re-copies the same one on every
88
+ install. Switch to `dev` or a version range to get updates.
89
+
90
+ ## Documentation
91
+
92
+ **[Storybook](https://storybook.js.org/)** is the documentation platform — the component catalog, every prop, and the setup
93
+ guides. It isn't hosted yet, so run it locally:
94
+
95
+ ```bash
96
+ npm run dev # builds the library, then Storybook + a rebuild watcher
97
+ ```
98
+
99
+ See the [Developer Docs](./docs/README.md).
100
+
101
+ ## Development
102
+
103
+ Install the deps and configure git:
16
104
 
17
105
  ```bash
18
- npm install # installs deps, then configures git (see notes below)
106
+ npm install
107
+ npm run git:config # hooks path + rebase-only pull/merge
19
108
  ```
20
109
 
21
- > **Note — `npm install` also configures git** via the `prepare` script: sets `.githooks` as the hooks path, and enforces a
22
- > rebase-only pull/merge strategy (`pull.rebase true`, `merge.ff only`). No manual git setup needed.
110
+ **Try out **`/web-ui-dev-getting-started`** for an ai assisted start.**
23
111
 
24
- > **Note — install scripts are disabled** (`.npmrc` sets `ignore-scripts=true` for supply-chain security). Today's native
25
- > deps (esbuild, fsevents) ship their platform binary as an optional dependency, so a fresh install works out of the box. If
26
- > you add a package that relies on a native install hook (e.g. `sharp`, `node-gyp` builds), it won't run —
27
- > `npm rebuild <pkg>` once after install, and note it here.
112
+ ### LLM integration
28
113
 
29
- # Using the library
114
+ These skills are shipped with a `/web-ui-dev-*` prefix, so to make their names identifiable and not pollute the skill
115
+ namespace.
30
116
 
31
- If you're consuming `@imfusion/web-ui` in a downstream app rather than working on it, **Storybook is your entry point** — it
32
- carries the setup guides and a live, browsable catalog of every component and its props. It isn't hosted yet; run it locally
33
- with `npm run dev` (or `npm run storybook`) and open the printed URL.
117
+ | Skill | What it does |
118
+ | ----------------------------------------- | ------------------------------------------------------------------------------------- |
119
+ | `/web-ui-dev-getting-started` | Interactive intro — gauges your experience, works out your goal, routes you. |
120
+ | `/web-ui-dev-start` | Starting ritual for any task: Jira context, `master` vs. a worktree, context summary. |
121
+ | `/web-ui-dev-new-component` | Scaffolds an architecture-compliant primitive (adapted or absorbed). |
122
+ | `/web-ui-dev-update-component` | Pointers to every file a prop, variant, or sub-component change touches. |
123
+ | `/web-ui-dev-design-component` | Brand design pass for a component that works but isn't styled yet. |
124
+ | `/web-ui-dev-story` | Author or update a Storybook story. |
125
+ | `/web-ui-dev-commit` | Commit workflow: staged docs audit, CI-parity checks, house commit format. |
126
+ | `/web-ui-dev-audit-docs` | Audit docs against staged or recent changes for staleness, gaps, and drift. |
127
+ | `/web-ui-dev-audit-pass-through-defaults` | Check `@default` annotations on pass-through props against Base UI upstream. |
128
+ | `/web-ui-dev-manage-worktrees` | Create, close, or discard a worktree in `.workspaces/`. |
129
+ | `/web-ui-dev-refresh-design-reference` | Refresh the committed brand snapshots in `design/` from the Figma styleguide. |
130
+ | `/web-ui-dev-mcp` | Set up or recover Storybook, DevTools, Atlassian, or Figma MCP access. |
34
131
 
35
- If an AI agent is working in that downstream app, run `npx web-ui-install-skills` (once `@imfusion/web-ui` is installed) to
36
- give it the `imf-web-ui` skill family — setup, component lookup, UX guidance, and frontend patterns, with a small router
37
- skill deciding which applies — no need for it to read this library's source. The skills ship inside the package at
38
- `src/llms/skills/` (next to the generated llms index; deliberately _not_ in `.agents/skills/`, which holds this repo's own
39
- development skills). See the **AI Agents** page in Storybook's User Guide for the full skill family and how it routes.
132
+ ## Releasing
40
133
 
41
- # Working with an AI agent
134
+ The published version is derived from git tags, not from `package.json`.
42
135
 
43
- The agent config is tool-neutral by design, so the repo isn't tied to a single LLM client:
136
+ The `version` field stays at `0.0.0` in the repository and he CI computes the real number with
137
+ [`scripts/version.ts`](./scripts/version.ts) and writes it into `package.json` on the build agent just before publishing.
44
138
 
45
- - **`AGENTS.md`** holds the instructions, following the cross-tool `AGENTS.md` convention that Cursor, Aider, Windsurf,
46
- Copilot and others read directly.
47
- - **`.agents/skills/`** holds the skills — the reusable, step-by-step workflows an agent runs.
139
+ That working copy is thrown away, so nothing is committed back.
48
140
 
49
- Claude Code is the one client that looks for its own paths rather than these, so it's bridged with two symlinks:
50
- `CLAUDE.md → AGENTS.md` and `.claude/skills → .agents/skills`. Edit through either path — it's the same file — but `.agents/`
51
- and `AGENTS.md` are the canonical, git-tracked source.
141
+ `scripts/version.ts` reads `git describe` and applies two rules:
52
142
 
53
- # Architecture
143
+ Given the tag `web-ui/v0.5.0`:
54
144
 
55
- The library's wrappers, types, and public API follow strict rules captured in
56
- **[docs/architecture.md](docs/architecture.md)**. Read it before contributing — every wrapped component, hook, and public API
57
- decision must satisfy these rules.
145
+ | Where HEAD sits | Published version | dist-tag |
146
+ | -------------------------- | ----------------------- | -------- |
147
+ | Exactly on `web-ui/v0.5.0` | `0.5.0` | `latest` |
148
+ | 5 commits after that tag | `0.5.1-dev.5.gb4de52d7` | `dev` |
58
149
 
59
- That's one of several developer docs — architecture, conventions, the color system, responsiveness, and story authoring.
60
- Start from **[docs/README.md](docs/README.md)**, which lays out the full reading order.
150
+ The off-tag string breaks down as:
151
+
152
+ ```
153
+ 0.5.1-dev.5.gb4de52d7
154
+ └─┬─┘ └┬┘ │ └───┬────┘
155
+ │ │ │ └── commit hash, abbreviated; the leading g means "git"
156
+ │ │ └──────── commits since the tag
157
+ │ └─────────── pre-release marker, which is what puts it on the dev tag
158
+ └──────────────── the tag's version, patch bumped
159
+ ```
160
+
161
+ Bumping the patch is what makes a dev build sort above the release it follows and below the next one.
162
+
163
+ Every build of `master` publishes a `dev` version automatically. To check what the current checkout would publish:
164
+
165
+ ```bash
166
+ npx tsx scripts/version.ts # prints, changes nothing
167
+ ```
168
+
169
+ ### Cutting a release
170
+
171
+ Tag the commit you want to ship and push the tag:
172
+
173
+ ```bash
174
+ git tag -a web-ui/v0.5.1 -m "web-ui 0.5.1"
175
+ git push origin web-ui/v0.5.1
176
+ ```
177
+
178
+ The tag triggers a build that publishes `0.5.1` as `latest` and repoints the `dev` dist-tag at the same version.
179
+
180
+ **The tag must be annotated (`-a`).** `git describe` ignores lightweight tags, so a tag pushed without `-a` is invisible to
181
+ the version script — the build reads through it to the previous release tag and publishes another `dev` version instead of
182
+ the release.
183
+
184
+ The tag name must match `web-ui/vX.Y.Z` exactly. Both the version script's `--match` and the TeamCity branch filter in
185
+ [`.teamcity/settings.kts`](./.teamcity/settings.kts) key off that shape; a tag in any other form publishes nothing.
186
+
187
+ ### Building locally with a real version
188
+
189
+ A local `npm run build` stamps `dist` with `0.0.0`. When you need the true version in a local artifact:
190
+
191
+ ```bash
192
+ npx tsx scripts/version.ts --write # writes it into package.json
193
+ npm run build
194
+ git checkout package.json # discard the write
195
+ ```
package/bin/install.js ADDED
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Installs this package's agent tooling into the consumer project: the
4
+ // skill family (../src/llms/skills/imf-web-ui*/) and the agent lifecycle
5
+ // hooks the frontend-setup skill ships as templates. Runs in the
6
+ // CONSUMER's environment, so it may only use this package's real runtime
7
+ // dependencies (@clack/prompts). Never wire this into a postinstall hook:
8
+ // ambient script execution on `npm install` is a live supply-chain attack
9
+ // vector — install stays an explicit, user-run command.
10
+ //
11
+ // The skills cross-reference each other, so they install as one bundle.
12
+ // When both targets are selected, .agents/skills/ holds the real copy and
13
+ // .claude/skills/ symlinks it (this repo's own convention) so the two
14
+ // can't drift apart.
15
+ //
16
+ // Flags: bare = skills only; --hooks adds the agent lifecycle hooks (driven
17
+ // by the imf-web-ui-agent-setup skill, which checks the repo's existing hooks
18
+ // first). --target claude|agents (repeatable) for scripted installs,
19
+ // --reconfigure to re-open the target prompt on an existing install.
20
+
21
+ import {
22
+ chmodSync,
23
+ cpSync,
24
+ existsSync,
25
+ lstatSync,
26
+ mkdirSync,
27
+ readdirSync,
28
+ readFileSync,
29
+ rmSync,
30
+ symlinkSync,
31
+ writeFileSync
32
+ } from "node:fs";
33
+ import { dirname, relative, resolve } from "node:path";
34
+ import { fileURLToPath } from "node:url";
35
+ import * as p from "@clack/prompts";
36
+
37
+ const SKILL_PREFIX = "imf-web-ui";
38
+ const VERSION_MARKER = ".imf-web-ui-skill-version.json";
39
+
40
+ const here = dirname(fileURLToPath(import.meta.url));
41
+ // Source is relative to THIS SCRIPT's location (inside node_modules), not
42
+ // the consumer's cwd — the script is invoked from the consumer's project
43
+ // root, but the skills it copies ship alongside this file in the package.
44
+ const skillsRoot = resolve(here, "..", "src", "llms", "skills");
45
+ const packageJson = JSON.parse(readFileSync(resolve(here, "..", "package.json"), "utf-8"));
46
+ const currentVersion = packageJson.version;
47
+
48
+ // Destination is relative to the consumer's project root (cwd), since
49
+ // that's where their `.claude/` or `.agents/` directory lives.
50
+ const projectRoot = process.cwd();
51
+
52
+ const TARGETS = {
53
+ claude: { label: "Claude Code", root: resolve(projectRoot, ".claude", "skills") },
54
+ agents: { label: "Vendor-neutral (.agents/)", root: resolve(projectRoot, ".agents", "skills") }
55
+ };
56
+
57
+ function discoverSkills() {
58
+ if (!existsSync(skillsRoot)) return [];
59
+ return readdirSync(skillsRoot, { withFileTypes: true })
60
+ .filter(entry => entry.isDirectory() && entry.name.startsWith(SKILL_PREFIX))
61
+ .map(entry => entry.name)
62
+ .sort();
63
+ }
64
+
65
+ function displayPath(absPath) {
66
+ return `./${relative(projectRoot, absPath)}`;
67
+ }
68
+
69
+ function readInstalledVersion(dir) {
70
+ const markerPath = resolve(dir, VERSION_MARKER);
71
+ if (!existsSync(markerPath)) return null;
72
+ try {
73
+ return JSON.parse(readFileSync(markerPath, "utf-8")).version ?? null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ function writeRealCopy(sourceDir, destDir) {
80
+ mkdirSync(dirname(destDir), { recursive: true });
81
+ // force: true makes re-running after a version bump overwrite cleanly.
82
+ cpSync(sourceDir, destDir, { recursive: true, force: true });
83
+ writeFileSync(resolve(destDir, VERSION_MARKER), JSON.stringify({ version: currentVersion }, null, 2) + "\n");
84
+ }
85
+
86
+ function writeSymlink(linkPath, targetPath) {
87
+ mkdirSync(dirname(linkPath), { recursive: true });
88
+ // lstat (not existsSync, which follows symlinks) catches a broken/stale
89
+ // symlink left over from a prior run, not just a real file or directory.
90
+ if (lstatSync(linkPath, { throwIfNoEntry: false })) {
91
+ rmSync(linkPath, { recursive: true, force: true });
92
+ }
93
+ symlinkSync(relative(dirname(linkPath), targetPath), linkPath);
94
+ }
95
+
96
+ // A prior install is recoverable from the filesystem, so a re-run doesn't
97
+ // re-ask: a target counts as chosen when any bundled skill is present under
98
+ // it. Symlinks count — they're how the both-targets layout represents
99
+ // .claude/, and lstat avoids following them into the real copy.
100
+ function detectInstalledTargets(skills) {
101
+ return Object.keys(TARGETS).filter(key =>
102
+ skills.some(name => lstatSync(resolve(TARGETS[key].root, name), { throwIfNoEntry: false }))
103
+ );
104
+ }
105
+
106
+ // Hook templates ship inside the agent-setup skill so they're readable as
107
+ // part of its documentation; the installer copies the scripts into the
108
+ // consumer's .agents/hooks/ and merges the registrations into
109
+ // .claude/settings.json.
110
+ const hooksSourceDir = resolve(skillsRoot, "imf-web-ui-agent-setup", "templates", "hooks");
111
+ const hooksSettingsTemplate = resolve(skillsRoot, "imf-web-ui-agent-setup", "templates", "settings.json");
112
+ // Installer-owned subdirectory: refreshed wholesale on every run, so a repo's
113
+ // own hooks in .agents/hooks/ are never touched.
114
+ const hooksDestDir = resolve(projectRoot, ".agents", "hooks", "imf-web-ui");
115
+ const settingsPath = resolve(projectRoot, ".claude", "settings.json");
116
+
117
+ function installHookScripts() {
118
+ const scripts = readdirSync(hooksSourceDir).filter(name => name.endsWith(".sh"));
119
+ mkdirSync(hooksDestDir, { recursive: true });
120
+ for (const name of scripts) {
121
+ const dest = resolve(hooksDestDir, name);
122
+ cpSync(resolve(hooksSourceDir, name), dest, { force: true });
123
+ chmodSync(dest, 0o755);
124
+ }
125
+ return scripts.length;
126
+ }
127
+
128
+ // Merge, never clobber: a registration is added only when no existing entry
129
+ // for that event already runs the same command, so re-runs are idempotent
130
+ // and hand-written settings survive.
131
+ function mergeHookRegistrations() {
132
+ const template = JSON.parse(readFileSync(hooksSettingsTemplate, "utf-8"));
133
+ let settings = {};
134
+ if (existsSync(settingsPath)) {
135
+ try {
136
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+ settings.hooks ??= {};
142
+ let added = 0;
143
+ for (const [event, entries] of Object.entries(template.hooks)) {
144
+ settings.hooks[event] ??= [];
145
+ for (const entry of entries) {
146
+ const commands = entry.hooks.map(hook => hook.command);
147
+ const present = settings.hooks[event].some(existing =>
148
+ (existing.hooks ?? []).some(hook => commands.includes(hook.command))
149
+ );
150
+ if (!present) {
151
+ settings.hooks[event].push(entry);
152
+ added++;
153
+ }
154
+ }
155
+ }
156
+ mkdirSync(dirname(settingsPath), { recursive: true });
157
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
158
+ return added;
159
+ }
160
+
161
+ // The AGENTS.md baseline note lives inside a fenced block owned by this
162
+ // installer. The template between the markers is the source of truth; an
163
+ // existing AGENTS.md gets the block replaced in place (or appended when
164
+ // absent), everything outside the fence is untouched. No AGENTS.md at all
165
+ // is left alone — scaffolding one is the setup skill's job.
166
+ const AGENTS_BLOCK_BEGIN = "<!-- imf-web-ui:begin";
167
+ const AGENTS_BLOCK_END = "<!-- imf-web-ui:end -->";
168
+ const agentsTemplatePath = resolve(skillsRoot, "imf-web-ui-frontend-setup", "templates", "AGENTS.md");
169
+ const agentsPath = resolve(projectRoot, "AGENTS.md");
170
+
171
+ function extractAgentsBlock(content) {
172
+ const begin = content.indexOf(AGENTS_BLOCK_BEGIN);
173
+ const end = content.indexOf(AGENTS_BLOCK_END);
174
+ if (begin === -1 || end === -1) return null;
175
+ return content.slice(begin, end + AGENTS_BLOCK_END.length);
176
+ }
177
+
178
+ function upsertAgentsBlock() {
179
+ if (!existsSync(agentsPath)) return "absent";
180
+ const block = extractAgentsBlock(readFileSync(agentsTemplatePath, "utf-8"));
181
+ if (!block) return "absent";
182
+ const current = readFileSync(agentsPath, "utf-8");
183
+ const existing = extractAgentsBlock(current);
184
+ const next = existing ? current.replace(existing, block) : `${current.trimEnd()}\n\n${block}\n`;
185
+ if (next === current) return "unchanged";
186
+ writeFileSync(agentsPath, next);
187
+ return existing ? "updated" : "added";
188
+ }
189
+
190
+ // --target claude|agents (repeatable) selects targets without the
191
+ // interactive prompt — for CI and scripted installs.
192
+ function parseTargetFlags(argv) {
193
+ const targets = [];
194
+ for (let i = 0; i < argv.length; i++) {
195
+ if (argv[i] !== "--target") continue;
196
+ const value = argv[i + 1];
197
+ if (!value || !(value in TARGETS)) {
198
+ console.error(`--target expects one of: ${Object.keys(TARGETS).join(", ")}`);
199
+ process.exit(1);
200
+ }
201
+ targets.push(value);
202
+ i++;
203
+ }
204
+ return targets;
205
+ }
206
+
207
+ async function main() {
208
+ const skills = discoverSkills();
209
+ const argv = process.argv.slice(2);
210
+ const flagTargets = parseTargetFlags(argv);
211
+ const reconfigure = argv.includes("--reconfigure");
212
+ // Bare invocation installs skills only. Hooks are opt-in via --hooks — the
213
+ // imf-web-ui-agent-setup skill drives that after checking what the repo
214
+ // already registers; the binary stays the dumb mechanical tail.
215
+ const wantHooks = argv.includes("--hooks");
216
+ const wantSkills = argv.includes("--skills") || !wantHooks;
217
+
218
+ const components = [wantSkills && `${skills.length} skills`, wantHooks && "agent hooks"].filter(Boolean);
219
+ p.intro(`@imfusion/web-ui install — ${components.join(" + ")}`);
220
+
221
+ if (wantSkills && skills.length === 0) {
222
+ p.log.error(`No skills found at ${skillsRoot}. Reinstall @imfusion/web-ui and try again.`);
223
+ p.outro("Nothing installed.");
224
+ process.exitCode = 1;
225
+ return;
226
+ }
227
+
228
+ if (wantHooks) {
229
+ const scriptCount = installHookScripts();
230
+ const added = mergeHookRegistrations();
231
+ p.log.success(`Agent hooks -> ${displayPath(hooksDestDir)} (${scriptCount} scripts)`);
232
+ if (added === null) {
233
+ p.log.warn(`${displayPath(settingsPath)} is not valid JSON — registrations not merged, fix it and re-run.`);
234
+ } else if (added > 0) {
235
+ p.log.success(`Registered ${added} hook(s) in ${displayPath(settingsPath)}`);
236
+ } else {
237
+ p.log.info(`Hook registrations already present in ${displayPath(settingsPath)}`);
238
+ }
239
+ if (!wantSkills) {
240
+ p.outro("Done — hooks installed.");
241
+ return;
242
+ }
243
+ }
244
+
245
+ const installedTargets = detectInstalledTargets(skills);
246
+
247
+ const existingVersions = Object.values(TARGETS)
248
+ .flatMap(({ root }) => skills.map(name => readInstalledVersion(resolve(root, name))))
249
+ .filter(Boolean);
250
+ if (existingVersions.length > 0 && existingVersions.every(v => v === currentVersion)) {
251
+ p.log.info(`Already up to date (v${currentVersion}). Re-running will overwrite with the same content.`);
252
+ } else if (existingVersions.some(v => v !== currentVersion)) {
253
+ const from = existingVersions.find(v => v !== currentVersion);
254
+ p.log.info(`Updating installed skills from v${from} to v${currentVersion}.`);
255
+ }
256
+
257
+ p.log.message(`Skills in this bundle:\n${skills.map(name => ` - ${name}`).join("\n")}`);
258
+
259
+ let selected;
260
+ if (flagTargets.length > 0) {
261
+ selected = flagTargets;
262
+ p.log.info(`Targets from --target flags: ${selected.join(", ")}`);
263
+ } else if (installedTargets.length > 0 && !reconfigure) {
264
+ selected = installedTargets;
265
+ p.log.info(
266
+ `Refreshing the existing install: ${selected.map(key => displayPath(TARGETS[key].root)).join(", ")}` +
267
+ ` — pass --reconfigure to choose different targets.`
268
+ );
269
+ } else {
270
+ selected = await p.multiselect({
271
+ message: "Install into which skill directory (or directories)?",
272
+ options: Object.entries(TARGETS).map(([key, { label, root }]) => ({
273
+ value: key,
274
+ label,
275
+ hint: displayPath(root)
276
+ })),
277
+ required: true
278
+ });
279
+
280
+ if (p.isCancel(selected)) {
281
+ p.cancel("Cancelled — nothing installed.");
282
+ return;
283
+ }
284
+ }
285
+
286
+ const both = selected.includes("claude") && selected.includes("agents");
287
+
288
+ for (const name of skills) {
289
+ const sourceDir = resolve(skillsRoot, name);
290
+ if (both) {
291
+ // .agents/ is the canonical real copy; .claude/ aliases it via symlink.
292
+ const realDir = resolve(TARGETS.agents.root, name);
293
+ writeRealCopy(sourceDir, realDir);
294
+ writeSymlink(resolve(TARGETS.claude.root, name), realDir);
295
+ } else {
296
+ for (const key of selected) {
297
+ writeRealCopy(sourceDir, resolve(TARGETS[key].root, name));
298
+ }
299
+ }
300
+ }
301
+
302
+ if (both) {
303
+ p.log.success(`Vendor-neutral (.agents/) -> ${displayPath(TARGETS.agents.root)}/${SKILL_PREFIX}*`);
304
+ p.log.success(`Claude Code -> ${displayPath(TARGETS.claude.root)}/${SKILL_PREFIX}* (symlinks -> .agents/)`);
305
+ } else {
306
+ for (const key of selected) {
307
+ p.log.success(`${TARGETS[key].label} -> ${displayPath(TARGETS[key].root)}/${SKILL_PREFIX}*`);
308
+ }
309
+ }
310
+
311
+ const agentsResult = upsertAgentsBlock();
312
+ if (agentsResult === "updated" || agentsResult === "added") {
313
+ p.log.success(`Refreshed the imf-web-ui block in ${displayPath(agentsPath)}`);
314
+ }
315
+
316
+ p.outro(`Done — ${skills.length} skills installed${wantHooks ? " + agent hooks" : ""}.`);
317
+ }
318
+
319
+ await main();