@godxjp/ui 32.12.1 → 32.14.0
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/DESIGN.md +1 -1
- package/README.md +2 -2
- package/agent/DESIGN.md +1 -1
- package/agent/START-HERE.md +4 -4
- package/agent/components/ListRow.json +1 -1
- package/agent/components/PageContainer.json +2 -2
- package/agent/components/Segmented.json +1 -0
- package/agent/components/Tabs.json +3 -2
- package/agent/components.json +7 -5
- package/agent/index.json +4 -4
- package/agent/llms.txt +5 -5
- package/agent/patterns/inertia-list-page.json +1 -1
- package/agent/patterns.json +1 -1
- package/agent/tokens.json +38 -2
- package/dist/components/layout/topbar.js +1 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/styles/base.css +19 -19
- package/dist/styles/data-display-layout.css +16 -17
- package/dist/styles/layers.json +1 -1
- package/dist/styles/layout.css +1 -1
- package/dist/styles/shell-layout.css +1 -0
- package/dist/styles.css +1 -1
- package/dist/tokens/components/data-display.css +10 -0
- package/dist/tokens/semantic/layout.css +1 -1
- package/package.json +5 -2
- package/scripts/_agent-setup.mjs +118 -0
- package/scripts/agent-guard.mjs +176 -0
- package/scripts/cli.mjs +25 -2
- package/scripts/codemod.mjs +313 -42
- package/scripts/init-agent-kit.mjs +14 -0
- package/scripts/mcp-call.mjs +40 -0
- package/scripts/ui-audit.mjs +77 -0
- package/scripts/user-guard.mjs +78 -0
- package/scripts/visual-audit.mjs +75 -9
|
@@ -54,6 +54,14 @@
|
|
|
54
54
|
--prose-quote-border-width: initial;
|
|
55
55
|
--prose-quote-space-inline: var(--space-3);
|
|
56
56
|
--prose-code-font-size: calc(1em / var(--font-size-ratio));
|
|
57
|
+
--prose-code-block-font-size: 1em;
|
|
58
|
+
|
|
59
|
+
--prose-heading-h1-font-size: calc(
|
|
60
|
+
1em * var(--font-size-ratio) * var(--font-size-ratio) * var(--font-size-ratio)
|
|
61
|
+
);
|
|
62
|
+
--prose-heading-h2-font-size: calc(1em * var(--font-size-ratio) * var(--font-size-ratio));
|
|
63
|
+
--prose-heading-h3-font-size: calc(1em * var(--font-size-ratio));
|
|
64
|
+
--prose-heading-h4-font-size: 1em;
|
|
57
65
|
--prose-code-space-inline: var(--space-1);
|
|
58
66
|
|
|
59
67
|
--prose-cell-image-max-inline-size: 20rem;
|
|
@@ -63,6 +71,8 @@
|
|
|
63
71
|
--block-editor-gutter-inline-size: calc(2 * var(--control-height-xs) + var(--space-2));
|
|
64
72
|
|
|
65
73
|
--block-editor-compact-gutter-inline-size: var(--block-editor-frame-padding);
|
|
74
|
+
|
|
75
|
+
--block-editor-raw-block-head-font-size: calc(1em / var(--font-size-ratio));
|
|
66
76
|
--block-editor-min-height: var(--band-height-3xl);
|
|
67
77
|
|
|
68
78
|
--block-editor-frame-padding: var(--space-3);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "32.
|
|
4
|
-
"godxUiMcp": "32.
|
|
3
|
+
"version": "32.14.0",
|
|
4
|
+
"godxUiMcp": "32.14.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"scripts/visual-audit-rules.mjs",
|
|
39
39
|
"scripts/brand-accent.generated.mjs",
|
|
40
40
|
"scripts/audit-hook.mjs",
|
|
41
|
+
"scripts/agent-guard.mjs",
|
|
42
|
+
"scripts/user-guard.mjs",
|
|
43
|
+
"scripts/mcp-call.mjs",
|
|
41
44
|
"scripts/init-agent-kit.mjs",
|
|
42
45
|
"scripts/init-guinea-pig.mjs",
|
|
43
46
|
"scripts/guinea-pig-skill.md",
|
package/scripts/_agent-setup.mjs
CHANGED
|
@@ -234,6 +234,9 @@ Before any JSX for a screen:
|
|
|
234
234
|
3. \`get_exemplar\` (no argument lists the archetypes: list, detail, editor, reader, settings,
|
|
235
235
|
dashboard, approval-queue) — match its structure.
|
|
236
236
|
|
|
237
|
+
No \`mcp__godx-ui__*\` tools (the session started outside this repo)? The same catalog over stdio:
|
|
238
|
+
\`npx --no-install @godxjp/ui mcp-call search_components '{"query":"<area>"}'\` (no tool lists them).
|
|
239
|
+
|
|
237
240
|
After building:
|
|
238
241
|
4. \`npx @godxjp/ui audit <files>\` and \`npx @godxjp/ui visual-audit <url> <routes>\` (6 widths) — zero.
|
|
239
242
|
5. Inspect every screenshot at every width, region by region; no top-N cap. Report "probes passed;
|
|
@@ -566,6 +569,121 @@ export function ensureClaudeHooks(root) {
|
|
|
566
569
|
return added;
|
|
567
570
|
}
|
|
568
571
|
|
|
572
|
+
/**
|
|
573
|
+
* The nearest `.mcp.json` at or above `fromDir` that serves the catalog: `{ dir, key, entry }`, or
|
|
574
|
+
* null. Read-only; a malformed file reads as "no catalog here".
|
|
575
|
+
*/
|
|
576
|
+
export function findUiMcpConfig(fromDir) {
|
|
577
|
+
for (let dir = resolve(fromDir); ; dir = dirname(dir)) {
|
|
578
|
+
const read = readJsonFile(join(dir, ".mcp.json"));
|
|
579
|
+
if (read.state === "ok") {
|
|
580
|
+
const servers = read.json.mcpServers;
|
|
581
|
+
if (servers && typeof servers === "object" && !Array.isArray(servers)) {
|
|
582
|
+
// The `godx-ui` key under any launcher (a local `node …/mcp/dist/index.js` link included),
|
|
583
|
+
// else whichever key launches @godxjp/ui-mcp.
|
|
584
|
+
const key = servers[MCP_KEY]
|
|
585
|
+
? MCP_KEY
|
|
586
|
+
: Object.keys(servers).find((name) => runsUiMcp(servers[name]));
|
|
587
|
+
if (key) return { dir, key, entry: servers[key] };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (dirname(dir) === dir) return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Where `init-agent --user` puts the guard shim, under Claude Code's user config dir. */
|
|
595
|
+
export const USER_GUARD_FILE = "godxjp-ui-guard.mjs";
|
|
596
|
+
const USER_GUARD_SOURCE = join(SELF_ROOT, "scripts", "user-guard.mjs");
|
|
597
|
+
|
|
598
|
+
/** Claude Code's user config dir: `$CLAUDE_CONFIG_DIR`, else `~/.claude` (HOME-relative). */
|
|
599
|
+
export function userConfigDir(env = process.env) {
|
|
600
|
+
return env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* `init-agent --user` (gh#1324): copy the guard shim to `<config>/hooks/godxjp-ui-guard.mjs` and
|
|
605
|
+
* register it in `<config>/settings.json` for PreToolUse and PostToolUse on Write|Edit|MultiEdit.
|
|
606
|
+
* Merges: every other key and hook is kept, an existing godxjp-ui-guard entry is left as it is,
|
|
607
|
+
* and the file is only rewritten when something was added. A settings.json that cannot be read,
|
|
608
|
+
* parsed or recognised is refused with a sidecar, like the project files.
|
|
609
|
+
* Returns the lines to print.
|
|
610
|
+
*/
|
|
611
|
+
export function ensureUserGuard(configDir = userConfigDir()) {
|
|
612
|
+
const lines = [];
|
|
613
|
+
const shim = join(configDir, "hooks", USER_GUARD_FILE);
|
|
614
|
+
const source = readFileSync(USER_GUARD_SOURCE, "utf8");
|
|
615
|
+
let current = null;
|
|
616
|
+
try {
|
|
617
|
+
current = readFileSync(shim, "utf8");
|
|
618
|
+
} catch {
|
|
619
|
+
// Not installed yet.
|
|
620
|
+
}
|
|
621
|
+
if (current === source) lines.push(`${shim} — present`);
|
|
622
|
+
else {
|
|
623
|
+
mkdirp(dirname(shim));
|
|
624
|
+
writeFileAtomic(shim, source);
|
|
625
|
+
lines.push(`${shim} — ${current === null ? "created" : "updated"}`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const path = join(configDir, "settings.json");
|
|
629
|
+
const group = (event) => ({
|
|
630
|
+
matcher: "Write|Edit|MultiEdit",
|
|
631
|
+
hooks: [{ type: "command", command: `node "${shim}" ${event}` }],
|
|
632
|
+
});
|
|
633
|
+
const suggested = {
|
|
634
|
+
hooks: { PreToolUse: [group("pre")], PostToolUse: [group("post")] },
|
|
635
|
+
};
|
|
636
|
+
const read = readJsonFile(path);
|
|
637
|
+
if (read.state !== "ok" && read.state !== "missing") {
|
|
638
|
+
lines.push(
|
|
639
|
+
`${path} — ` +
|
|
640
|
+
refuseAndSuggest(path, JSON.stringify(suggested, null, 2) + "\n", READ_FAILURE[read.state]),
|
|
641
|
+
);
|
|
642
|
+
return lines;
|
|
643
|
+
}
|
|
644
|
+
const json = read.state === "ok" ? read.json : {};
|
|
645
|
+
if (
|
|
646
|
+
json.hooks !== undefined &&
|
|
647
|
+
(json.hooks === null || typeof json.hooks !== "object" || Array.isArray(json.hooks))
|
|
648
|
+
) {
|
|
649
|
+
lines.push(
|
|
650
|
+
`${path} — ` +
|
|
651
|
+
refuseAndSuggest(
|
|
652
|
+
path,
|
|
653
|
+
JSON.stringify(suggested, null, 2) + "\n",
|
|
654
|
+
"`hooks` is not an object",
|
|
655
|
+
),
|
|
656
|
+
);
|
|
657
|
+
return lines;
|
|
658
|
+
}
|
|
659
|
+
json.hooks = json.hooks ?? {};
|
|
660
|
+
const added = [];
|
|
661
|
+
for (const [event, arg] of [
|
|
662
|
+
["PreToolUse", "pre"],
|
|
663
|
+
["PostToolUse", "post"],
|
|
664
|
+
]) {
|
|
665
|
+
const groups = json.hooks[event] ?? [];
|
|
666
|
+
if (!Array.isArray(groups)) {
|
|
667
|
+
lines.push(`${path} — hooks.${event} is not an array, left untouched`);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const has = groups.some((g) =>
|
|
671
|
+
(g?.hooks ?? []).some((h) => String(h?.command ?? "").includes(USER_GUARD_FILE)),
|
|
672
|
+
);
|
|
673
|
+
if (has) continue;
|
|
674
|
+
json.hooks[event] = [...groups, group(arg)];
|
|
675
|
+
added.push(event);
|
|
676
|
+
}
|
|
677
|
+
if (added.length === 0) {
|
|
678
|
+
lines.push(`${path} — guard hooks already present`);
|
|
679
|
+
return lines;
|
|
680
|
+
}
|
|
681
|
+
mkdirp(dirname(path));
|
|
682
|
+
writeFileAtomic(path, JSON.stringify(json, null, read.state === "ok" ? read.indent : 2) + "\n");
|
|
683
|
+
lines.push(`${path} — added ${added.join(" + ")} (Write|Edit|MultiEdit)`);
|
|
684
|
+
return lines;
|
|
685
|
+
}
|
|
686
|
+
|
|
569
687
|
/** Write the workflow mandate the SessionStart hook reads (only if absent). */
|
|
570
688
|
export function writeWorkflowMd(root) {
|
|
571
689
|
const path = join(root, ".claude", "godxjp-ui-workflow.md");
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agent-guard — the logic behind the user-level shim (scripts/user-guard.mjs, installed by
|
|
4
|
+
* `npx godxjp-ui init-agent --user`; gh#1324). Usage, from the shim only:
|
|
5
|
+
*
|
|
6
|
+
* node agent-guard.mjs pre|post <repo root> < Claude Code hook payload on stdin
|
|
7
|
+
*
|
|
8
|
+
* The shim has already established that the edited .tsx/.jsx sits in <repo root> and that the
|
|
9
|
+
* repo's `.mcp.json` mounts @godxjp/ui-mcp.
|
|
10
|
+
*
|
|
11
|
+
* pre BLOCK (exit 2) unless the agent's own transcript shows it consulted the catalog: a tool
|
|
12
|
+
* call to `mcp__<key>__*` for the repo's catalog key, or a Bash call to the stdio fallback
|
|
13
|
+
* (`godxjp-ui mcp-call …`, or `godx-ui-mcp call …`). A subagent is judged by ITS transcript
|
|
14
|
+
* (`agent_transcript_path`), so a parent's lookup does not license a subagent's write.
|
|
15
|
+
* post When the session did not start in <repo root>, the repo's own PostToolUse audit hook
|
|
16
|
+
* never loaded: run this package's audit-hook.mjs in its place. Otherwise do nothing, so
|
|
17
|
+
* a session that did start there is not audited twice.
|
|
18
|
+
*
|
|
19
|
+
* No network. FAIL OPEN: an internal error — unreadable payload, missing or unparseable
|
|
20
|
+
* transcript, a crash — exits 0. Only the deliberate "not consulted" verdict blocks.
|
|
21
|
+
*/
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
24
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
|
|
29
|
+
/** Same test as runsUiMcp in _agent-setup.mjs — the catalog server under any key. */
|
|
30
|
+
const runsUiMcp = (entry) =>
|
|
31
|
+
[entry?.command, ...(Array.isArray(entry?.args) ? entry.args : [])].some(
|
|
32
|
+
(part) => typeof part === "string" && /(^|\/)@godxjp\/ui-mcp(@|$)/.test(part),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/** The `.mcp.json` keys that serve the catalog — the `<key>` in `mcp__<key>__<tool>`. */
|
|
36
|
+
export function catalogKeys(root) {
|
|
37
|
+
const servers = JSON.parse(readFileSync(join(root, ".mcp.json"), "utf8"))?.mcpServers ?? {};
|
|
38
|
+
const keys = Object.keys(servers).filter((k) => k === "godx-ui" || runsUiMcp(servers[k]));
|
|
39
|
+
return keys.length ? keys : ["godx-ui"];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A Bash command that queries the catalog over the stdio fallback. */
|
|
43
|
+
const CLI_CALL =
|
|
44
|
+
/\bmcp-call\s+[a-z_]+|\bui-mcp(?:@\S+)?\s+call\s+[a-z_]+|\bgodx-ui-mcp\s+call\s+[a-z_]+/;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* True when the transcript (JSONL) holds a tool_use that consulted the catalog. Only tool_use
|
|
48
|
+
* blocks count: the block message itself quotes the fallback command, and a transcript that
|
|
49
|
+
* merely CONTAINS that text (in a tool result, in the agent's prose) has not run it.
|
|
50
|
+
*/
|
|
51
|
+
export function consulted(transcript, keys) {
|
|
52
|
+
const prefixes = keys.map((k) => `mcp__${k}__`);
|
|
53
|
+
for (const line of transcript.split("\n")) {
|
|
54
|
+
if (!line.includes('"tool_use"')) continue;
|
|
55
|
+
let entry;
|
|
56
|
+
try {
|
|
57
|
+
entry = JSON.parse(line);
|
|
58
|
+
} catch {
|
|
59
|
+
continue; // a torn last line while the session is still writing
|
|
60
|
+
}
|
|
61
|
+
const content = entry?.message?.content;
|
|
62
|
+
if (!Array.isArray(content)) continue;
|
|
63
|
+
for (const block of content) {
|
|
64
|
+
if (block?.type !== "tool_use" || typeof block.name !== "string") continue;
|
|
65
|
+
if (prefixes.some((p) => block.name.startsWith(p))) return true;
|
|
66
|
+
if (block.name === "Bash" && CLI_CALL.test(String(block.input?.command ?? ""))) return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const realOrSelf = (p) => {
|
|
73
|
+
try {
|
|
74
|
+
return realpathSync(p);
|
|
75
|
+
} catch {
|
|
76
|
+
return resolve(p);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Did the session START in <root>? Claude Code loads project hooks from the start directory and
|
|
82
|
+
* hands hooks `CLAUDE_PROJECT_DIR`; without it, the transcript's project folder is the start
|
|
83
|
+
* directory with every non-alphanumeric character turned into "-" (the measured layout:
|
|
84
|
+
* ~/.claude/projects/<slug>/<session>.jsonl, a subagent's under <session>/subagents/). A write in
|
|
85
|
+
* <top>/.claude/worktrees/<x> of a session started at <top> counts as started there: <top>'s
|
|
86
|
+
* hooks are the ones that loaded.
|
|
87
|
+
*/
|
|
88
|
+
export function startedIn(root, payload, env = process.env) {
|
|
89
|
+
const roots = new Set([resolve(root), realOrSelf(root)]);
|
|
90
|
+
for (const r of [...roots]) {
|
|
91
|
+
const at = r.indexOf(`${sep}.claude${sep}worktrees${sep}`);
|
|
92
|
+
if (at > 0) roots.add(r.slice(0, at));
|
|
93
|
+
}
|
|
94
|
+
if (env.CLAUDE_PROJECT_DIR) {
|
|
95
|
+
const start = env.CLAUDE_PROJECT_DIR;
|
|
96
|
+
return roots.has(resolve(start)) || roots.has(realOrSelf(start));
|
|
97
|
+
}
|
|
98
|
+
let transcript = String(payload?.transcript_path ?? "");
|
|
99
|
+
if (!transcript) return false;
|
|
100
|
+
if (transcript.includes(`${sep}subagents${sep}`)) transcript = dirname(dirname(transcript));
|
|
101
|
+
const slug = (p) => p.replace(/[^A-Za-z0-9]/g, "-");
|
|
102
|
+
return [...roots].some((r) => basename(dirname(transcript)) === slug(r));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The @godxjp/ui repo itself builds primitives from raw elements; the guard is for consumers. */
|
|
106
|
+
function isLibrary(root) {
|
|
107
|
+
try {
|
|
108
|
+
const name = JSON.parse(readFileSync(join(root, "package.json"), "utf8"))?.name;
|
|
109
|
+
return name === "@godxjp/ui" || name === "@godxjp/ui-mcp";
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function blockMessage(file, root, keys, started) {
|
|
116
|
+
const at = /^[\w./@-]+$/.test(root) ? root : `'${root.replace(/'/g, "'\\''")}'`;
|
|
117
|
+
const key = keys[0];
|
|
118
|
+
return [
|
|
119
|
+
`godxjp-ui guard: ${file} was NOT written.`,
|
|
120
|
+
`${root} builds its UI with @godxjp/ui, and this agent has not consulted the catalog yet.`,
|
|
121
|
+
started
|
|
122
|
+
? null
|
|
123
|
+
: `- This session did not start in ${root}, so its .mcp.json (the ${key} MCP), CLAUDE.md and hooks are NOT loaded.`,
|
|
124
|
+
`- Consult the catalog first: call mcp__${key}__search_components, then get_component for each component you use.`,
|
|
125
|
+
` No mcp__${key}__* tools in this session? Query the same catalog over stdio:`,
|
|
126
|
+
` cd ${at} && npx --no-install @godxjp/ui mcp-call search_components '{"query":"<what you are building>"}'`,
|
|
127
|
+
` cd ${at} && npx --no-install @godxjp/ui mcp-call get_component '{"name":"<Component>"}'`,
|
|
128
|
+
` (\`npx --no-install @godxjp/ui mcp-call\` with no tool lists every tool.)`,
|
|
129
|
+
`- Read ${join(root, "CLAUDE.md")} (the @godxjp/ui section), then retry the write.`,
|
|
130
|
+
]
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function main() {
|
|
136
|
+
const [event, root] = process.argv.slice(2);
|
|
137
|
+
if (!root || (event !== "pre" && event !== "post") || isLibrary(root)) process.exit(0);
|
|
138
|
+
const raw = readFileSync(0, "utf8");
|
|
139
|
+
const payload = JSON.parse(raw);
|
|
140
|
+
const file = payload?.tool_input?.file_path ?? payload?.tool_input?.filePath;
|
|
141
|
+
if (typeof file !== "string" || /\.(test|stories)\.[jt]sx$/.test(file)) process.exit(0);
|
|
142
|
+
const started = startedIn(root, payload);
|
|
143
|
+
|
|
144
|
+
if (event === "post") {
|
|
145
|
+
if (started) process.exit(0); // the repo's own PostToolUse audit hook already ran
|
|
146
|
+
const hook = join(HERE, "audit-hook.mjs");
|
|
147
|
+
if (!existsSync(hook)) process.exit(0);
|
|
148
|
+
const r = spawnSync(process.execPath, [hook], {
|
|
149
|
+
input: raw,
|
|
150
|
+
cwd: root,
|
|
151
|
+
encoding: "utf8",
|
|
152
|
+
timeout: 30_000,
|
|
153
|
+
});
|
|
154
|
+
if (r.error || r.status === null) process.exit(0);
|
|
155
|
+
if (r.stdout) process.stdout.write(r.stdout);
|
|
156
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
157
|
+
process.exit(r.status === 2 ? 2 : 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const transcriptPath = payload.agent_transcript_path ?? payload.transcript_path;
|
|
161
|
+
if (!transcriptPath || !existsSync(transcriptPath)) process.exit(0); // nothing to judge by
|
|
162
|
+
const keys = catalogKeys(root);
|
|
163
|
+
if (consulted(readFileSync(transcriptPath, "utf8"), keys)) process.exit(0);
|
|
164
|
+
process.stderr.write(blockMessage(file, root, keys, started) + "\n");
|
|
165
|
+
process.exit(2); // exit 2 on PreToolUse → Claude Code blocks the call and shows stderr
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Run only as a script, so tests can import the helpers.
|
|
169
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
170
|
+
process.on("uncaughtException", () => process.exit(0)); // fail open, see the header
|
|
171
|
+
try {
|
|
172
|
+
main();
|
|
173
|
+
} catch {
|
|
174
|
+
process.exit(0); // fail open
|
|
175
|
+
}
|
|
176
|
+
}
|
package/scripts/cli.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* @godxjp/ui CLI — `npx @godxjp/ui <command>`.
|
|
4
|
-
* init-agent scaffold the agent forcing-kit (MCP + auto-audit hook + mandate)
|
|
4
|
+
* init-agent scaffold the agent forcing-kit (MCP + auto-audit hook + mandate); --user: guard
|
|
5
|
+
* mcp-call query the godx-ui catalog over plain stdio, for a session without the MCP
|
|
5
6
|
* sync-rules write/refresh package-owned agent rules (--dry-run previews)
|
|
6
7
|
* audit static UI audit (regex over source)
|
|
7
8
|
* visual-audit runtime audit (Playwright) against a running app, at six widths
|
|
@@ -17,6 +18,7 @@ const MAP = {
|
|
|
17
18
|
"init-agent": "init-agent-kit.mjs",
|
|
18
19
|
audit: "ui-audit.mjs",
|
|
19
20
|
"audit-hook": "audit-hook.mjs",
|
|
21
|
+
"mcp-call": "mcp-call.mjs",
|
|
20
22
|
"visual-audit": "visual-audit.mjs",
|
|
21
23
|
"prune-css": "prune-css.mjs",
|
|
22
24
|
codemod: "codemod.mjs",
|
|
@@ -32,7 +34,18 @@ const HELP = {
|
|
|
32
34
|
.claude/settings.json a PostToolUse hook that audits every Write/Edit of a .tsx file
|
|
33
35
|
.claude/godxjp-ui-workflow.md
|
|
34
36
|
CLAUDE.md the godxjp-ui mandate block
|
|
35
|
-
Idempotent. Refuses inside the @godxjp/ui repo itself. Restart the agent afterwards
|
|
37
|
+
Idempotent. Refuses inside the @godxjp/ui repo itself. Restart the agent afterwards.
|
|
38
|
+
|
|
39
|
+
--user install the USER-level guard instead (gh#1324), for sessions that start outside the
|
|
40
|
+
app: the project files above load only when Claude Code starts in the app, so a
|
|
41
|
+
session started elsewhere has no MCP, no CLAUDE.md and no audit hook. Writes
|
|
42
|
+
~/.claude/hooks/godxjp-ui-guard.mjs ($CLAUDE_CONFIG_DIR/hooks when set)
|
|
43
|
+
~/.claude/settings.json PreToolUse + PostToolUse on Write|Edit|MultiEdit
|
|
44
|
+
PreToolUse blocks a .tsx/.jsx write in a repo whose .mcp.json mounts @godxjp/ui-mcp
|
|
45
|
+
until the agent has consulted the catalog (an mcp__<key>__* call, or mcp-call).
|
|
46
|
+
PostToolUse runs that repo's audit hook when the session did not start there.
|
|
47
|
+
Merges into existing settings, idempotent, no network; the guard fails open on its
|
|
48
|
+
own errors. Remove the two entries and the file to uninstall.`,
|
|
36
49
|
"sync-rules": `godxjp-ui sync-rules
|
|
37
50
|
|
|
38
51
|
Refresh the package-owned agent files for the installed @godxjp/ui version: the godx-ui entry in
|
|
@@ -60,6 +73,14 @@ const HELP = {
|
|
|
60
73
|
--rules print the rule catalog as JSON and exit
|
|
61
74
|
--consumer inside the @godxjp/ui repo only: apply the consumer rules (always on in an app)
|
|
62
75
|
Pre-commit: npx godxjp-ui audit resources/js || exit 1`,
|
|
76
|
+
"mcp-call": `godxjp-ui mcp-call [tool] ['{"arg":"value"}']
|
|
77
|
+
|
|
78
|
+
Query the godx-ui catalog without an MCP client — for a session that did not load the app's
|
|
79
|
+
.mcp.json. Launches the app's own .mcp.json catalog entry (the pinned @godxjp/ui-mcp, its env)
|
|
80
|
+
with \`call <tool> <json>\` appended, so the text is what the MCP tool returns. No tool: list them.
|
|
81
|
+
npx --no-install @godxjp/ui mcp-call search_components '{"query":"settings page"}'
|
|
82
|
+
npx --no-install @godxjp/ui mcp-call get_component '{"name":"Segmented"}'
|
|
83
|
+
Network: as the MCP itself — npx may fetch the pinned server once on a cold cache.`,
|
|
63
84
|
"audit-hook": `godxjp-ui audit-hook
|
|
64
85
|
|
|
65
86
|
The Claude Code PostToolUse hook init-agent wires in: reads the hook payload on stdin and audits
|
|
@@ -114,6 +135,8 @@ const USAGE = `usage: godxjp-ui <command> [args]
|
|
|
114
135
|
|
|
115
136
|
commands:
|
|
116
137
|
init-agent install the agent forcing-kit (MCP + auto-audit hook + CLAUDE.md mandate)
|
|
138
|
+
--user: the user-level guard for sessions started outside the app
|
|
139
|
+
mcp-call query the godx-ui catalog over plain stdio (no MCP client needed)
|
|
117
140
|
sync-rules write/refresh package-owned agent rules (--dry-run previews)
|
|
118
141
|
audit static UI audit over source
|
|
119
142
|
audit-hook the per-edit audit hook (wired by init-agent)
|