@imfusion/web-ui 0.5.1-dev.2.g0a349a2a → 0.5.1-dev.20.g9170acaf
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/README.md +122 -43
- package/bin/install.js +343 -0
- package/bin/install.test.ts +166 -0
- package/dist/build/vite-css-module-names/index.d.ts +20 -0
- package/dist/build/vite-css-module-names.js +17 -0
- package/dist/components/logo/logo.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/style.css +1 -1
- package/package.json +34 -24
- package/src/docgen/doc.gen.json +1 -1
- package/src/llms/skills/imf-web-ui/SKILL.md +15 -11
- package/src/llms/skills/imf-web-ui-agent-setup/SKILL.md +71 -0
- package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/baseline-staleness.sh +17 -0
- package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/post-tool-use.sh +21 -0
- package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/session-start.sh +4 -0
- package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/user-prompt-submit.sh +4 -0
- package/src/llms/skills/imf-web-ui-agent-setup/templates/settings.json +37 -0
- package/src/llms/skills/imf-web-ui-components/SKILL.md +1 -1
- package/src/llms/skills/imf-web-ui-frontend-conventions/SKILL.md +45 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/assets.md +23 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/class-names.md +42 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/components.md +63 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/data.md +196 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/docs-structure.md +44 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/git.md +33 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/library-boundary.md +37 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/npm-project.md +57 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/project-structure.md +21 -0
- package/src/llms/skills/{imf-web-ui-frontend-patterns/references/react-patterns.md → imf-web-ui-frontend-conventions/references/react.md} +26 -12
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/stack.md +39 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/styling.md +91 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/testing.md +16 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/tooling.md +76 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/typescript.md +45 -0
- package/src/llms/skills/imf-web-ui-frontend-setup/SKILL.md +89 -0
- package/src/llms/skills/imf-web-ui-frontend-setup/templates/AGENTS.md +34 -0
- package/src/llms/skills/imf-web-ui-frontend-setup/templates/README.md +27 -0
- package/src/llms/skills/imf-web-ui-library-setup/SKILL.md +36 -0
- package/src/llms/skills/imf-web-ui-ux/SKILL.md +4 -4
- package/src/llms/skills/imf-web-ui-ux/references/forms.md +1 -1
- package/bin/install-skill.js +0 -180
- package/src/llms/skills/imf-web-ui-frontend-patterns/SKILL.md +0 -93
- package/src/llms/skills/imf-web-ui-frontend-patterns/references/code-conventions.md +0 -133
- package/src/llms/skills/imf-web-ui-imfusion-frontend-setup/SKILL.md +0 -201
- package/src/llms/skills/imf-web-ui-setup/SKILL.md +0 -57
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
`@imfusion/web-ui` — the official shared Web UI library for ImFusion web apps, built on Base UI.
|
|
4
4
|
|
|
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
|
|
9
|
+
|
|
5
10
|
## Usage
|
|
6
11
|
|
|
7
12
|
The packages are private on npmjs.com.
|
|
@@ -33,23 +38,54 @@ import "@imfusion/web-ui/styles.css";
|
|
|
33
38
|
import { WebUIProvider } from "@imfusion/web-ui";
|
|
34
39
|
```
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
> [`/imf-web-ui-setup`](./src/llms/skills/imf-web-ui-setup/SKILL.md).
|
|
41
|
+
### LLM integration
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
The `web-ui` package does offer first-class support for LLMs, but it's not wired up by default.
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
To install the skills, run this command after the package is installed:
|
|
42
46
|
|
|
43
47
|
```bash
|
|
44
|
-
|
|
45
|
-
npm run git:config # hooks path + rebase-only pull/merge
|
|
48
|
+
npx web-ui-install
|
|
46
49
|
```
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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, agent hooks, and TanStack Intent. |
|
|
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.
|
|
53
89
|
|
|
54
90
|
## Documentation
|
|
55
91
|
|
|
@@ -62,41 +98,21 @@ npm run dev # builds the library, then Storybook + a rebuild watcher
|
|
|
62
98
|
|
|
63
99
|
See the [Developer Docs](./docs/README.md).
|
|
64
100
|
|
|
65
|
-
##
|
|
66
|
-
|
|
67
|
-
Two families of Agent Skills, told apart by their prefix:
|
|
68
|
-
|
|
69
|
-
| Prefix | For | Lives in | How to get it |
|
|
70
|
-
| -------------- | ----------------------------- | -------------------------- | --------------------------- |
|
|
71
|
-
| `imf-web-ui-*` | Apps consuming the library | shipped in the npm package | `npx web-ui-install-skills` |
|
|
72
|
-
| `web-ui-dev-*` | Developing the library itself | `.agents/skills/` | already there after cloning |
|
|
73
|
-
|
|
74
|
-
See the [skills/ folder](.agents/skills/) for the full list.
|
|
75
|
-
|
|
76
|
-
**`imf-web-ui-*`** travels with the package, so an agent in a consumer app gets library guidance without reading this
|
|
77
|
-
library's source. ` imf-web-ui` — the bare name, no suffix — is the router: it costs little to load and decides which
|
|
78
|
-
companion applies, or that none does.
|
|
79
|
-
|
|
80
|
-
The companions cover library wiring (`-setup`), the component reference (`-components`), UX guidance (`-ux`), frontend
|
|
81
|
-
patterns (`-frontend-patterns`), and ImFusion project tooling (`-imfusion-frontend-setup`).
|
|
101
|
+
## Development
|
|
82
102
|
|
|
83
|
-
|
|
84
|
-
committing. Start with `/web-ui-dev-getting-started`, which routes you the same way.
|
|
103
|
+
Install the deps and configure git:
|
|
85
104
|
|
|
86
|
-
|
|
105
|
+
```bash
|
|
106
|
+
npm install
|
|
107
|
+
npm run git:config # hooks path + rebase-only pull/merge
|
|
108
|
+
```
|
|
87
109
|
|
|
88
|
-
|
|
110
|
+
**Try out **`/web-ui-dev-getting-started`** for an ai assisted start.**
|
|
89
111
|
|
|
90
|
-
|
|
91
|
-
| ------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
92
|
-
| `/imf-web-ui` | The router: decides whether guidance is needed, then which companion to open. |
|
|
93
|
-
| `/imf-web-ui-setup` | One-time wiring in a consumer project — the styles import and the provider. |
|
|
94
|
-
| `/imf-web-ui-components` | Component reference — what exists and how it's meant to be used. |
|
|
95
|
-
| `/imf-web-ui-ux` | UX guidance for building interfaces with the library. |
|
|
96
|
-
| `/imf-web-ui-frontend-patterns` | Frontend patterns, including the sanctioned styling seams (tokens, data attributes). |
|
|
97
|
-
| `/imf-web-ui-imfusion-frontend-setup` | Set up or audit an ImFusion frontend's tooling against the house baseline. |
|
|
112
|
+
### LLM integration
|
|
98
113
|
|
|
99
|
-
|
|
114
|
+
These skills are shipped with a `/web-ui-dev-*` prefix, so to make their names identifiable and not pollute the skill
|
|
115
|
+
namespace.
|
|
100
116
|
|
|
101
117
|
| Skill | What it does |
|
|
102
118
|
| ----------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
@@ -109,8 +125,71 @@ The split is the audience, and the prefix is how you tell at a glance which side
|
|
|
109
125
|
| `/web-ui-dev-commit` | Commit workflow: staged docs audit, CI-parity checks, house commit format. |
|
|
110
126
|
| `/web-ui-dev-audit-docs` | Audit docs against staged or recent changes for staleness, gaps, and drift. |
|
|
111
127
|
| `/web-ui-dev-audit-pass-through-defaults` | Check `@default` annotations on pass-through props against Base UI upstream. |
|
|
112
|
-
| `/web-ui-dev-
|
|
128
|
+
| `/web-ui-dev-teardown-worktree` | Tear down a worktree, wherever it lives — merging its branch first or dropping it. |
|
|
113
129
|
| `/web-ui-dev-refresh-design-reference` | Refresh the committed brand snapshots in `design/` from the Figma styleguide. |
|
|
114
130
|
| `/web-ui-dev-mcp` | Set up or recover Storybook, DevTools, Atlassian, or Figma MCP access. |
|
|
115
131
|
|
|
116
|
-
|
|
132
|
+
## Releasing
|
|
133
|
+
|
|
134
|
+
The published version is derived from git tags, not from `package.json`.
|
|
135
|
+
|
|
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.
|
|
138
|
+
|
|
139
|
+
That working copy is thrown away, so nothing is committed back.
|
|
140
|
+
|
|
141
|
+
`scripts/version.ts` reads `git describe` and applies two rules:
|
|
142
|
+
|
|
143
|
+
Given the tag `web-ui/v0.5.0`:
|
|
144
|
+
|
|
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` |
|
|
149
|
+
|
|
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,343 @@
|
|
|
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
|
+
function removeOrphanedSkills(selected, skills, both) {
|
|
97
|
+
const bundledSkills = new Set(skills);
|
|
98
|
+
const selectedTargets = both ? ["agents", "claude"] : selected;
|
|
99
|
+
|
|
100
|
+
for (const key of selectedTargets) {
|
|
101
|
+
const { root } = TARGETS[key];
|
|
102
|
+
if (!existsSync(root)) continue;
|
|
103
|
+
|
|
104
|
+
const orphanedSkills = readdirSync(root).filter(name => name.startsWith(`${SKILL_PREFIX}-`) && !bundledSkills.has(name));
|
|
105
|
+
for (const name of orphanedSkills) {
|
|
106
|
+
rmSync(resolve(root, name), { recursive: true, force: true });
|
|
107
|
+
// In the two-target layout, .claude/ is a symlinked view of the
|
|
108
|
+
// canonical .agents/ copy. Remove its matching link along with the
|
|
109
|
+
// orphaned canonical skill.
|
|
110
|
+
if (both && key === "agents") {
|
|
111
|
+
rmSync(resolve(TARGETS.claude.root, name), { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
p.log.info(`Removed orphaned skill: ${name} (no longer in this bundle)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// A prior install is recoverable from the filesystem, so a re-run doesn't
|
|
119
|
+
// re-ask: a target counts as chosen when any bundled skill is present under
|
|
120
|
+
// it. Symlinks count — they're how the both-targets layout represents
|
|
121
|
+
// .claude/, and lstat avoids following them into the real copy.
|
|
122
|
+
function detectInstalledTargets(skills) {
|
|
123
|
+
return Object.keys(TARGETS).filter(key =>
|
|
124
|
+
skills.some(name => lstatSync(resolve(TARGETS[key].root, name), { throwIfNoEntry: false }))
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Hook templates ship inside the agent-setup skill so they're readable as
|
|
129
|
+
// part of its documentation; the installer copies the scripts into the
|
|
130
|
+
// consumer's .agents/hooks/ and merges the registrations into
|
|
131
|
+
// .claude/settings.json.
|
|
132
|
+
const hooksSourceDir = resolve(skillsRoot, "imf-web-ui-agent-setup", "templates", "hooks");
|
|
133
|
+
const hooksSettingsTemplate = resolve(skillsRoot, "imf-web-ui-agent-setup", "templates", "settings.json");
|
|
134
|
+
// Installer-owned subdirectory: refreshed wholesale on every run, so a repo's
|
|
135
|
+
// own hooks in .agents/hooks/ are never touched.
|
|
136
|
+
const hooksDestDir = resolve(projectRoot, ".agents", "hooks", "imf-web-ui");
|
|
137
|
+
const settingsPath = resolve(projectRoot, ".claude", "settings.json");
|
|
138
|
+
|
|
139
|
+
function installHookScripts() {
|
|
140
|
+
const scripts = readdirSync(hooksSourceDir).filter(name => name.endsWith(".sh"));
|
|
141
|
+
mkdirSync(hooksDestDir, { recursive: true });
|
|
142
|
+
for (const name of scripts) {
|
|
143
|
+
const dest = resolve(hooksDestDir, name);
|
|
144
|
+
cpSync(resolve(hooksSourceDir, name), dest, { force: true });
|
|
145
|
+
chmodSync(dest, 0o755);
|
|
146
|
+
}
|
|
147
|
+
return scripts.length;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Merge, never clobber: a registration is added only when no existing entry
|
|
151
|
+
// for that event already runs the same command, so re-runs are idempotent
|
|
152
|
+
// and hand-written settings survive.
|
|
153
|
+
function mergeHookRegistrations() {
|
|
154
|
+
const template = JSON.parse(readFileSync(hooksSettingsTemplate, "utf-8"));
|
|
155
|
+
let settings = {};
|
|
156
|
+
if (existsSync(settingsPath)) {
|
|
157
|
+
try {
|
|
158
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
settings.hooks ??= {};
|
|
164
|
+
let added = 0;
|
|
165
|
+
for (const [event, entries] of Object.entries(template.hooks)) {
|
|
166
|
+
settings.hooks[event] ??= [];
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const commands = entry.hooks.map(hook => hook.command);
|
|
169
|
+
const present = settings.hooks[event].some(existing =>
|
|
170
|
+
(existing.hooks ?? []).some(hook => commands.includes(hook.command))
|
|
171
|
+
);
|
|
172
|
+
if (!present) {
|
|
173
|
+
settings.hooks[event].push(entry);
|
|
174
|
+
added++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
179
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
180
|
+
return added;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The AGENTS.md baseline note lives inside a fenced block owned by this
|
|
184
|
+
// installer. The template between the markers is the source of truth; an
|
|
185
|
+
// existing AGENTS.md gets the block replaced in place (or appended when
|
|
186
|
+
// absent), everything outside the fence is untouched. No AGENTS.md at all
|
|
187
|
+
// is left alone — scaffolding one is the setup skill's job.
|
|
188
|
+
const AGENTS_BLOCK_BEGIN = "<!-- imf-web-ui:begin";
|
|
189
|
+
const AGENTS_BLOCK_END = "<!-- imf-web-ui:end -->";
|
|
190
|
+
const agentsTemplatePath = resolve(skillsRoot, "imf-web-ui-frontend-setup", "templates", "AGENTS.md");
|
|
191
|
+
const agentsPath = resolve(projectRoot, "AGENTS.md");
|
|
192
|
+
|
|
193
|
+
function extractAgentsBlock(content) {
|
|
194
|
+
const begin = content.indexOf(AGENTS_BLOCK_BEGIN);
|
|
195
|
+
const end = content.indexOf(AGENTS_BLOCK_END);
|
|
196
|
+
if (begin === -1 || end === -1) return null;
|
|
197
|
+
return content.slice(begin, end + AGENTS_BLOCK_END.length);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function upsertAgentsBlock() {
|
|
201
|
+
if (!existsSync(agentsPath)) return "absent";
|
|
202
|
+
const block = extractAgentsBlock(readFileSync(agentsTemplatePath, "utf-8"));
|
|
203
|
+
if (!block) return "absent";
|
|
204
|
+
const current = readFileSync(agentsPath, "utf-8");
|
|
205
|
+
const existing = extractAgentsBlock(current);
|
|
206
|
+
const next = existing ? current.replace(existing, block) : `${current.trimEnd()}\n\n${block}\n`;
|
|
207
|
+
if (next === current) return "unchanged";
|
|
208
|
+
writeFileSync(agentsPath, next);
|
|
209
|
+
return existing ? "updated" : "added";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --target claude|agents (repeatable) selects targets without the
|
|
213
|
+
// interactive prompt — for CI and scripted installs.
|
|
214
|
+
function parseTargetFlags(argv) {
|
|
215
|
+
const targets = [];
|
|
216
|
+
for (let i = 0; i < argv.length; i++) {
|
|
217
|
+
if (argv[i] !== "--target") continue;
|
|
218
|
+
const value = argv[i + 1];
|
|
219
|
+
if (!value || !(value in TARGETS)) {
|
|
220
|
+
console.error(`--target expects one of: ${Object.keys(TARGETS).join(", ")}`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
targets.push(value);
|
|
224
|
+
i++;
|
|
225
|
+
}
|
|
226
|
+
return targets;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function main() {
|
|
230
|
+
const skills = discoverSkills();
|
|
231
|
+
const argv = process.argv.slice(2);
|
|
232
|
+
const flagTargets = parseTargetFlags(argv);
|
|
233
|
+
const reconfigure = argv.includes("--reconfigure");
|
|
234
|
+
// Bare invocation installs skills only. Hooks are opt-in via --hooks — the
|
|
235
|
+
// imf-web-ui-agent-setup skill drives that after checking what the repo
|
|
236
|
+
// already registers; the binary stays the dumb mechanical tail.
|
|
237
|
+
const wantHooks = argv.includes("--hooks");
|
|
238
|
+
const wantSkills = argv.includes("--skills") || !wantHooks;
|
|
239
|
+
|
|
240
|
+
const components = [wantSkills && `${skills.length} skills`, wantHooks && "agent hooks"].filter(Boolean);
|
|
241
|
+
p.intro(`@imfusion/web-ui install — ${components.join(" + ")}`);
|
|
242
|
+
|
|
243
|
+
if (wantSkills && skills.length === 0) {
|
|
244
|
+
p.log.error(`No skills found at ${skillsRoot}. Reinstall @imfusion/web-ui and try again.`);
|
|
245
|
+
p.outro("Nothing installed.");
|
|
246
|
+
process.exitCode = 1;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (wantHooks) {
|
|
251
|
+
const scriptCount = installHookScripts();
|
|
252
|
+
const added = mergeHookRegistrations();
|
|
253
|
+
p.log.success(`Agent hooks -> ${displayPath(hooksDestDir)} (${scriptCount} scripts)`);
|
|
254
|
+
if (added === null) {
|
|
255
|
+
p.log.warn(`${displayPath(settingsPath)} is not valid JSON — registrations not merged, fix it and re-run.`);
|
|
256
|
+
} else if (added > 0) {
|
|
257
|
+
p.log.success(`Registered ${added} hook(s) in ${displayPath(settingsPath)}`);
|
|
258
|
+
} else {
|
|
259
|
+
p.log.info(`Hook registrations already present in ${displayPath(settingsPath)}`);
|
|
260
|
+
}
|
|
261
|
+
if (!wantSkills) {
|
|
262
|
+
p.outro("Done — hooks installed.");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const installedTargets = detectInstalledTargets(skills);
|
|
268
|
+
|
|
269
|
+
const existingVersions = Object.values(TARGETS)
|
|
270
|
+
.flatMap(({ root }) => skills.map(name => readInstalledVersion(resolve(root, name))))
|
|
271
|
+
.filter(Boolean);
|
|
272
|
+
if (existingVersions.length > 0 && existingVersions.every(v => v === currentVersion)) {
|
|
273
|
+
p.log.info(`Already up to date (v${currentVersion}). Re-running will overwrite with the same content.`);
|
|
274
|
+
} else if (existingVersions.some(v => v !== currentVersion)) {
|
|
275
|
+
const from = existingVersions.find(v => v !== currentVersion);
|
|
276
|
+
p.log.info(`Updating installed skills from v${from} to v${currentVersion}.`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
p.log.message(`Skills in this bundle:\n${skills.map(name => ` - ${name}`).join("\n")}`);
|
|
280
|
+
|
|
281
|
+
let selected;
|
|
282
|
+
if (flagTargets.length > 0) {
|
|
283
|
+
selected = flagTargets;
|
|
284
|
+
p.log.info(`Targets from --target flags: ${selected.join(", ")}`);
|
|
285
|
+
} else if (installedTargets.length > 0 && !reconfigure) {
|
|
286
|
+
selected = installedTargets;
|
|
287
|
+
p.log.info(
|
|
288
|
+
`Refreshing the existing install: ${selected.map(key => displayPath(TARGETS[key].root)).join(", ")}` +
|
|
289
|
+
` — pass --reconfigure to choose different targets.`
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
selected = await p.multiselect({
|
|
293
|
+
message: "Install into which skill directory (or directories)?",
|
|
294
|
+
options: Object.entries(TARGETS).map(([key, { label, root }]) => ({
|
|
295
|
+
value: key,
|
|
296
|
+
label,
|
|
297
|
+
hint: displayPath(root)
|
|
298
|
+
})),
|
|
299
|
+
required: true
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (p.isCancel(selected)) {
|
|
303
|
+
p.cancel("Cancelled — nothing installed.");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const both = selected.includes("claude") && selected.includes("agents");
|
|
309
|
+
|
|
310
|
+
for (const name of skills) {
|
|
311
|
+
const sourceDir = resolve(skillsRoot, name);
|
|
312
|
+
if (both) {
|
|
313
|
+
// .agents/ is the canonical real copy; .claude/ aliases it via symlink.
|
|
314
|
+
const realDir = resolve(TARGETS.agents.root, name);
|
|
315
|
+
writeRealCopy(sourceDir, realDir);
|
|
316
|
+
writeSymlink(resolve(TARGETS.claude.root, name), realDir);
|
|
317
|
+
} else {
|
|
318
|
+
for (const key of selected) {
|
|
319
|
+
writeRealCopy(sourceDir, resolve(TARGETS[key].root, name));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
removeOrphanedSkills(selected, skills, both);
|
|
325
|
+
|
|
326
|
+
if (both) {
|
|
327
|
+
p.log.success(`Vendor-neutral (.agents/) -> ${displayPath(TARGETS.agents.root)}/${SKILL_PREFIX}*`);
|
|
328
|
+
p.log.success(`Claude Code -> ${displayPath(TARGETS.claude.root)}/${SKILL_PREFIX}* (symlinks -> .agents/)`);
|
|
329
|
+
} else {
|
|
330
|
+
for (const key of selected) {
|
|
331
|
+
p.log.success(`${TARGETS[key].label} -> ${displayPath(TARGETS[key].root)}/${SKILL_PREFIX}*`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const agentsResult = upsertAgentsBlock();
|
|
336
|
+
if (agentsResult === "updated" || agentsResult === "added") {
|
|
337
|
+
p.log.success(`Refreshed the imf-web-ui block in ${displayPath(agentsPath)}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
p.outro(`Done — ${skills.length} skills installed${wantHooks ? " + agent hooks" : ""}.`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
await main();
|