@imfusion/web-ui 0.5.1-dev.28.g1156ad00 → 0.5.1-dev.30.gf9b152db
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/bin/install.js +35 -4
- package/bin/install.test.ts +48 -2
- package/package.json +2 -2
package/bin/install.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
symlinkSync,
|
|
31
31
|
writeFileSync
|
|
32
32
|
} from "node:fs";
|
|
33
|
+
import { spawnSync } from "node:child_process";
|
|
33
34
|
import { dirname, relative, resolve } from "node:path";
|
|
34
35
|
import { fileURLToPath } from "node:url";
|
|
35
36
|
import * as p from "@clack/prompts";
|
|
@@ -136,6 +137,19 @@ const hooksSettingsTemplate = resolve(skillsRoot, "imf-web-ui-agent-setup", "tem
|
|
|
136
137
|
const hooksDestDir = resolve(projectRoot, ".agents", "hooks", "imf-web-ui");
|
|
137
138
|
const settingsPath = resolve(projectRoot, ".claude", "settings.json");
|
|
138
139
|
|
|
140
|
+
// Serializing the file reflows entries this installer doesn't own; the
|
|
141
|
+
// consumer's own Prettier puts them back. Best-effort: absent, or the path is
|
|
142
|
+
// .prettierignore'd, nothing happens.
|
|
143
|
+
function formatSettings() {
|
|
144
|
+
let toolRoot = projectRoot;
|
|
145
|
+
while (toolRoot !== dirname(toolRoot) && !existsSync(resolve(toolRoot, "node_modules", ".bin"))) {
|
|
146
|
+
toolRoot = dirname(toolRoot);
|
|
147
|
+
}
|
|
148
|
+
const prettier = resolve(toolRoot, "node_modules", ".bin", "prettier");
|
|
149
|
+
if (!existsSync(prettier)) return;
|
|
150
|
+
spawnSync(prettier, ["--write", settingsPath], { cwd: projectRoot, stdio: "ignore" });
|
|
151
|
+
}
|
|
152
|
+
|
|
139
153
|
function installHookScripts() {
|
|
140
154
|
const scripts = readdirSync(hooksSourceDir).filter(name => name.endsWith(".sh"));
|
|
141
155
|
mkdirSync(hooksDestDir, { recursive: true });
|
|
@@ -147,9 +161,24 @@ function installHookScripts() {
|
|
|
147
161
|
return scripts.length;
|
|
148
162
|
}
|
|
149
163
|
|
|
164
|
+
// A registration is identified by the script it runs, not the command string:
|
|
165
|
+
// "$VAR/..." and "${VAR}/..." are the same hook.
|
|
166
|
+
const HOOKS_DIR_SEGMENT = ".agents/hooks/imf-web-ui/";
|
|
167
|
+
|
|
168
|
+
function hookScriptId(command) {
|
|
169
|
+
if (typeof command !== "string") return null;
|
|
170
|
+
const at = command.lastIndexOf(HOOKS_DIR_SEGMENT);
|
|
171
|
+
if (at === -1) return null;
|
|
172
|
+
return command.slice(at + HOOKS_DIR_SEGMENT.length).replace(/["'\s]+$/, "");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function entryScriptIds(entry) {
|
|
176
|
+
return (entry.hooks ?? []).map(hook => hookScriptId(hook.command)).filter(Boolean);
|
|
177
|
+
}
|
|
178
|
+
|
|
150
179
|
// Merge, never clobber: a registration is added only when no existing entry
|
|
151
|
-
// for that event already runs the same
|
|
152
|
-
//
|
|
180
|
+
// for that event already runs the same script, so re-runs are idempotent and
|
|
181
|
+
// hand-written settings survive. A run that adds nothing writes nothing.
|
|
153
182
|
function mergeHookRegistrations() {
|
|
154
183
|
const template = JSON.parse(readFileSync(hooksSettingsTemplate, "utf-8"));
|
|
155
184
|
let settings = {};
|
|
@@ -165,9 +194,9 @@ function mergeHookRegistrations() {
|
|
|
165
194
|
for (const [event, entries] of Object.entries(template.hooks)) {
|
|
166
195
|
settings.hooks[event] ??= [];
|
|
167
196
|
for (const entry of entries) {
|
|
168
|
-
const
|
|
197
|
+
const scripts = entryScriptIds(entry);
|
|
169
198
|
const present = settings.hooks[event].some(existing =>
|
|
170
|
-
(existing
|
|
199
|
+
entryScriptIds(existing).some(script => scripts.includes(script))
|
|
171
200
|
);
|
|
172
201
|
if (!present) {
|
|
173
202
|
settings.hooks[event].push(entry);
|
|
@@ -175,8 +204,10 @@ function mergeHookRegistrations() {
|
|
|
175
204
|
}
|
|
176
205
|
}
|
|
177
206
|
}
|
|
207
|
+
if (added === 0) return 0;
|
|
178
208
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
179
209
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
210
|
+
formatSettings();
|
|
180
211
|
return added;
|
|
181
212
|
}
|
|
182
213
|
|
package/bin/install.test.ts
CHANGED
|
@@ -27,7 +27,7 @@ function runInstall(cwd: string, ...args: string[]) {
|
|
|
27
27
|
function readSettings(cwd: string) {
|
|
28
28
|
return JSON.parse(readFileSync(join(cwd, ".claude", "settings.json"), "utf-8")) as {
|
|
29
29
|
permissions?: { allow: string[] };
|
|
30
|
-
hooks: Record<string, { hooks: { command
|
|
30
|
+
hooks: Record<string, { hooks: { command?: string }[] }[]>;
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -103,7 +103,53 @@ describe("web-ui-install", () => {
|
|
|
103
103
|
const commands = settings.hooks.PostToolUse.flatMap(entry => entry.hooks.map(hook => hook.command));
|
|
104
104
|
expect(commands).toContain("echo existing");
|
|
105
105
|
// the shipped registration is added exactly once across both runs
|
|
106
|
-
expect(commands.filter(command => command
|
|
106
|
+
expect(commands.filter(command => command?.includes("post-tool-use.sh"))).toHaveLength(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("recognises registrations written with braced ${CLAUDE_PROJECT_DIR}", () => {
|
|
110
|
+
mkdirSync(join(consumer, ".claude"), { recursive: true });
|
|
111
|
+
writeFileSync(
|
|
112
|
+
join(consumer, ".claude", "settings.json"),
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
hooks: {
|
|
115
|
+
PostToolUse: [
|
|
116
|
+
{
|
|
117
|
+
matcher: "Edit|MultiEdit|Write",
|
|
118
|
+
hooks: [
|
|
119
|
+
{
|
|
120
|
+
type: "command",
|
|
121
|
+
command: '"${CLAUDE_PROJECT_DIR}/.agents/hooks/imf-web-ui/post-tool-use.sh"',
|
|
122
|
+
statusMessage: "Verifying file"
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
// a foreign entry carrying no `command` at all must not throw
|
|
127
|
+
{ matcher: "Bash", hooks: [{ type: "command", args: ["${CLAUDE_PROJECT_DIR}/other/gate.mjs"] }] }
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
runInstall(consumer, "--hooks");
|
|
134
|
+
|
|
135
|
+
const settings = readSettings(consumer);
|
|
136
|
+
const commands = settings.hooks.PostToolUse.flatMap(entry => entry.hooks.map(hook => hook.command));
|
|
137
|
+
expect(commands.filter(command => command?.includes("post-tool-use.sh"))).toHaveLength(1);
|
|
138
|
+
expect(commands).toContain('"${CLAUDE_PROJECT_DIR}/.agents/hooks/imf-web-ui/post-tool-use.sh"');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("does not touch settings.json when every registration is already present", () => {
|
|
142
|
+
runInstall(consumer, "--hooks");
|
|
143
|
+
const settingsFile = join(consumer, ".claude", "settings.json");
|
|
144
|
+
const settings = readSettings(consumer);
|
|
145
|
+
settings.hooks.PreToolUse = [{ hooks: [{ command: "./other/gate.mjs" }] }];
|
|
146
|
+
const before = JSON.stringify(settings, null, 4) + "\n";
|
|
147
|
+
writeFileSync(settingsFile, before);
|
|
148
|
+
|
|
149
|
+
const result = runInstall(consumer, "--hooks");
|
|
150
|
+
|
|
151
|
+
expect(readFileSync(settingsFile, "utf-8")).toBe(before);
|
|
152
|
+
expect(result.stdout).toContain("Hook registrations already present");
|
|
107
153
|
});
|
|
108
154
|
|
|
109
155
|
it("upserts the fenced AGENTS.md block and leaves the rest untouched", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imfusion/web-ui",
|
|
3
|
-
"version": "0.5.1-dev.
|
|
3
|
+
"version": "0.5.1-dev.30.gf9b152db",
|
|
4
4
|
"description": "The official Web UI component library for ImFusion web apps",
|
|
5
5
|
"author": "ImFusion GmbH",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"codegen:llms": "tsx src/llms/gen-llms.ts",
|
|
62
62
|
"verify:format": "prettier --check .",
|
|
63
63
|
"verify:lint": "eslint . --cache --max-warnings=0",
|
|
64
|
-
"verify:typecheck": "npm run codegen && tsc -p tsconfig.lib.json --noEmit && tsc -p .storybook/tsconfig.json --noEmit",
|
|
64
|
+
"verify:typecheck": "npm run codegen && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.lib.json --noEmit && tsc -p .storybook/tsconfig.json --noEmit",
|
|
65
65
|
"verify:tests": "vitest run",
|
|
66
66
|
"verify:deps": "tsx scripts/check-pinned-dependencies.ts",
|
|
67
67
|
"verify:changelog": "tsx scripts/verify/changelog.ts",
|