@imfusion/web-ui 0.5.1-dev.53.gb710322e → 0.5.1-dev.6.g9d275fa8
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 +20 -59
- package/bin/install-skill.js +180 -0
- package/dist/code-qBbqAHK-.js +190 -0
- package/dist/components/callout/callout.d.ts +1 -1
- package/dist/components/chip-link/chip-link.d.ts +1 -1
- package/dist/components/code/code.d.ts +18 -10
- package/dist/components/input/input.d.ts +1 -3
- package/dist/components/typo/typo.d.ts +2 -2
- package/dist/hooks/index.d.ts +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +5140 -5746
- package/dist/integrations/code-highlight/code-highlight.d.ts +6 -8
- package/dist/integrations/code-highlight/highlighter.d.ts +3 -32
- package/dist/integrations/code-highlight.js +59 -198
- package/dist/integrations/image-display-options.js +56 -56
- package/dist/meta-B8C51eyL.js +74 -0
- package/dist/provider/web-ui-provider.d.ts +1 -4
- package/dist/style.css +1 -1
- package/dist/{tabs-CYqtw1q9.js → tabs-DqBFSqq6.js} +2 -3
- package/package.json +26 -46
- package/src/docgen/doc.gen.json +38 -381
- package/src/llms/llms.gen.txt +0 -17
- package/src/llms/skills/imf-web-ui/SKILL.md +12 -13
- package/src/llms/skills/imf-web-ui-components/SKILL.md +3 -56
- package/src/llms/skills/imf-web-ui-frontend-patterns/SKILL.md +93 -0
- package/src/llms/skills/imf-web-ui-frontend-patterns/references/code-conventions.md +133 -0
- package/src/llms/skills/imf-web-ui-frontend-patterns/references/react-patterns.md +94 -0
- package/src/llms/skills/imf-web-ui-imfusion-frontend-setup/SKILL.md +201 -0
- package/src/llms/skills/imf-web-ui-setup/SKILL.md +37 -67
- package/src/llms/skills/imf-web-ui-ux/SKILL.md +4 -4
- package/src/llms/skills/imf-web-ui-ux/references/forms.md +2 -2
- package/bin/install.js +0 -428
- package/bin/install.test.ts +0 -329
- package/dist/build/vite-css-module-names/index.d.ts +0 -20
- package/dist/build/vite-css-module-names.js +0 -17
- package/dist/chunk-DA_OvQe2.js +0 -11
- package/dist/code-BRy_lg-h.js +0 -135
- package/dist/codegen/gen-icons.d.ts +0 -24
- package/dist/components/field/field.d.ts +0 -104
- package/dist/components/field/field.meta.d.ts +0 -2
- package/dist/components/field/index.d.ts +0 -2
- package/dist/components/fieldset/fieldset.d.ts +0 -29
- package/dist/components/fieldset/fieldset.meta.d.ts +0 -2
- package/dist/components/fieldset/index.d.ts +0 -2
- package/dist/components/icon/icon.d.ts +0 -16
- package/dist/components/icon/icon.meta.d.ts +0 -2
- package/dist/components/icon/index.d.ts +0 -4
- package/dist/components/icon/types.d.ts +0 -2
- package/dist/docgen/component-sources.d.ts +0 -7
- package/dist/hooks/use-resize-observer.d.ts +0 -2
- package/dist/icons/catalog.gen.d.ts +0 -8357
- package/dist/icons/icon-config-provider.d.ts +0 -8
- package/dist/icons/icon-context.d.ts +0 -4
- package/dist/icons/icons.gen.d.ts +0 -1672
- package/dist/icons/index.d.ts +0 -3
- package/dist/icons-BhogvWBV.js +0 -77
- package/dist/icons.js +0 -2
- package/dist/integrations/code-highlight/language-patterns.d.ts +0 -7
- package/dist/integrations/code-highlight/languages/cmake.d.ts +0 -1
- package/dist/integrations/code-highlight/languages/cpp.d.ts +0 -1
- package/dist/integrations/code-highlight/languages/python.d.ts +0 -1
- package/dist/llms/gen-tokens.d.ts +0 -7
- package/dist/meta-BwCupYyq.js +0 -64
- package/src/llms/icon-catalog.gen.json +0 -11203
- package/src/llms/install-templates/AGENTS.md +0 -34
- package/src/llms/install-templates/codex-hooks.json +0 -44
- package/src/llms/install-templates/hooks/baseline-staleness.sh +0 -17
- package/src/llms/install-templates/hooks/session-start.sh +0 -5
- package/src/llms/install-templates/hooks/stop.sh +0 -18
- package/src/llms/install-templates/hooks/subagent-start.sh +0 -5
- package/src/llms/install-templates/hooks/user-prompt-submit.sh +0 -5
- package/src/llms/install-templates/settings.json +0 -45
- package/src/llms/skills/imf-web-ui-audit/SKILL.md +0 -119
- package/src/llms/skills/imf-web-ui-conventions/SKILL.md +0 -57
- package/src/llms/skills/imf-web-ui-conventions/templates/AUDIT_CHECKLIST.md +0 -141
- package/src/llms/skills/imf-web-ui-conventions/templates/REPORT.md +0 -45
- package/src/llms/skills/imf-web-ui-conventions/topics/agent-tooling.md +0 -82
- package/src/llms/skills/imf-web-ui-conventions/topics/assets.md +0 -27
- package/src/llms/skills/imf-web-ui-conventions/topics/authentication.md +0 -65
- package/src/llms/skills/imf-web-ui-conventions/topics/class-names.md +0 -50
- package/src/llms/skills/imf-web-ui-conventions/topics/components.md +0 -101
- package/src/llms/skills/imf-web-ui-conventions/topics/data.md +0 -221
- package/src/llms/skills/imf-web-ui-conventions/topics/docs-structure.md +0 -40
- package/src/llms/skills/imf-web-ui-conventions/topics/git.md +0 -34
- package/src/llms/skills/imf-web-ui-conventions/topics/library-boundary.md +0 -35
- package/src/llms/skills/imf-web-ui-conventions/topics/library-setup.md +0 -26
- package/src/llms/skills/imf-web-ui-conventions/topics/npm-project.md +0 -53
- package/src/llms/skills/imf-web-ui-conventions/topics/project-structure.md +0 -44
- package/src/llms/skills/imf-web-ui-conventions/topics/react.md +0 -109
- package/src/llms/skills/imf-web-ui-conventions/topics/styling.md +0 -88
- package/src/llms/skills/imf-web-ui-conventions/topics/testing.md +0 -25
- package/src/llms/skills/imf-web-ui-conventions/topics/tokens.md +0 -7
- package/src/llms/skills/imf-web-ui-conventions/topics/tooling.md +0 -116
- package/src/llms/skills/imf-web-ui-conventions/topics/typescript.md +0 -73
- package/src/llms/skills/imf-web-ui-conventions/topics/validation.md +0 -62
- package/src/llms/skills/imf-web-ui-update/SKILL.md +0 -157
- package/src/llms/tokens.gen.json +0 -887
|
@@ -1,87 +1,57 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: imf-web-ui-setup
|
|
3
3
|
description:
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
setup action."
|
|
8
|
-
argument-hint: "[full|library-setup|tooling|git|npm-project|authentication|project-structure|docs-structure|data|testing|agent-tooling]"
|
|
9
|
-
allowed-tools: Read Glob Grep Write
|
|
4
|
+
"One-time wiring of a consumer project: the @imfusion/web-ui styles import and WebUIProvider wrapper. Also covers what to
|
|
5
|
+
say about the Agent Skills a dependency ships. Load when installing the library for the first time, or when components
|
|
6
|
+
render unstyled or without theme context."
|
|
10
7
|
---
|
|
11
8
|
|
|
12
9
|
# imf-web-ui-setup
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
topics, and prepare a complete proposal for every file you would create or change. The project wins where it already has a
|
|
16
|
-
working convention. This skill is plan-first: enter the host's plan mode before presenting the proposal, and do not create a
|
|
17
|
-
report file as a substitute for the host plan.
|
|
11
|
+
This is library wiring: the styles import and the provider. It applies to anyone using `@imfusion/web-ui`.
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
If the project is an **ImFusion** frontend and this is first-time setup, mention once that
|
|
14
|
+
`imf-web-ui-imfusion-frontend-setup` sets up or audits the repo's tooling (formatting, linting, hooks, scripts) against the
|
|
15
|
+
ImFusion baseline, and let the human decide. Offer it; never run it uninvited, and don't raise it again if they pass — the
|
|
16
|
+
library works fine without any of it.
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
merely _mentions_ one area ("wire up the tooling", "get the project set up") is still a `full` run — the words describe a
|
|
23
|
-
starting point, not a scope that excludes the rest. For a greenfield `full` run the application boundary (router, route
|
|
24
|
-
groups, current-user query, AppShell) is part of the deliverable even when the prompt never names it; propose it and let
|
|
25
|
-
the human remove it, never silently defer it as "out of scope." For an unsupported topic argument, list the available
|
|
26
|
-
setup topics and point the human to `imf-web-ui-audit` for a read-only report.
|
|
27
|
-
2. Enter the host's plan mode. If it is not already active, use the host plan-mode control before inspecting and planning.
|
|
28
|
-
3. Read the selected convention topics and inspect the repository with `Read`, `Glob`, and `Grep` only.
|
|
29
|
-
4. Build the complete proposal in the host plan from the shared report contract at
|
|
30
|
-
[`../imf-web-ui-conventions/templates/REPORT.md`](../imf-web-ui-conventions/templates/REPORT.md). Include the concrete
|
|
31
|
-
content of every file the approved setup would create or change, with repository evidence for each decision.
|
|
32
|
-
5. A greenfield `full` setup proposes the whole starter skeleton, not just tooling — tooling without the application boundary
|
|
33
|
-
is an unfinished setup. Include, as concrete files in the plan:
|
|
34
|
-
- the TanStack Router **package added to `package.json`** (with its router devtools as a dev dependency), not just
|
|
35
|
-
imported — code that imports an uninstalled package is an incomplete setup
|
|
36
|
-
([project-structure.md](../imf-web-ui-conventions/topics/project-structure.md));
|
|
37
|
-
- pathless `_public/` and `_app/` route groups behind one server-backed current-user boundary
|
|
38
|
-
([authentication.md](../imf-web-ui-conventions/topics/authentication.md));
|
|
39
|
-
- the minimal branded `AppShell` in `_app/` by default — omit the shell only when the human explicitly says the product
|
|
40
|
-
has no persistent authenticated navigation (ask first), but keep the `_app/` boundary either way;
|
|
41
|
-
- a per-topic `api/` folder for any data the first screen shows, never an inline fixture in the component
|
|
42
|
-
([data.md](../imf-web-ui-conventions/topics/data.md)).
|
|
18
|
+
Every consumer entry point needs exactly two lines, in this order:
|
|
43
19
|
|
|
44
|
-
|
|
45
|
-
|
|
20
|
+
```tsx
|
|
21
|
+
import "@imfusion/web-ui/styles.css";
|
|
22
|
+
import { WebUIProvider, Button } from "@imfusion/web-ui";
|
|
23
|
+
```
|
|
46
24
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
step and review the findings it reports back before declaring the setup complete. The audit stays out of plan mode here —
|
|
50
|
-
the human has just left it to let this work happen.
|
|
25
|
+
Wrap the app root in `<WebUIProvider>` once. Components rendered outside it won't have the theme/CSS-variable context they
|
|
26
|
+
expect.
|
|
51
27
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
28
|
+
Never import a Base UI (or other upstream) stylesheet or component directly — everything a web-ui component needs is already
|
|
29
|
+
inside `styles.css` and the package's own exports; reaching around web-ui to the upstream library is always wrong, even if
|
|
30
|
+
the upstream docs show it that way.
|
|
55
31
|
|
|
56
|
-
##
|
|
32
|
+
## Dependency-shipped skills
|
|
57
33
|
|
|
58
|
-
|
|
59
|
-
[
|
|
60
|
-
|
|
34
|
+
Some libraries ship Agent Skills inside their npm package; TanStack does across much of the suite.
|
|
35
|
+
[`@tanstack/intent`](https://github.com/TanStack/intent) is the CLI that surfaces them — an agent holding a dependency but
|
|
36
|
+
not its guidance writes plausible code against a half-remembered API.
|
|
61
37
|
|
|
62
|
-
|
|
38
|
+
Setting it up is the project's own call, not something web-ui does on its behalf. Point it out:
|
|
63
39
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
and report runtime or machine-local state that cannot be established statically as unverified. After approval, `Write` is
|
|
67
|
-
limited to the files listed in the approved report; anything needing execution goes in the plan for the human.
|
|
40
|
+
> This project has TanStack dependencies that ship their own Agent Skills. `@tanstack/intent` can make them reachable — worth
|
|
41
|
+
> a look if you want your agent working from the library's own guidance.
|
|
68
42
|
|
|
69
|
-
|
|
43
|
+
Intent offers two things: a fenced instructions block in `AGENTS.md`, and a `PreToolUse` hook that blocks an edit until a
|
|
44
|
+
matching skill has been read. The house preference is both — the block alone is advice an agent can walk past. The hook
|
|
45
|
+
refuses every edit while no matching skill is loadable, so a project adopting it wants the current docs open; that sequencing
|
|
46
|
+
belongs to whoever runs it.
|
|
70
47
|
|
|
71
|
-
|
|
48
|
+
Whatever the project decides, guidance you didn't read is not guidance you have: use `npx @tanstack/cli` for TanStack docs,
|
|
49
|
+
and never guess at a skill name.
|
|
72
50
|
|
|
73
|
-
|
|
74
|
-
| ------------------- | ------------------------------------------------------------------------------ |
|
|
75
|
-
| `library-setup` | styles import, `WebUIProvider`, and library package wiring |
|
|
76
|
-
| `tooling` | dependency selection, devtools, Prettier, ESLint, TypeScript, and verification |
|
|
77
|
-
| `git` | tracked hooks, verification scopes, and staleness wiring |
|
|
78
|
-
| `npm-project` | package metadata, scripts, pins, npm, and Node configuration |
|
|
79
|
-
| `authentication` | current-user query, public/app guards, login, and logout |
|
|
80
|
-
| `project-structure` | source tree, route groups, optional app shell, naming, and placement |
|
|
81
|
-
| `docs-structure` | README, AGENTS, docs index, and content boundaries |
|
|
82
|
-
| `data` | transport, schemas, query/mutation options, keys, and invalidation |
|
|
83
|
-
| `testing` | test boundaries and verification coverage |
|
|
84
|
-
| `agent-tooling` | vendored skills, AGENTS fence, lifecycle hooks, registrations, and staleness |
|
|
51
|
+
## Symptoms of a broken setup
|
|
85
52
|
|
|
86
|
-
|
|
87
|
-
|
|
53
|
+
- **Components render but look unstyled** — the `styles.css` import is missing from the entry point.
|
|
54
|
+
- **Components render but ignore the theme (wrong colors, no CSS variables resolving)** — they're mounted outside
|
|
55
|
+
`<WebUIProvider>`.
|
|
56
|
+
- **An integration component throws on import** — its optional peer dependency isn't installed; check the component's
|
|
57
|
+
description in the docgen index (`imf-web-ui-components`) for which peer to add to your `package.json`.
|
|
@@ -11,7 +11,7 @@ description:
|
|
|
11
11
|
Most teams consuming `@imfusion/web-ui` don't have a designer on call. This skill stands in: it encodes the library authors'
|
|
12
12
|
UX experience — the whole experience of a screen, its visual design, and the usability where both meet. Follow it by default;
|
|
13
13
|
deviate when the product has a real reason to. Code-level patterns (tokens, layers, wrappers) live in
|
|
14
|
-
`imf-web-ui-
|
|
14
|
+
`imf-web-ui-frontend-patterns`; project wiring lives in `imf-web-ui-setup`.
|
|
15
15
|
|
|
16
16
|
Component names below are real — verify any API against the docgen index (`imf-web-ui-components`) before use. Never invent a
|
|
17
17
|
component this library doesn't ship.
|
|
@@ -82,13 +82,13 @@ Every screen ships four states, not one:
|
|
|
82
82
|
|
|
83
83
|
Custom UI the library doesn't cover should be indistinguishable from library UI: build it from `--imf-ui-*` tokens and
|
|
84
84
|
compose it with library primitives. The goal lives here; the mechanics (tokens, layers, wrappers) live in
|
|
85
|
-
`imf-web-ui-
|
|
85
|
+
`imf-web-ui-frontend-patterns`.
|
|
86
86
|
|
|
87
87
|
## Experimental components
|
|
88
88
|
|
|
89
89
|
The identity index marks each component `stable` or `experimental`. Experimental ones are fine to use, but expect API
|
|
90
|
-
movement across releases — prefer wrapping them once (see `imf-web-ui-
|
|
91
|
-
not forty call sites.
|
|
90
|
+
movement across releases — prefer wrapping them once (see `imf-web-ui-frontend-patterns`) so a breaking change lands in one
|
|
91
|
+
file, not forty call sites.
|
|
92
92
|
|
|
93
93
|
## Deep dives
|
|
94
94
|
|
|
@@ -8,8 +8,8 @@ non-compliant ones. Read before building any form beyond two fields.
|
|
|
8
8
|
|
|
9
9
|
The library ships the controls (`Input`, `Select`, `Checkbox`, `Switch`, `Slider`) but no form or field wrapper, so labels,
|
|
10
10
|
grouping, and where errors appear are composed by you. That's exactly where these rules apply. Composing the markup is not
|
|
11
|
-
the same as owning the state: form state and validation belong to a form library (see `imf-web-ui-
|
|
12
|
-
rules govern how its errors get presented.
|
|
11
|
+
the same as owning the state: form state and validation belong to a form library (see `imf-web-ui-frontend-patterns`), and
|
|
12
|
+
these rules govern how its errors get presented.
|
|
13
13
|
|
|
14
14
|
## Structure
|
|
15
15
|
|
package/bin/install.js
DELETED
|
@@ -1,428 +0,0 @@
|
|
|
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 in the installer-owned template home. 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 after the
|
|
17
|
-
// consumer checks the repo's existing hooks and still installs the skills.
|
|
18
|
-
// --target claude|agents
|
|
19
|
-
// (repeatable) selects scripted install targets,
|
|
20
|
-
// --reconfigure to re-open the target prompt on an existing install.
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
chmodSync,
|
|
24
|
-
cpSync,
|
|
25
|
-
existsSync,
|
|
26
|
-
lstatSync,
|
|
27
|
-
mkdirSync,
|
|
28
|
-
readdirSync,
|
|
29
|
-
readFileSync,
|
|
30
|
-
rmSync,
|
|
31
|
-
symlinkSync,
|
|
32
|
-
writeFileSync
|
|
33
|
-
} from "node:fs";
|
|
34
|
-
import { spawnSync } from "node:child_process";
|
|
35
|
-
import { dirname, relative, resolve } from "node:path";
|
|
36
|
-
import { fileURLToPath } from "node:url";
|
|
37
|
-
import * as p from "@clack/prompts";
|
|
38
|
-
|
|
39
|
-
const SKILL_PREFIX = "imf-web-ui";
|
|
40
|
-
const VERSION_MARKER = ".imf-web-ui-skill-version.json";
|
|
41
|
-
|
|
42
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
43
|
-
// Source is relative to THIS SCRIPT's location (inside node_modules), not
|
|
44
|
-
// the consumer's cwd — the script is invoked from the consumer's project
|
|
45
|
-
// root, but the skills it copies ship alongside this file in the package.
|
|
46
|
-
const skillsRoot = resolve(here, "..", "src", "llms", "skills");
|
|
47
|
-
const packageJson = JSON.parse(readFileSync(resolve(here, "..", "package.json"), "utf-8"));
|
|
48
|
-
const currentVersion = packageJson.version;
|
|
49
|
-
|
|
50
|
-
// Destination is relative to the consumer's project root (cwd), since
|
|
51
|
-
// that's where their `.claude/` or `.agents/` directory lives.
|
|
52
|
-
const projectRoot = process.cwd();
|
|
53
|
-
|
|
54
|
-
const TARGETS = {
|
|
55
|
-
claude: { label: "Claude Code", root: resolve(projectRoot, ".claude", "skills") },
|
|
56
|
-
agents: { label: "Vendor-neutral (.agents/)", root: resolve(projectRoot, ".agents", "skills") }
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
function discoverSkills() {
|
|
60
|
-
if (!existsSync(skillsRoot)) return [];
|
|
61
|
-
return readdirSync(skillsRoot, { withFileTypes: true })
|
|
62
|
-
.filter(entry => entry.isDirectory() && entry.name.startsWith(SKILL_PREFIX))
|
|
63
|
-
.map(entry => entry.name)
|
|
64
|
-
.sort();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function displayPath(absPath) {
|
|
68
|
-
return `./${relative(projectRoot, absPath)}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function readInstalledVersion(dir) {
|
|
72
|
-
const markerPath = resolve(dir, VERSION_MARKER);
|
|
73
|
-
if (!existsSync(markerPath)) return null;
|
|
74
|
-
try {
|
|
75
|
-
return JSON.parse(readFileSync(markerPath, "utf-8")).version ?? null;
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function writeRealCopy(sourceDir, destDir) {
|
|
82
|
-
mkdirSync(dirname(destDir), { recursive: true });
|
|
83
|
-
// force: true makes re-running after a version bump overwrite cleanly.
|
|
84
|
-
cpSync(sourceDir, destDir, { recursive: true, force: true });
|
|
85
|
-
writeFileSync(resolve(destDir, VERSION_MARKER), JSON.stringify({ version: currentVersion }, null, 2) + "\n");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function writeSymlink(linkPath, targetPath) {
|
|
89
|
-
mkdirSync(dirname(linkPath), { recursive: true });
|
|
90
|
-
// lstat (not existsSync, which follows symlinks) catches a broken/stale
|
|
91
|
-
// symlink left over from a prior run, not just a real file or directory.
|
|
92
|
-
if (lstatSync(linkPath, { throwIfNoEntry: false })) {
|
|
93
|
-
rmSync(linkPath, { recursive: true, force: true });
|
|
94
|
-
}
|
|
95
|
-
symlinkSync(relative(dirname(linkPath), targetPath), linkPath);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function removeOrphanedSkills(selected, skills, both) {
|
|
99
|
-
const bundledSkills = new Set(skills);
|
|
100
|
-
const selectedTargets = both ? ["agents", "claude"] : selected;
|
|
101
|
-
|
|
102
|
-
for (const key of selectedTargets) {
|
|
103
|
-
const { root } = TARGETS[key];
|
|
104
|
-
if (!existsSync(root)) continue;
|
|
105
|
-
|
|
106
|
-
const orphanedSkills = readdirSync(root).filter(name => name.startsWith(`${SKILL_PREFIX}-`) && !bundledSkills.has(name));
|
|
107
|
-
for (const name of orphanedSkills) {
|
|
108
|
-
rmSync(resolve(root, name), { recursive: true, force: true });
|
|
109
|
-
// In the two-target layout, .claude/ is a symlinked view of the
|
|
110
|
-
// canonical .agents/ copy. Remove its matching link along with the
|
|
111
|
-
// orphaned canonical skill.
|
|
112
|
-
if (both && key === "agents") {
|
|
113
|
-
rmSync(resolve(TARGETS.claude.root, name), { recursive: true, force: true });
|
|
114
|
-
}
|
|
115
|
-
p.log.info(`Removed orphaned skill: ${name} (no longer in this bundle)`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// A prior install is recoverable from the filesystem, so a re-run doesn't
|
|
121
|
-
// re-ask: a target counts as chosen when any bundled skill is present under
|
|
122
|
-
// it. Symlinks count — they're how the both-targets layout represents
|
|
123
|
-
// .claude/, and lstat avoids following them into the real copy.
|
|
124
|
-
function detectInstalledTargets(skills) {
|
|
125
|
-
return Object.keys(TARGETS).filter(key =>
|
|
126
|
-
skills.some(name => lstatSync(resolve(TARGETS[key].root, name), { throwIfNoEntry: false }))
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Hook templates are installer-owned; the installer copies the scripts into the
|
|
131
|
-
// consumer's .agents/hooks/ and merges the registrations into the host
|
|
132
|
-
// registration files: .claude/settings.json from settings.json, .codex/hooks.json
|
|
133
|
-
// from codex-hooks.json. One pristine template per host, merged verbatim.
|
|
134
|
-
const installTemplatesRoot = resolve(here, "..", "src", "llms", "install-templates");
|
|
135
|
-
const hooksSourceDir = resolve(installTemplatesRoot, "hooks");
|
|
136
|
-
const hooksSettingsTemplate = resolve(installTemplatesRoot, "settings.json");
|
|
137
|
-
const codexHooksTemplate = resolve(installTemplatesRoot, "codex-hooks.json");
|
|
138
|
-
// Installer-owned subdirectory: refreshed wholesale on every run, so a repo's
|
|
139
|
-
// own hooks in .agents/hooks/ are never touched.
|
|
140
|
-
const hooksDestDir = resolve(projectRoot, ".agents", "hooks", "imf-web-ui");
|
|
141
|
-
const settingsPath = resolve(projectRoot, ".claude", "settings.json");
|
|
142
|
-
const codexHooksPath = resolve(projectRoot, ".codex", "hooks.json");
|
|
143
|
-
|
|
144
|
-
// Serializing the file reflows entries this installer doesn't own; the
|
|
145
|
-
// consumer's own Prettier puts them back. Best-effort: absent, or the path is
|
|
146
|
-
// .prettierignore'd, nothing happens.
|
|
147
|
-
function formatSettings(path) {
|
|
148
|
-
let toolRoot = projectRoot;
|
|
149
|
-
while (toolRoot !== dirname(toolRoot) && !existsSync(resolve(toolRoot, "node_modules", ".bin"))) {
|
|
150
|
-
toolRoot = dirname(toolRoot);
|
|
151
|
-
}
|
|
152
|
-
const prettier = resolve(toolRoot, "node_modules", ".bin", "prettier");
|
|
153
|
-
if (!existsSync(prettier)) return;
|
|
154
|
-
spawnSync(prettier, ["--write", path], { cwd: projectRoot, stdio: "ignore" });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function installHookScripts() {
|
|
158
|
-
const scripts = readdirSync(hooksSourceDir).filter(name => name.endsWith(".sh"));
|
|
159
|
-
mkdirSync(hooksDestDir, { recursive: true });
|
|
160
|
-
for (const name of scripts) {
|
|
161
|
-
const dest = resolve(hooksDestDir, name);
|
|
162
|
-
cpSync(resolve(hooksSourceDir, name), dest, { force: true });
|
|
163
|
-
chmodSync(dest, 0o755);
|
|
164
|
-
}
|
|
165
|
-
// The directory is installer-owned: a script dropped from the template is
|
|
166
|
-
// removed on refresh, so a stale hook can't keep running after an upgrade.
|
|
167
|
-
const removed = readdirSync(hooksDestDir).filter(name => name.endsWith(".sh") && !scripts.includes(name));
|
|
168
|
-
for (const name of removed) {
|
|
169
|
-
rmSync(resolve(hooksDestDir, name), { force: true });
|
|
170
|
-
}
|
|
171
|
-
return { installed: scripts, removed };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// A registration is identified by the script it runs, not the command string:
|
|
175
|
-
// "$VAR/..." and "${VAR}/..." are the same hook.
|
|
176
|
-
const HOOKS_DIR_SEGMENT = ".agents/hooks/imf-web-ui/";
|
|
177
|
-
|
|
178
|
-
function hookScriptId(command) {
|
|
179
|
-
if (typeof command !== "string") return null;
|
|
180
|
-
const at = command.lastIndexOf(HOOKS_DIR_SEGMENT);
|
|
181
|
-
if (at === -1) return null;
|
|
182
|
-
// Only a bare script name (plus an optional closing quote) identifies a
|
|
183
|
-
// registration as installer-owned. Trailing arguments or redirection mean
|
|
184
|
-
// the consumer customized the command; that counts as foreign, so the
|
|
185
|
-
// stale-prune below can never delete it.
|
|
186
|
-
const match = command.slice(at + HOOKS_DIR_SEGMENT.length).match(/^([\w.-]+)["']?$/);
|
|
187
|
-
return match ? match[1] : null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function entryScriptIds(entry) {
|
|
191
|
-
return (entry.hooks ?? []).map(hook => hookScriptId(hook.command)).filter(Boolean);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Merge, never clobber: a registration is added only when no existing entry
|
|
195
|
-
// for that event already runs the same script, so re-runs are idempotent and
|
|
196
|
-
// hand-written settings survive. The one exception is entries that only run
|
|
197
|
-
// scripts from the installer-owned directory that are no longer shipped —
|
|
198
|
-
// those are pruned, or they would point at a script the refresh just removed.
|
|
199
|
-
// A run that changes nothing writes nothing.
|
|
200
|
-
function mergeHookRegistrations(targetPath, templateHooks, shippedScripts) {
|
|
201
|
-
let settings = {};
|
|
202
|
-
if (existsSync(targetPath)) {
|
|
203
|
-
try {
|
|
204
|
-
settings = JSON.parse(readFileSync(targetPath, "utf-8"));
|
|
205
|
-
} catch {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
settings.hooks ??= {};
|
|
210
|
-
let added = 0;
|
|
211
|
-
let removed = 0;
|
|
212
|
-
// Pruning is per hook, not per entry: only an individual hook that
|
|
213
|
-
// unambiguously runs a no-longer-shipped installer-owned script goes;
|
|
214
|
-
// consumer hooks sharing the same entry stay.
|
|
215
|
-
const emptiedEvents = [];
|
|
216
|
-
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
217
|
-
if (!Array.isArray(entries)) continue;
|
|
218
|
-
let pruned = 0;
|
|
219
|
-
const kept = entries
|
|
220
|
-
.map(entry => {
|
|
221
|
-
if (!Array.isArray(entry.hooks)) return entry;
|
|
222
|
-
const hooks = entry.hooks.filter(hook => {
|
|
223
|
-
const script = hookScriptId(hook.command);
|
|
224
|
-
return script === null || shippedScripts.includes(script);
|
|
225
|
-
});
|
|
226
|
-
pruned += entry.hooks.length - hooks.length;
|
|
227
|
-
return hooks.length === entry.hooks.length ? entry : { ...entry, hooks };
|
|
228
|
-
})
|
|
229
|
-
.filter(entry => !Array.isArray(entry.hooks) || entry.hooks.length > 0);
|
|
230
|
-
if (pruned === 0) continue;
|
|
231
|
-
removed += pruned;
|
|
232
|
-
if (kept.length === 0 && !(event in templateHooks)) emptiedEvents.push(event);
|
|
233
|
-
else settings.hooks[event] = kept;
|
|
234
|
-
}
|
|
235
|
-
// An event whose entries were all pruned disappears instead of lingering as [].
|
|
236
|
-
if (emptiedEvents.length > 0) {
|
|
237
|
-
settings.hooks = Object.fromEntries(Object.entries(settings.hooks).filter(([event]) => !emptiedEvents.includes(event)));
|
|
238
|
-
}
|
|
239
|
-
for (const [event, entries] of Object.entries(templateHooks)) {
|
|
240
|
-
settings.hooks[event] ??= [];
|
|
241
|
-
for (const entry of entries) {
|
|
242
|
-
const scripts = entryScriptIds(entry);
|
|
243
|
-
const present = settings.hooks[event].some(existing =>
|
|
244
|
-
entryScriptIds(existing).some(script => scripts.includes(script))
|
|
245
|
-
);
|
|
246
|
-
if (!present) {
|
|
247
|
-
settings.hooks[event].push(entry);
|
|
248
|
-
added++;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (added === 0 && removed === 0) return { added, removed };
|
|
253
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
254
|
-
writeFileSync(targetPath, JSON.stringify(settings, null, 2) + "\n");
|
|
255
|
-
formatSettings(targetPath);
|
|
256
|
-
return { added, removed };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// The AGENTS.md baseline note lives inside a fenced block owned by this
|
|
260
|
-
// installer. The template between the markers is the source of truth; an
|
|
261
|
-
// existing AGENTS.md gets the block replaced in place (or appended when
|
|
262
|
-
// absent), everything outside the fence is untouched. No AGENTS.md at all
|
|
263
|
-
// is left alone — scaffolding one is the setup skill's job.
|
|
264
|
-
const AGENTS_BLOCK_BEGIN = "<!-- imf-web-ui:begin";
|
|
265
|
-
const AGENTS_BLOCK_END = "<!-- imf-web-ui:end -->";
|
|
266
|
-
const agentsTemplatePath = resolve(installTemplatesRoot, "AGENTS.md");
|
|
267
|
-
const agentsPath = resolve(projectRoot, "AGENTS.md");
|
|
268
|
-
|
|
269
|
-
function extractAgentsBlock(content) {
|
|
270
|
-
const begin = content.indexOf(AGENTS_BLOCK_BEGIN);
|
|
271
|
-
const end = content.indexOf(AGENTS_BLOCK_END);
|
|
272
|
-
if (begin === -1 || end === -1) return null;
|
|
273
|
-
return content.slice(begin, end + AGENTS_BLOCK_END.length);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function upsertAgentsBlock() {
|
|
277
|
-
if (!existsSync(agentsPath)) return "absent";
|
|
278
|
-
const block = extractAgentsBlock(readFileSync(agentsTemplatePath, "utf-8"));
|
|
279
|
-
if (!block) return "absent";
|
|
280
|
-
const current = readFileSync(agentsPath, "utf-8");
|
|
281
|
-
const existing = extractAgentsBlock(current);
|
|
282
|
-
const next = existing ? current.replace(existing, block) : `${current.trimEnd()}\n\n${block}\n`;
|
|
283
|
-
if (next === current) return "unchanged";
|
|
284
|
-
writeFileSync(agentsPath, next);
|
|
285
|
-
return existing ? "updated" : "added";
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// --target claude|agents (repeatable) selects targets without the
|
|
289
|
-
// interactive prompt — for CI and scripted installs.
|
|
290
|
-
function parseTargetFlags(argv) {
|
|
291
|
-
const targets = [];
|
|
292
|
-
for (let i = 0; i < argv.length; i++) {
|
|
293
|
-
if (argv[i] !== "--target") continue;
|
|
294
|
-
const value = argv[i + 1];
|
|
295
|
-
if (!value || !(value in TARGETS)) {
|
|
296
|
-
console.error(`--target expects one of: ${Object.keys(TARGETS).join(", ")}`);
|
|
297
|
-
process.exit(1);
|
|
298
|
-
}
|
|
299
|
-
targets.push(value);
|
|
300
|
-
i++;
|
|
301
|
-
}
|
|
302
|
-
return targets;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function main() {
|
|
306
|
-
const skills = discoverSkills();
|
|
307
|
-
const argv = process.argv.slice(2);
|
|
308
|
-
const flagTargets = parseTargetFlags(argv);
|
|
309
|
-
const reconfigure = argv.includes("--reconfigure");
|
|
310
|
-
// Every install includes the skill bundle. Hooks are opt-in via --hooks —
|
|
311
|
-
// the consumer's setup workflow drives that after checking what the repo
|
|
312
|
-
// already registers, but hooks without their companion skills are invalid.
|
|
313
|
-
const wantHooks = argv.includes("--hooks");
|
|
314
|
-
const wantSkills = true;
|
|
315
|
-
|
|
316
|
-
const components = [wantSkills && `${skills.length} skills`, wantHooks && "agent hooks"].filter(Boolean);
|
|
317
|
-
p.intro(`@imfusion/web-ui install — ${components.join(" + ")}`);
|
|
318
|
-
|
|
319
|
-
if (wantSkills && skills.length === 0) {
|
|
320
|
-
p.log.error(`No skills found at ${skillsRoot}. Reinstall @imfusion/web-ui and try again.`);
|
|
321
|
-
p.outro("Nothing installed.");
|
|
322
|
-
process.exitCode = 1;
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (wantHooks) {
|
|
327
|
-
const { installed, removed } = installHookScripts();
|
|
328
|
-
p.log.success(`Agent hooks -> ${displayPath(hooksDestDir)} (${installed.length} scripts)`);
|
|
329
|
-
if (removed.length > 0) {
|
|
330
|
-
p.log.info(`Removed stale hook script(s): ${removed.join(", ")} (no longer shipped)`);
|
|
331
|
-
}
|
|
332
|
-
const registrations = [
|
|
333
|
-
[settingsPath, JSON.parse(readFileSync(hooksSettingsTemplate, "utf-8")).hooks],
|
|
334
|
-
[codexHooksPath, JSON.parse(readFileSync(codexHooksTemplate, "utf-8")).hooks]
|
|
335
|
-
];
|
|
336
|
-
for (const [path, hooks] of registrations) {
|
|
337
|
-
const result = mergeHookRegistrations(path, hooks, installed);
|
|
338
|
-
if (result === null) {
|
|
339
|
-
p.log.warn(`${displayPath(path)} is not valid JSON — registrations not merged, fix it and re-run.`);
|
|
340
|
-
} else if (result.added > 0 || result.removed > 0) {
|
|
341
|
-
const changes = [
|
|
342
|
-
result.added > 0 && `registered ${result.added}`,
|
|
343
|
-
result.removed > 0 && `removed ${result.removed} stale`
|
|
344
|
-
].filter(Boolean);
|
|
345
|
-
p.log.success(`${changes.join(", ")} hook registration(s) in ${displayPath(path)}`);
|
|
346
|
-
} else {
|
|
347
|
-
p.log.info(`Hook registrations already present in ${displayPath(path)}`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const installedTargets = detectInstalledTargets(skills);
|
|
353
|
-
|
|
354
|
-
const existingVersions = Object.values(TARGETS)
|
|
355
|
-
.flatMap(({ root }) => skills.map(name => readInstalledVersion(resolve(root, name))))
|
|
356
|
-
.filter(Boolean);
|
|
357
|
-
if (existingVersions.length > 0 && existingVersions.every(v => v === currentVersion)) {
|
|
358
|
-
p.log.info(`Already up to date (v${currentVersion}). Re-running will overwrite with the same content.`);
|
|
359
|
-
} else if (existingVersions.some(v => v !== currentVersion)) {
|
|
360
|
-
const from = existingVersions.find(v => v !== currentVersion);
|
|
361
|
-
p.log.info(`Updating installed skills from v${from} to v${currentVersion}.`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
p.log.message(`Skills in this bundle:\n${skills.map(name => ` - ${name}`).join("\n")}`);
|
|
365
|
-
|
|
366
|
-
let selected;
|
|
367
|
-
if (flagTargets.length > 0) {
|
|
368
|
-
selected = flagTargets;
|
|
369
|
-
p.log.info(`Targets from --target flags: ${selected.join(", ")}`);
|
|
370
|
-
} else if (installedTargets.length > 0 && !reconfigure) {
|
|
371
|
-
selected = installedTargets;
|
|
372
|
-
p.log.info(
|
|
373
|
-
`Refreshing the existing install: ${selected.map(key => displayPath(TARGETS[key].root)).join(", ")}` +
|
|
374
|
-
` — pass --reconfigure to choose different targets.`
|
|
375
|
-
);
|
|
376
|
-
} else {
|
|
377
|
-
selected = await p.multiselect({
|
|
378
|
-
message: "Install into which skill directory (or directories)?",
|
|
379
|
-
options: Object.entries(TARGETS).map(([key, { label, root }]) => ({
|
|
380
|
-
value: key,
|
|
381
|
-
label,
|
|
382
|
-
hint: displayPath(root)
|
|
383
|
-
})),
|
|
384
|
-
required: true
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
if (p.isCancel(selected)) {
|
|
388
|
-
p.cancel("Cancelled — nothing installed.");
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const both = selected.includes("claude") && selected.includes("agents");
|
|
394
|
-
|
|
395
|
-
for (const name of skills) {
|
|
396
|
-
const sourceDir = resolve(skillsRoot, name);
|
|
397
|
-
if (both) {
|
|
398
|
-
// .agents/ is the canonical real copy; .claude/ aliases it via symlink.
|
|
399
|
-
const realDir = resolve(TARGETS.agents.root, name);
|
|
400
|
-
writeRealCopy(sourceDir, realDir);
|
|
401
|
-
writeSymlink(resolve(TARGETS.claude.root, name), realDir);
|
|
402
|
-
} else {
|
|
403
|
-
for (const key of selected) {
|
|
404
|
-
writeRealCopy(sourceDir, resolve(TARGETS[key].root, name));
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
removeOrphanedSkills(selected, skills, both);
|
|
410
|
-
|
|
411
|
-
if (both) {
|
|
412
|
-
p.log.success(`Vendor-neutral (.agents/) -> ${displayPath(TARGETS.agents.root)}/${SKILL_PREFIX}*`);
|
|
413
|
-
p.log.success(`Claude Code -> ${displayPath(TARGETS.claude.root)}/${SKILL_PREFIX}* (symlinks -> .agents/)`);
|
|
414
|
-
} else {
|
|
415
|
-
for (const key of selected) {
|
|
416
|
-
p.log.success(`${TARGETS[key].label} -> ${displayPath(TARGETS[key].root)}/${SKILL_PREFIX}*`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const agentsResult = upsertAgentsBlock();
|
|
421
|
-
if (agentsResult === "updated" || agentsResult === "added") {
|
|
422
|
-
p.log.success(`Refreshed the imf-web-ui block in ${displayPath(agentsPath)}`);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
p.outro(`Done — ${skills.length} skills installed${wantHooks ? " + agent hooks" : ""}.`);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
await main();
|