@estiva-app/ui 0.28.0 → 0.30.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/README.md +5 -7
- package/dist/cn.d.ts.map +1 -1
- package/dist/gates/app-checks.d.ts.map +1 -1
- package/dist/gates/{chunk-XE6HC5MJ.js → chunk-JZE7TIQY.js} +9 -2
- package/dist/gates/chunk-JZE7TIQY.js.map +7 -0
- package/dist/gates/{chunk-I7CJLUPM.js → chunk-XAPFC37F.js} +73 -6
- package/dist/gates/chunk-XAPFC37F.js.map +7 -0
- package/dist/gates/chunk-YKZXKVR2.js +39 -0
- package/dist/gates/chunk-YKZXKVR2.js.map +7 -0
- package/dist/gates/cli.js +2 -1
- package/dist/gates/cli.js.map +1 -1
- package/dist/gates/create-app.d.ts.map +1 -1
- package/dist/gates/create-app.js +2 -1
- package/dist/gates/create-app.js.map +1 -1
- package/dist/gates/hook.d.ts +7 -1
- package/dist/gates/hook.d.ts.map +1 -1
- package/dist/gates/index.js +26 -7
- package/dist/gates/index.js.map +2 -2
- package/dist/gates/status.d.ts +6 -0
- package/dist/gates/status.d.ts.map +1 -1
- package/dist/gates/token-lint.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/registry/{chunk-6OH3TWGF.js → chunk-KMZIMB3G.js} +5 -2
- package/dist/registry/{chunk-6OH3TWGF.js.map → chunk-KMZIMB3G.js.map} +2 -2
- package/dist/registry/cli.js +187 -25
- package/dist/registry/cli.js.map +4 -4
- package/dist/registry/find.d.ts +5 -0
- package/dist/registry/find.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/siblings.d.ts +21 -0
- package/dist/registry/siblings.d.ts.map +1 -0
- package/dist/registry/skill.d.ts +20 -0
- package/dist/registry/skill.d.ts.map +1 -0
- package/package.json +4 -2
- package/registry.json +1 -1
- package/skill/estiva-ui.md +128 -0
- package/src/ScrollArea.mdx +8 -4
- package/src/ScrollArea.stories.tsx +6 -6
- package/src/cn.ts +1 -0
- package/src/gates/app-checks.ts +15 -2
- package/src/gates/create-app.test.ts +8 -1
- package/src/gates/create-app.ts +5 -0
- package/src/gates/gates.test.ts +46 -0
- package/src/gates/hook.ts +69 -5
- package/src/gates/status.ts +25 -0
- package/src/gates/token-lint.ts +5 -0
- package/src/registry/app.test.ts +58 -1
- package/src/registry/cli.ts +60 -8
- package/src/registry/find.ts +10 -1
- package/src/registry/siblings.test.ts +85 -0
- package/src/registry/siblings.ts +96 -0
- package/src/registry/skill.test.ts +96 -0
- package/src/registry/skill.ts +117 -0
- package/dist/gates/chunk-I7CJLUPM.js.map +0 -7
- package/dist/gates/chunk-XE6HC5MJ.js.map +0 -7
|
@@ -177,7 +177,10 @@ function findInRegistries(registries, query, { limit = 5 } = {}) {
|
|
|
177
177
|
if (matched > 0) findings.push({ entry, matched, score, where: [...where], props: [...byName] });
|
|
178
178
|
}
|
|
179
179
|
const fromPackage = (finding) => finding.entry.app === null ? 0 : 1;
|
|
180
|
-
|
|
180
|
+
const namedInPackage = (finding) => finding.entry.app === null && finding.where.includes("name") ? 0 : 1;
|
|
181
|
+
findings.sort(
|
|
182
|
+
(a, b) => b.matched - a.matched || namedInPackage(a) - namedInPackage(b) || b.score - a.score || fromPackage(a) - fromPackage(b) || a.entry.name.localeCompare(b.entry.name)
|
|
183
|
+
);
|
|
181
184
|
return findings.slice(0, limit);
|
|
182
185
|
}
|
|
183
186
|
function docsLink(registry, entry) {
|
|
@@ -243,4 +246,4 @@ export {
|
|
|
243
246
|
docsLink,
|
|
244
247
|
formatFindings
|
|
245
248
|
};
|
|
246
|
-
//# sourceMappingURL=chunk-
|
|
249
|
+
//# sourceMappingURL=chunk-KMZIMB3G.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/registry/contract.ts", "../../src/registry/find.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * The usage-page contract, checked against an app's catalogue (UIG-19).\n *\n * Every part an app classifies **reusable** (or **promote-candidate**) has a\n * page beside it \u2014 `<PartName>.mdx` in the component's own folder \u2014 that opens\n * with a line saying what the part is, then `When`, `When not`, `How` (with\n * code) and `What it owns`, in that order. And it is drawn somewhere: a story\n * of its own, or a **Seen in** line on its page naming the stories that draw\n * it, or saying why none can (Katerina, 21 and 23 September 2026). A one-off\n * needs only its class and a purpose line, which the catalogue already refuses\n * to build without.\n *\n * This is the one copy. Peek and Ship each used to carry their own, pasted into\n * their `scripts/gates-checks.mjs` and run only by `gates:status`, so a page\n * could lose its `When not` and still merge. `estiva-ui check` runs this in\n * every app's `gate` job (docs/GATES.md \u00A723: one copy, in the package).\n *\n * The package's own pages keep the fuller contract in `src/pages.test.ts`,\n * which also reads `What it owns` back against the lint rule.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Registry } from './schema'\n\n/** The four headings, in the order a page carries them. */\nexport const PAGE_SECTIONS = ['When', 'When not', 'How', 'What it owns'] as const\n\n/** The classes that owe a page and a picture. */\nexport const CONTRACT_KINDS = ['reusable', 'promote-candidate'] as const\n\n/** The line that stands in for a story of the part's own. */\nconst SEEN_IN = /^\\*\\*Seen in\\*\\*/m\n\n/**\n * Why a page breaks the contract, or null when it keeps it.\n *\n * Read on normalised text: the apps are checked out with CRLF on Windows.\n */\nexport function pageProblem(source: string): string | null {\n const text = source.replace(/\\r\\n/g, '\\n')\n const heads = [...text.matchAll(/^##+\\s+(.+?)\\s*$/gm)].map((m) => m[1])\n const at = PAGE_SECTIONS.map((s) => heads.indexOf(s))\n const missing = PAGE_SECTIONS.filter((_, i) => at[i] < 0)\n if (missing.length) return `no ${missing.map((s) => `\"${s}\"`).join(', ')} section`\n if (at.some((i, n) => n > 0 && i < at[n - 1])) return `sections out of order: they go ${PAGE_SECTIONS.join(', ')}`\n // The opening line: the first paragraph under the title that is not markup.\n const opening = text\n .split(/^# .*$/m)[1]\n ?.split(/^##\\s/m)[0]\n ?.split(/\\n\\s*\\n/)\n .map((s) => s.trim())\n .find((s) => s && !s.startsWith('<') && !s.startsWith('import '))\n if (!opening) return 'no opening line under the title saying what the part is'\n if (!/^##\\s+How\\s*\\n[\\s\\S]*?```tsx?\\n/m.test(text)) return 'no code under \"How\"'\n return null\n}\n\n/** Whether a page records where the part is drawn, in place of a story of its own. */\nexport function hasSeenIn(source: string): boolean {\n return SEEN_IN.test(source.replace(/\\r\\n/g, '\\n'))\n}\n\n/**\n * Every catalogue link that names a story or a docs page its Storybook does not\n * have (UIG-19: a dead link is a build failure, not a dead end found six weeks\n * later). `ids` is every entry id in the Storybook's own index \u2014 what\n * `storybook index` writes \u2014 so this is Storybook's answer, not a guess at how\n * it builds an id.\n */\nexport function linkProblems(registry: Registry, ids: ReadonlySet<string>): string[] {\n const problems: string[] = []\n for (const entry of registry.entries) {\n for (const key of ['storyId', 'docsId'] as const) {\n const id = entry[key]\n if (id && !ids.has(id)) problems.push(`${entry.name} (${entry.sourceFile}) links to ${key === 'storyId' ? 'a story' : 'a docs page'} its Storybook does not have: ${id}`)\n }\n }\n return problems\n}\n\n/**\n * Every way an app's catalogue breaks the contract, one line each, naming the\n * part, its file and what to do. Empty when it keeps it.\n *\n * `root` is the app's own folder: the one its catalogue's paths are relative to.\n */\nexport function contractProblems(registry: Registry, root: string): string[] {\n const problems: string[] = []\n for (const entry of registry.entries) {\n const cls = entry.app?.class\n if (!cls || !(CONTRACT_KINDS as readonly string[]).includes(cls)) continue\n // Where the page must be: beside the component, named after the part \u2014\n // the same place the catalogue looks (src/registry/app.ts).\n const page = join(entry.sourceFile.replace(/[^/\\\\]+$/, ''), `${entry.name}.mdx`).replace(/\\\\/g, '/')\n const where = `${entry.name} (${cls}, ${entry.sourceFile})`\n if (!existsSync(join(root, page))) {\n problems.push(`${where} has no usage page: write ${page}, with an opening line, then ${PAGE_SECTIONS.join(', ')}`)\n continue\n }\n const text = readFileSync(join(root, page), 'utf8')\n const problem = pageProblem(text)\n if (problem) problems.push(`${where}: ${page} has ${problem}`)\n if (entry.storyId === null && !hasSeenIn(text)) {\n problems.push(`${where} is drawn nowhere: give it a story, or write a \"**Seen in**\" line on ${page} naming the stories that draw it, or why none can`)\n }\n }\n return problems\n}\n", "/**\n * The catalogue's reader (UIG-12): `ui:find <words>`.\n *\n * It exists to prove the format. A schema nobody consumes is a schema nobody\n * has tested, and the questions it has to answer are regression tests, not the\n * scope: \"floating panel\" must find `Popover`, \"scrolling\" `ScrollArea`,\n * \"empty\" `EmptyState`.\n *\n * It is also the answer to \"what do we have for this?\", which is the question\n * the wall cannot answer: a lint rule refuses the wrong thing, and this is\n * where the right one is named (UIG-20 makes a session ask it first).\n */\nimport type { Registry, RegistryEntry } from './schema'\n\nexport interface Finding {\n entry: RegistryEntry\n /** How many of the query's words matched anywhere. Ranked on this first. */\n matched: number\n score: number\n /** Which fields matched, for the reader to say why. */\n where: string[]\n /**\n * The props the query named **by name**, so the answer shows those rather\n * than all of them. A prop whose note merely carries one of the words counts\n * towards the score and is not shown: on \"floating panel\", six of `Popover`'s\n * notes say \"panel\", and printing all six buries the answer.\n */\n props: string[]\n}\n\n/**\n * Words that are in nearly every sentence, and so tell a search nothing.\n *\n * Measured: `ui:find \"truncate a long link\"` printed `Link`'s `external` prop,\n * because its note says \"in **a** new tab\" and the one-letter word matched\n * itself exactly. The minimum length in `alike` only guards a *prefix* match;\n * an exact match short-circuits it, which is right for `tab` and useless for `a`.\n */\nconst STOP = new Set([\n 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by', 'can', 'do', 'for', 'from', 'how', 'i', 'if', 'in', 'is', 'it', 'its', 'me', 'my', 'no', 'not', 'of',\n 'on', 'or', 'our', 'so', 'that', 'the', 'their', 'them', 'then', 'there', 'they', 'this', 'to', 'up', 'was', 'we', 'what', 'when', 'which', 'with', 'you', 'your',\n])\n\n/**\n * The words of a piece of text, with names taken apart.\n *\n * `CommandPalette` is kept whole *and* split, so \"palette\" reaches it and\n * \"commandpalette\" still does. Prose is already words; this costs it nothing.\n */\nfunction words(text: string): string[] {\n const found: string[] = []\n for (const token of text.match(/[A-Za-z0-9]+/g) ?? []) {\n found.push(token.toLowerCase())\n const parts = token.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/)\n if (parts.length > 1) found.push(...parts.map((part) => part.toLowerCase()))\n }\n return found\n}\n\n/**\n * Whether a word of the query means a word of the text.\n *\n * Either being a prefix of the other covers \"scroll\" against \"scrolling\"; the\n * five-letter key covers \"scrolls\" against \"scrolling\", where neither is. Crude\n * on purpose \u2014 a stemmer is a dependency and a surprise, and the failure here\n * is an extra row in a list of five, not a wrong answer.\n *\n * **Three letters at least**, or a prefix match is no match at all: `person's`\n * puts the word \"s\" in the text, and every query on earth starts with some\n * letter. Measured \u2014 before this, \"scrolling\" found `Avatar`.\n */\nfunction alike(query: string, text: string): boolean {\n if (query === text) return true\n if (Math.min(query.length, text.length) < 3) return false\n if (text.startsWith(query) || query.startsWith(text)) return true\n return query.slice(0, 5) === text.slice(0, 5)\n}\n\nconst hit = (query: string, text: string) => words(text).some((word) => alike(query, word))\n\n/**\n * Search name, purpose, behaviours and variants.\n *\n * A row that answers more of the question comes first, whatever it scored:\n * \"floating panel\" has to reach `Popover`, whose purpose carries both words,\n * over `MenuPanel`, whose name carries one of them loudly.\n */\nexport function findInRegistry(registry: Registry, query: string, { limit = 5 }: { limit?: number } = {}): Finding[] {\n return findInRegistries([registry], query, { limit })\n}\n\n/**\n * Search several catalogues as one: the package's, the app's own, and any app\n * beside it (UIG-13).\n *\n * An app's pass-on is not a result of its own \u2014 it is the package's part, and\n * the package's entry answers for it, saying which apps hand it on. At the\n * same strength the package's part comes first: it is the one to reach for.\n */\nexport function findInRegistries(registries: Registry[], query: string, { limit = 5 }: { limit?: number } = {}): Finding[] {\n const all = words(query)\n // Drop the words that mean nothing. If the question was only those, keep them\n // rather than answer nothing at all.\n const carrying = all.filter((word) => word.length >= 3 && !STOP.has(word))\n const asked = carrying.length ? carrying : all\n if (asked.length === 0) return []\n\n const findings: Finding[] = []\n const entries = registries.flatMap((registry) => registry.entries).filter((entry) => entry.app?.class !== 're-export')\n for (const entry of entries) {\n let score = 0\n let matched = 0\n const where = new Set<string>()\n const byName = new Set<string>()\n const byNote = new Set<string>()\n for (const word of asked) {\n let best = 0\n if (entry.name.toLowerCase() === word) {\n best = 100\n where.add('name')\n } else if (hit(word, entry.name)) {\n best = 40\n where.add('name')\n }\n if (hit(word, entry.purpose)) {\n best = Math.max(best, 20)\n where.add('purpose')\n }\n for (const owned of entry.ownsBehaviours) {\n if (hit(word, owned.behaviour) || hit(word, owned.id)) {\n best = Math.max(best, 15)\n where.add('behaviour')\n }\n }\n for (const variant of entry.variants) {\n if (variant.values.some((value) => hit(word, value)) || hit(word, variant.prop)) {\n best = Math.max(best, 8)\n where.add('variant')\n byName.add(variant.prop)\n }\n }\n // What it can do. \"does Link already truncate?\" is the question this\n // answers, and answering it is what stops the thing being built twice.\n for (const prop of entry.props) {\n // A prop the question named by name is the answer; one whose note\n // happens to carry the word is a hint. Named ones go first, so a short\n // answer shows the right lines.\n if (hit(word, prop.name)) {\n best = Math.max(best, 14)\n where.add('prop')\n byName.add(prop.name)\n } else if (prop.note !== null && hit(word, prop.note)) {\n best = Math.max(best, 10)\n where.add('prop')\n byNote.add(prop.name)\n }\n }\n if (best > 0) matched += 1\n score += best\n }\n if (matched > 0) findings.push({ entry, matched, score, where: [...where], props: [...byName] })\n }\n\n const fromPackage = (finding: Finding) => (finding.entry.app === null ? 0 : 1)\n findings.sort((a, b) => b.matched - a.matched || b.score - a.score || fromPackage(a) - fromPackage(b) || a.entry.name.localeCompare(b.entry.name))\n return findings.slice(0, limit)\n}\n\n/** `http://localhost:6008/?path=/docs/overlays-popover--docs`, or `null` where the entry has no page. */\nexport function docsLink(registry: Registry, entry: RegistryEntry): string | null {\n if (!entry.docsId) return null\n return registry.storybook.devUrl + registry.storybook.docsPath.replace('{docsId}', entry.docsId)\n}\n\n/** What an app's part is, in the words the search prints. */\nconst CLASS_WORDS: Record<string, string> = {\n 'one-off': 'used in one place',\n reusable: 'used in several places',\n 'promote-candidate': 'used in several places; could move into the package',\n unused: 'used nowhere in the app',\n}\n\n/** What the command prints: the import line first, because that is what the reader came for. */\nexport function formatFindings(searched: Registry | Registry[], findings: Finding[], query: string): string {\n const registries = Array.isArray(searched) ? searched : [searched]\n if (findings.length === 0) {\n return [\n `Nothing in ${registries.map((registry) => registry.builtFrom.repo).join(', ')} matches \"${query}\".`,\n 'If nothing here does what you need, say so and ask \u2014 do not invent a component.',\n ].join('\\n')\n }\n const registryOf = (entry: RegistryEntry) => registries.find((registry) => registry.builtFrom.repo === entry.repo) ?? registries[0]\n // Which apps hand a package part on, and from where: in Peek, `Button` is\n // imported from `@/components/ui/Button`, and the answer should say so.\n const handedOn = new Map<string, string[]>()\n for (const registry of registries) {\n for (const entry of registry.entries) {\n if (entry.app?.class !== 're-export' || entry.app.handsOn === null) continue\n handedOn.set(entry.app.handsOn, [...(handedOn.get(entry.app.handsOn) ?? []), `${entry.repo} hands it on${entry.name === entry.app.handsOn ? '' : ` as ${entry.name}`} from ${entry.importPath}`])\n }\n }\n\n const blocks = findings.map((finding) => {\n const { entry } = finding\n const facts = entry.app\n const lines = [\n `${entry.name} \u00B7 ${facts ? CLASS_WORDS[facts.class] ?? facts.class : entry.kind} \u00B7 ${entry.repo} (matched ${finding.where.join(', ')})`,\n ` ${entry.purpose}`,\n facts?.defaultExport ? ` import ${entry.name} from '${entry.importPath}'` : ` import { ${entry.name} } from '${entry.importPath}'`,\n ]\n if (!facts) for (const said of handedOn.get(entry.name) ?? []) lines.push(` ${said}`)\n if (facts?.packageNamesake) lines.push(` the package has a ${facts.packageNamesake} too: check it first`)\n if (facts && facts.usedIn.length) lines.push(` used in ${facts.usedIn.slice(0, 2).join(', ')}${facts.usedIn.length > 2 ? ` and ${facts.usedIn.length - 2} more` : ''}`)\n if (entry.ownsBehaviours.length) lines.push(` owns: ${entry.ownsBehaviours.map((owned) => owned.behaviour).join(' \u00B7 ')}`)\n if (entry.variants.length) lines.push(` ${entry.variants.map((variant) => `${variant.prop}: ${variant.values.join(' | ')}`).join(' ')}`)\n // The props the question named, with their own line \u2014 not all of them. A\n // component with twenty props would bury its own answer.\n const shown = finding.props\n .filter((name) => !entry.variants.some((variant) => variant.prop === name))\n .slice(0, 3)\n .map((name) => entry.props.find((prop) => prop.name === name))\n for (const prop of shown) {\n if (prop) lines.push(` ${prop.name}: ${prop.takes}${prop.note ? ` \u2014 ${prop.note}` : ''}`)\n }\n if (entry.props.length) lines.push(` ${entry.props.length} props in all; see ${entry.docsId ? 'the page' : entry.sourceFile} for the rest`)\n const link = docsLink(registryOf(entry), entry)\n if (link) lines.push(` ${link}`)\n return lines.join('\\n')\n })\n\n return blocks.join('\\n\\n')\n}\n"],
|
|
5
|
-
"mappings": ";;;AAoBA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AAId,IAAM,gBAAgB,CAAC,QAAQ,YAAY,OAAO,cAAc;AAGhE,IAAM,iBAAiB,CAAC,YAAY,mBAAmB;AAG9D,IAAM,UAAU;AAOT,SAAS,YAAY,QAA+B;AACzD,QAAM,OAAO,OAAO,QAAQ,SAAS,IAAI;AACzC,QAAM,QAAQ,CAAC,GAAG,KAAK,SAAS,oBAAoB,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AACtE,QAAM,KAAK,cAAc,IAAI,CAAC,MAAM,MAAM,QAAQ,CAAC,CAAC;AACpD,QAAM,UAAU,cAAc,OAAO,CAAC,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC;AACxD,MAAI,QAAQ,OAAQ,QAAO,MAAM,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC;AACxE,MAAI,GAAG,KAAK,CAAC,GAAG,MAAM,IAAI,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC,EAAG,QAAO,kCAAkC,cAAc,KAAK,IAAI,CAAC;AAEhH,QAAM,UAAU,KACb,MAAM,SAAS,EAAE,CAAC,GACjB,MAAM,QAAQ,EAAE,CAAC,GACjB,MAAM,SAAS,EAChB,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC,EAAE,WAAW,SAAS,CAAC;AAClE,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,CAAC,mCAAmC,KAAK,IAAI,EAAG,QAAO;AAC3D,SAAO;AACT;AAGO,SAAS,UAAU,QAAyB;AACjD,SAAO,QAAQ,KAAK,OAAO,QAAQ,SAAS,IAAI,CAAC;AACnD;AASO,SAAS,aAAa,UAAoB,KAAoC;AACnF,QAAM,WAAqB,CAAC;AAC5B,aAAW,SAAS,SAAS,SAAS;AACpC,eAAW,OAAO,CAAC,WAAW,QAAQ,GAAY;AAChD,YAAM,KAAK,MAAM,GAAG;AACpB,UAAI,MAAM,CAAC,IAAI,IAAI,EAAE,EAAG,UAAS,KAAK,GAAG,MAAM,IAAI,KAAK,MAAM,UAAU,cAAc,QAAQ,YAAY,YAAY,aAAa,iCAAiC,EAAE,EAAE;AAAA,IAC1K;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,iBAAiB,UAAoB,MAAwB;AAC3E,QAAM,WAAqB,CAAC;AAC5B,aAAW,SAAS,SAAS,SAAS;AACpC,UAAM,MAAM,MAAM,KAAK;AACvB,QAAI,CAAC,OAAO,CAAE,eAAqC,SAAS,GAAG,EAAG;AAGlE,UAAM,OAAO,KAAK,MAAM,WAAW,QAAQ,YAAY,EAAE,GAAG,GAAG,MAAM,IAAI,MAAM,EAAE,QAAQ,OAAO,GAAG;AACnG,UAAM,QAAQ,GAAG,MAAM,IAAI,KAAK,GAAG,KAAK,MAAM,UAAU;AACxD,QAAI,CAAC,WAAW,KAAK,MAAM,IAAI,CAAC,GAAG;AACjC,eAAS,KAAK,GAAG,KAAK,6BAA6B,IAAI,gCAAgC,cAAc,KAAK,IAAI,CAAC,EAAE;AACjH;AAAA,IACF;AACA,UAAM,OAAO,aAAa,KAAK,MAAM,IAAI,GAAG,MAAM;AAClD,UAAM,UAAU,YAAY,IAAI;AAChC,QAAI,QAAS,UAAS,KAAK,GAAG,KAAK,KAAK,IAAI,QAAQ,OAAO,EAAE;AAC7D,QAAI,MAAM,YAAY,QAAQ,CAAC,UAAU,IAAI,GAAG;AAC9C,eAAS,KAAK,GAAG,KAAK,wEAAwE,IAAI,mDAAmD;AAAA,IACvJ;AAAA,EACF;AACA,SAAO;AACT;;;ACrEA,IAAM,OAAO,oBAAI,IAAI;AAAA,EACnB;AAAA,EAAK;AAAA,EAAM;AAAA,EAAO;AAAA,EAAO;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAO;AAAA,EAAM;AAAA,EAAO;AAAA,EAAM;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAK;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAO;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAO;AAAA,EACxJ;AAAA,EAAM;AAAA,EAAM;AAAA,EAAO;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAM;AAAA,EAAM;AAAA,EAAO;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAO;AAC7J,CAAC;AAQD,SAAS,MAAM,MAAwB;AACrC,QAAM,QAAkB,CAAC;AACzB,aAAW,SAAS,KAAK,MAAM,eAAe,KAAK,CAAC,GAAG;AACrD,UAAM,KAAK,MAAM,YAAY,CAAC;AAC9B,UAAM,QAAQ,MAAM,MAAM,iDAAiD;AAC3E,QAAI,MAAM,SAAS,EAAG,OAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,KAAK,YAAY,CAAC,CAAC;AAAA,EAC7E;AACA,SAAO;AACT;AAcA,SAAS,MAAM,OAAe,MAAuB;AACnD,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,KAAK,IAAI,MAAM,QAAQ,KAAK,MAAM,IAAI,EAAG,QAAO;AACpD,MAAI,KAAK,WAAW,KAAK,KAAK,MAAM,WAAW,IAAI,EAAG,QAAO;AAC7D,SAAO,MAAM,MAAM,GAAG,CAAC,MAAM,KAAK,MAAM,GAAG,CAAC;AAC9C;AAEA,IAAM,MAAM,CAAC,OAAe,SAAiB,MAAM,IAAI,EAAE,KAAK,CAAC,SAAS,MAAM,OAAO,IAAI,CAAC;AASnF,SAAS,eAAe,UAAoB,OAAe,EAAE,QAAQ,EAAE,IAAwB,CAAC,GAAc;AACnH,SAAO,iBAAiB,CAAC,QAAQ,GAAG,OAAO,EAAE,MAAM,CAAC;AACtD;
|
|
4
|
+
"sourcesContent": ["/**\n * The usage-page contract, checked against an app's catalogue (UIG-19).\n *\n * Every part an app classifies **reusable** (or **promote-candidate**) has a\n * page beside it \u2014 `<PartName>.mdx` in the component's own folder \u2014 that opens\n * with a line saying what the part is, then `When`, `When not`, `How` (with\n * code) and `What it owns`, in that order. And it is drawn somewhere: a story\n * of its own, or a **Seen in** line on its page naming the stories that draw\n * it, or saying why none can (Katerina, 21 and 23 September 2026). A one-off\n * needs only its class and a purpose line, which the catalogue already refuses\n * to build without.\n *\n * This is the one copy. Peek and Ship each used to carry their own, pasted into\n * their `scripts/gates-checks.mjs` and run only by `gates:status`, so a page\n * could lose its `When not` and still merge. `estiva-ui check` runs this in\n * every app's `gate` job (docs/GATES.md \u00A723: one copy, in the package).\n *\n * The package's own pages keep the fuller contract in `src/pages.test.ts`,\n * which also reads `What it owns` back against the lint rule.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Registry } from './schema'\n\n/** The four headings, in the order a page carries them. */\nexport const PAGE_SECTIONS = ['When', 'When not', 'How', 'What it owns'] as const\n\n/** The classes that owe a page and a picture. */\nexport const CONTRACT_KINDS = ['reusable', 'promote-candidate'] as const\n\n/** The line that stands in for a story of the part's own. */\nconst SEEN_IN = /^\\*\\*Seen in\\*\\*/m\n\n/**\n * Why a page breaks the contract, or null when it keeps it.\n *\n * Read on normalised text: the apps are checked out with CRLF on Windows.\n */\nexport function pageProblem(source: string): string | null {\n const text = source.replace(/\\r\\n/g, '\\n')\n const heads = [...text.matchAll(/^##+\\s+(.+?)\\s*$/gm)].map((m) => m[1])\n const at = PAGE_SECTIONS.map((s) => heads.indexOf(s))\n const missing = PAGE_SECTIONS.filter((_, i) => at[i] < 0)\n if (missing.length) return `no ${missing.map((s) => `\"${s}\"`).join(', ')} section`\n if (at.some((i, n) => n > 0 && i < at[n - 1])) return `sections out of order: they go ${PAGE_SECTIONS.join(', ')}`\n // The opening line: the first paragraph under the title that is not markup.\n const opening = text\n .split(/^# .*$/m)[1]\n ?.split(/^##\\s/m)[0]\n ?.split(/\\n\\s*\\n/)\n .map((s) => s.trim())\n .find((s) => s && !s.startsWith('<') && !s.startsWith('import '))\n if (!opening) return 'no opening line under the title saying what the part is'\n if (!/^##\\s+How\\s*\\n[\\s\\S]*?```tsx?\\n/m.test(text)) return 'no code under \"How\"'\n return null\n}\n\n/** Whether a page records where the part is drawn, in place of a story of its own. */\nexport function hasSeenIn(source: string): boolean {\n return SEEN_IN.test(source.replace(/\\r\\n/g, '\\n'))\n}\n\n/**\n * Every catalogue link that names a story or a docs page its Storybook does not\n * have (UIG-19: a dead link is a build failure, not a dead end found six weeks\n * later). `ids` is every entry id in the Storybook's own index \u2014 what\n * `storybook index` writes \u2014 so this is Storybook's answer, not a guess at how\n * it builds an id.\n */\nexport function linkProblems(registry: Registry, ids: ReadonlySet<string>): string[] {\n const problems: string[] = []\n for (const entry of registry.entries) {\n for (const key of ['storyId', 'docsId'] as const) {\n const id = entry[key]\n if (id && !ids.has(id)) problems.push(`${entry.name} (${entry.sourceFile}) links to ${key === 'storyId' ? 'a story' : 'a docs page'} its Storybook does not have: ${id}`)\n }\n }\n return problems\n}\n\n/**\n * Every way an app's catalogue breaks the contract, one line each, naming the\n * part, its file and what to do. Empty when it keeps it.\n *\n * `root` is the app's own folder: the one its catalogue's paths are relative to.\n */\nexport function contractProblems(registry: Registry, root: string): string[] {\n const problems: string[] = []\n for (const entry of registry.entries) {\n const cls = entry.app?.class\n if (!cls || !(CONTRACT_KINDS as readonly string[]).includes(cls)) continue\n // Where the page must be: beside the component, named after the part \u2014\n // the same place the catalogue looks (src/registry/app.ts).\n const page = join(entry.sourceFile.replace(/[^/\\\\]+$/, ''), `${entry.name}.mdx`).replace(/\\\\/g, '/')\n const where = `${entry.name} (${cls}, ${entry.sourceFile})`\n if (!existsSync(join(root, page))) {\n problems.push(`${where} has no usage page: write ${page}, with an opening line, then ${PAGE_SECTIONS.join(', ')}`)\n continue\n }\n const text = readFileSync(join(root, page), 'utf8')\n const problem = pageProblem(text)\n if (problem) problems.push(`${where}: ${page} has ${problem}`)\n if (entry.storyId === null && !hasSeenIn(text)) {\n problems.push(`${where} is drawn nowhere: give it a story, or write a \"**Seen in**\" line on ${page} naming the stories that draw it, or why none can`)\n }\n }\n return problems\n}\n", "/**\n * The catalogue's reader (UIG-12): `ui:find <words>`.\n *\n * It exists to prove the format. A schema nobody consumes is a schema nobody\n * has tested, and the questions it has to answer are regression tests, not the\n * scope: \"floating panel\" must find `Popover`, \"scrolling\" `ScrollArea`,\n * \"empty\" `EmptyState`.\n *\n * It is also the answer to \"what do we have for this?\", which is the question\n * the wall cannot answer: a lint rule refuses the wrong thing, and this is\n * where the right one is named (UIG-20 makes a session ask it first).\n */\nimport type { Registry, RegistryEntry } from './schema'\n\nexport interface Finding {\n entry: RegistryEntry\n /** How many of the query's words matched anywhere. Ranked on this first. */\n matched: number\n score: number\n /** Which fields matched, for the reader to say why. */\n where: string[]\n /**\n * The props the query named **by name**, so the answer shows those rather\n * than all of them. A prop whose note merely carries one of the words counts\n * towards the score and is not shown: on \"floating panel\", six of `Popover`'s\n * notes say \"panel\", and printing all six buries the answer.\n */\n props: string[]\n}\n\n/**\n * Words that are in nearly every sentence, and so tell a search nothing.\n *\n * Measured: `ui:find \"truncate a long link\"` printed `Link`'s `external` prop,\n * because its note says \"in **a** new tab\" and the one-letter word matched\n * itself exactly. The minimum length in `alike` only guards a *prefix* match;\n * an exact match short-circuits it, which is right for `tab` and useless for `a`.\n */\nconst STOP = new Set([\n 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by', 'can', 'do', 'for', 'from', 'how', 'i', 'if', 'in', 'is', 'it', 'its', 'me', 'my', 'no', 'not', 'of',\n 'on', 'or', 'our', 'so', 'that', 'the', 'their', 'them', 'then', 'there', 'they', 'this', 'to', 'up', 'was', 'we', 'what', 'when', 'which', 'with', 'you', 'your',\n])\n\n/**\n * The words of a piece of text, with names taken apart.\n *\n * `CommandPalette` is kept whole *and* split, so \"palette\" reaches it and\n * \"commandpalette\" still does. Prose is already words; this costs it nothing.\n */\nfunction words(text: string): string[] {\n const found: string[] = []\n for (const token of text.match(/[A-Za-z0-9]+/g) ?? []) {\n found.push(token.toLowerCase())\n const parts = token.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/)\n if (parts.length > 1) found.push(...parts.map((part) => part.toLowerCase()))\n }\n return found\n}\n\n/**\n * Whether a word of the query means a word of the text.\n *\n * Either being a prefix of the other covers \"scroll\" against \"scrolling\"; the\n * five-letter key covers \"scrolls\" against \"scrolling\", where neither is. Crude\n * on purpose \u2014 a stemmer is a dependency and a surprise, and the failure here\n * is an extra row in a list of five, not a wrong answer.\n *\n * **Three letters at least**, or a prefix match is no match at all: `person's`\n * puts the word \"s\" in the text, and every query on earth starts with some\n * letter. Measured \u2014 before this, \"scrolling\" found `Avatar`.\n */\nfunction alike(query: string, text: string): boolean {\n if (query === text) return true\n if (Math.min(query.length, text.length) < 3) return false\n if (text.startsWith(query) || query.startsWith(text)) return true\n return query.slice(0, 5) === text.slice(0, 5)\n}\n\nconst hit = (query: string, text: string) => words(text).some((word) => alike(query, word))\n\n/**\n * Search name, purpose, behaviours and variants.\n *\n * A row that answers more of the question comes first, whatever it scored:\n * \"floating panel\" has to reach `Popover`, whose purpose carries both words,\n * over `MenuPanel`, whose name carries one of them loudly.\n */\nexport function findInRegistry(registry: Registry, query: string, { limit = 5 }: { limit?: number } = {}): Finding[] {\n return findInRegistries([registry], query, { limit })\n}\n\n/**\n * Search several catalogues as one: the package's, the app's own, and any app\n * beside it (UIG-13).\n *\n * An app's pass-on is not a result of its own \u2014 it is the package's part, and\n * the package's entry answers for it, saying which apps hand it on. At the\n * same strength the package's part comes first: it is the one to reach for.\n * And a package part whose name carries one of the words comes before an app's\n * part answering as many words, whatever each scored: \"panel header\" put an\n * app's own `Header`, whose whole name is one of the words, above the package's\n * `ContainerHeader` (UIG-20). Only by name: a package part the words reach\n * through its notes alone would bury the app part that is the answer.\n */\nexport function findInRegistries(registries: Registry[], query: string, { limit = 5 }: { limit?: number } = {}): Finding[] {\n const all = words(query)\n // Drop the words that mean nothing. If the question was only those, keep them\n // rather than answer nothing at all.\n const carrying = all.filter((word) => word.length >= 3 && !STOP.has(word))\n const asked = carrying.length ? carrying : all\n if (asked.length === 0) return []\n\n const findings: Finding[] = []\n const entries = registries.flatMap((registry) => registry.entries).filter((entry) => entry.app?.class !== 're-export')\n for (const entry of entries) {\n let score = 0\n let matched = 0\n const where = new Set<string>()\n const byName = new Set<string>()\n const byNote = new Set<string>()\n for (const word of asked) {\n let best = 0\n if (entry.name.toLowerCase() === word) {\n best = 100\n where.add('name')\n } else if (hit(word, entry.name)) {\n best = 40\n where.add('name')\n }\n if (hit(word, entry.purpose)) {\n best = Math.max(best, 20)\n where.add('purpose')\n }\n for (const owned of entry.ownsBehaviours) {\n if (hit(word, owned.behaviour) || hit(word, owned.id)) {\n best = Math.max(best, 15)\n where.add('behaviour')\n }\n }\n for (const variant of entry.variants) {\n if (variant.values.some((value) => hit(word, value)) || hit(word, variant.prop)) {\n best = Math.max(best, 8)\n where.add('variant')\n byName.add(variant.prop)\n }\n }\n // What it can do. \"does Link already truncate?\" is the question this\n // answers, and answering it is what stops the thing being built twice.\n for (const prop of entry.props) {\n // A prop the question named by name is the answer; one whose note\n // happens to carry the word is a hint. Named ones go first, so a short\n // answer shows the right lines.\n if (hit(word, prop.name)) {\n best = Math.max(best, 14)\n where.add('prop')\n byName.add(prop.name)\n } else if (prop.note !== null && hit(word, prop.note)) {\n best = Math.max(best, 10)\n where.add('prop')\n byNote.add(prop.name)\n }\n }\n if (best > 0) matched += 1\n score += best\n }\n if (matched > 0) findings.push({ entry, matched, score, where: [...where], props: [...byName] })\n }\n\n const fromPackage = (finding: Finding) => (finding.entry.app === null ? 0 : 1)\n const namedInPackage = (finding: Finding) => (finding.entry.app === null && finding.where.includes('name') ? 0 : 1)\n findings.sort(\n (a, b) =>\n b.matched - a.matched || namedInPackage(a) - namedInPackage(b) || b.score - a.score || fromPackage(a) - fromPackage(b) || a.entry.name.localeCompare(b.entry.name),\n )\n return findings.slice(0, limit)\n}\n\n/** `http://localhost:6008/?path=/docs/overlays-popover--docs`, or `null` where the entry has no page. */\nexport function docsLink(registry: Registry, entry: RegistryEntry): string | null {\n if (!entry.docsId) return null\n return registry.storybook.devUrl + registry.storybook.docsPath.replace('{docsId}', entry.docsId)\n}\n\n/** What an app's part is, in the words the search prints. */\nconst CLASS_WORDS: Record<string, string> = {\n 'one-off': 'used in one place',\n reusable: 'used in several places',\n 'promote-candidate': 'used in several places; could move into the package',\n unused: 'used nowhere in the app',\n}\n\n/** What the command prints: the import line first, because that is what the reader came for. */\nexport function formatFindings(searched: Registry | Registry[], findings: Finding[], query: string): string {\n const registries = Array.isArray(searched) ? searched : [searched]\n if (findings.length === 0) {\n return [\n `Nothing in ${registries.map((registry) => registry.builtFrom.repo).join(', ')} matches \"${query}\".`,\n 'If nothing here does what you need, say so and ask \u2014 do not invent a component.',\n ].join('\\n')\n }\n const registryOf = (entry: RegistryEntry) => registries.find((registry) => registry.builtFrom.repo === entry.repo) ?? registries[0]\n // Which apps hand a package part on, and from where: in Peek, `Button` is\n // imported from `@/components/ui/Button`, and the answer should say so.\n const handedOn = new Map<string, string[]>()\n for (const registry of registries) {\n for (const entry of registry.entries) {\n if (entry.app?.class !== 're-export' || entry.app.handsOn === null) continue\n handedOn.set(entry.app.handsOn, [...(handedOn.get(entry.app.handsOn) ?? []), `${entry.repo} hands it on${entry.name === entry.app.handsOn ? '' : ` as ${entry.name}`} from ${entry.importPath}`])\n }\n }\n\n const blocks = findings.map((finding) => {\n const { entry } = finding\n const facts = entry.app\n const lines = [\n `${entry.name} \u00B7 ${facts ? CLASS_WORDS[facts.class] ?? facts.class : entry.kind} \u00B7 ${entry.repo} (matched ${finding.where.join(', ')})`,\n ` ${entry.purpose}`,\n facts?.defaultExport ? ` import ${entry.name} from '${entry.importPath}'` : ` import { ${entry.name} } from '${entry.importPath}'`,\n ]\n if (!facts) for (const said of handedOn.get(entry.name) ?? []) lines.push(` ${said}`)\n if (facts?.packageNamesake) lines.push(` the package has a ${facts.packageNamesake} too: check it first`)\n if (facts && facts.usedIn.length) lines.push(` used in ${facts.usedIn.slice(0, 2).join(', ')}${facts.usedIn.length > 2 ? ` and ${facts.usedIn.length - 2} more` : ''}`)\n if (entry.ownsBehaviours.length) lines.push(` owns: ${entry.ownsBehaviours.map((owned) => owned.behaviour).join(' \u00B7 ')}`)\n if (entry.variants.length) lines.push(` ${entry.variants.map((variant) => `${variant.prop}: ${variant.values.join(' | ')}`).join(' ')}`)\n // The props the question named, with their own line \u2014 not all of them. A\n // component with twenty props would bury its own answer.\n const shown = finding.props\n .filter((name) => !entry.variants.some((variant) => variant.prop === name))\n .slice(0, 3)\n .map((name) => entry.props.find((prop) => prop.name === name))\n for (const prop of shown) {\n if (prop) lines.push(` ${prop.name}: ${prop.takes}${prop.note ? ` \u2014 ${prop.note}` : ''}`)\n }\n if (entry.props.length) lines.push(` ${entry.props.length} props in all; see ${entry.docsId ? 'the page' : entry.sourceFile} for the rest`)\n const link = docsLink(registryOf(entry), entry)\n if (link) lines.push(` ${link}`)\n return lines.join('\\n')\n })\n\n return blocks.join('\\n\\n')\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/registry/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
findInRegistries,
|
|
6
6
|
formatFindings,
|
|
7
7
|
linkProblems
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-KMZIMB3G.js";
|
|
9
9
|
import {
|
|
10
10
|
CLASSES,
|
|
11
11
|
validateRegistry
|
|
@@ -13,10 +13,134 @@ import {
|
|
|
13
13
|
|
|
14
14
|
// src/registry/cli.ts
|
|
15
15
|
import { spawnSync } from "node:child_process";
|
|
16
|
-
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
|
-
import { basename, join, resolve } from "node:path";
|
|
18
|
+
import { basename as basename2, dirname as dirname3, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
// src/registry/siblings.ts
|
|
22
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
23
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
24
|
+
var PACKAGE_NAME = "@estiva-app/ui";
|
|
25
|
+
function manifest(folder) {
|
|
26
|
+
const file = join(folder, "package.json");
|
|
27
|
+
if (!existsSync(file)) return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function appName(folder) {
|
|
35
|
+
const m = manifest(folder);
|
|
36
|
+
if (!m || m.name === PACKAGE_NAME) return null;
|
|
37
|
+
const uses = Boolean(m.dependencies?.[PACKAGE_NAME] ?? m.devDependencies?.[PACKAGE_NAME]);
|
|
38
|
+
return uses && existsSync(join(folder, "src")) ? m.name ?? basename(folder) : null;
|
|
39
|
+
}
|
|
40
|
+
var isDir = (path) => {
|
|
41
|
+
try {
|
|
42
|
+
return statSync(path).isDirectory();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function workspaceOf(from) {
|
|
48
|
+
let at = resolve(from);
|
|
49
|
+
for (; ; ) {
|
|
50
|
+
if (existsSync(join(at, ".git"))) return dirname(at);
|
|
51
|
+
const up = dirname(at);
|
|
52
|
+
if (up === at) return resolve(from);
|
|
53
|
+
at = up;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function findSiblings(workspace, { skip = [] } = {}) {
|
|
57
|
+
let tops;
|
|
58
|
+
try {
|
|
59
|
+
tops = readdirSync(workspace).filter((name) => !name.startsWith(".") && name !== "node_modules" && isDir(join(workspace, name)));
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const found = [];
|
|
64
|
+
for (const top of tops) {
|
|
65
|
+
const folder = join(workspace, top);
|
|
66
|
+
const inside = readdirSync(folder).filter((name) => !name.startsWith(".") && name !== "node_modules" && isDir(join(folder, name)));
|
|
67
|
+
for (const candidate of [folder, ...inside.map((name) => join(folder, name))]) {
|
|
68
|
+
const packageName = appName(candidate);
|
|
69
|
+
if (packageName) found.push({ name: top, folder: candidate, packageName });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
73
|
+
for (const sibling of found.sort((a, b) => a.name.length - b.name.length || a.name.localeCompare(b.name))) {
|
|
74
|
+
if (skip.includes(sibling.packageName) || byPackage.has(sibling.packageName)) continue;
|
|
75
|
+
byPackage.set(sibling.packageName, sibling);
|
|
76
|
+
}
|
|
77
|
+
return [...byPackage.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/registry/skill.ts
|
|
81
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
82
|
+
import { dirname as dirname2, join as join2, relative, resolve as resolve2, sep } from "node:path";
|
|
83
|
+
var SKILL_NAME = "estiva-ui";
|
|
84
|
+
var SKILL_DESCRIPTION = `Use before building, adding or changing anything on screen in an app that uses @estiva-app/ui, or in @estiva-app/ui itself: a screen, panel, list, row, header, dialog, menu, empty state, a new component, or how something looks. Also when someone asks for a component in plain words ("I need a\u2026", "add a\u2026", "is there a\u2026"). It searches every catalogue at once, the package's and each app's Storybook, for a part that already does the job, and lists the mistakes this codebase keeps making.`;
|
|
85
|
+
var SEARCH_RULES = ["Bash(npm run ui:find *)", "Bash(npm --prefix * run ui:find *)", "PowerShell(npm run ui:find *)", "PowerShell(npm --prefix * run ui:find *)"];
|
|
86
|
+
var skillTextPath = (packageRoot2) => join2(packageRoot2, "skill", `${SKILL_NAME}.md`);
|
|
87
|
+
var loaderPath = (folder) => join2(folder, ".claude", "skills", SKILL_NAME, "SKILL.md");
|
|
88
|
+
var settingsPath = (folder) => join2(folder, ".claude", "settings.json");
|
|
89
|
+
function loaderText(folder, packageRoot2) {
|
|
90
|
+
const fromTop = relative(folder, skillTextPath(packageRoot2)).split(sep).join("/");
|
|
91
|
+
return [
|
|
92
|
+
"---",
|
|
93
|
+
`name: ${SKILL_NAME}`,
|
|
94
|
+
`description: ${JSON.stringify(SKILL_DESCRIPTION)}`,
|
|
95
|
+
"---",
|
|
96
|
+
"",
|
|
97
|
+
"<!-- Written by `estiva-ui skill` from @estiva-app/ui; `estiva-ui check` fails when it differs. The text is the installed package\u2019s, read in below. -->",
|
|
98
|
+
"",
|
|
99
|
+
`!\`cat "\${CLAUDE_PROJECT_DIR}/${fromTop}"\``,
|
|
100
|
+
""
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
var readSettings = (folder) => {
|
|
104
|
+
const path = settingsPath(folder);
|
|
105
|
+
if (!existsSync2(path)) return {};
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
function settingsWithSearch(folder) {
|
|
113
|
+
const settings = readSettings(folder);
|
|
114
|
+
if (!settings) return null;
|
|
115
|
+
const allow = settings.permissions?.allow ?? [];
|
|
116
|
+
const missing = SEARCH_RULES.filter((rule) => !allow.includes(rule));
|
|
117
|
+
if (!missing.length && existsSync2(settingsPath(folder))) return null;
|
|
118
|
+
settings.permissions = { ...settings.permissions, allow: [...allow, ...missing] };
|
|
119
|
+
return `${JSON.stringify(settings, null, 2)}
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
function repositoryOf(from) {
|
|
123
|
+
let at = resolve2(from);
|
|
124
|
+
for (; ; ) {
|
|
125
|
+
if (existsSync2(join2(at, ".git"))) return at;
|
|
126
|
+
const up = dirname2(at);
|
|
127
|
+
if (up === at) return resolve2(from);
|
|
128
|
+
at = up;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function loaderProblem(folder, packageRoot2) {
|
|
132
|
+
const path = loaderPath(folder);
|
|
133
|
+
const shown = relative(process.cwd(), path).split(sep).join("/") || path;
|
|
134
|
+
if (!existsSync2(path)) return `${shown} is missing: run "estiva-ui skill" and commit it`;
|
|
135
|
+
if (readFileSync2(path, "utf8").replace(/\r\n/g, "\n") !== loaderText(folder, packageRoot2)) return `${shown} is not what the package writes: run "estiva-ui skill" and commit it`;
|
|
136
|
+
if (!existsSync2(skillTextPath(packageRoot2))) return `${shown} reads ${skillTextPath(packageRoot2)}, which is not there`;
|
|
137
|
+
const allow = readSettings(folder)?.permissions?.allow ?? [];
|
|
138
|
+
const missing = SEARCH_RULES.filter((rule) => !allow.includes(rule));
|
|
139
|
+
if (missing.length) return `.claude/settings.json does not allow the search (${missing.join(", ")}): run "estiva-ui skill" and commit it`;
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/registry/cli.ts
|
|
20
144
|
var packageBuilder = () => import("./build-LGFCCOLR.js");
|
|
21
145
|
var appBuilder = () => import("./app-FXN4EI34.js");
|
|
22
146
|
var gatedScan = () => import("./gated-5CPIUQAW.js");
|
|
@@ -39,24 +163,35 @@ var plain = () => {
|
|
|
39
163
|
}
|
|
40
164
|
return out;
|
|
41
165
|
};
|
|
42
|
-
var root =
|
|
43
|
-
var manifestAt = (folder) => JSON.parse(
|
|
44
|
-
var isPackage =
|
|
166
|
+
var root = resolve3(value("root") ?? ".");
|
|
167
|
+
var manifestAt = (folder) => JSON.parse(readFileSync3(resolve3(folder, "package.json"), "utf8"));
|
|
168
|
+
var isPackage = existsSync3(resolve3(root, "package.json")) && manifestAt(root).name === "@estiva-app/ui";
|
|
45
169
|
function packageRegistryPath() {
|
|
46
170
|
const named = value("file");
|
|
47
|
-
if (named) return
|
|
48
|
-
if (isPackage) return
|
|
171
|
+
if (named) return resolve3(named);
|
|
172
|
+
if (isPackage) return resolve3(root, "registry.json");
|
|
49
173
|
return fileURLToPath(new URL("../../registry.json", import.meta.url));
|
|
50
174
|
}
|
|
51
175
|
function readPackageRegistry() {
|
|
52
176
|
const path = packageRegistryPath();
|
|
53
|
-
if (!
|
|
54
|
-
const registry = JSON.parse(
|
|
177
|
+
if (!existsSync3(path)) throw new Error(`no registry.json at ${path} \u2014 run "estiva-ui build" in the package first`);
|
|
178
|
+
const registry = JSON.parse(readFileSync3(path, "utf8"));
|
|
55
179
|
const problems = validateRegistry(registry);
|
|
56
180
|
if (problems.length) throw new Error(`${path} does not match the schema:
|
|
57
181
|
${problems.join("\n ")}`);
|
|
58
182
|
return registry;
|
|
59
183
|
}
|
|
184
|
+
var packageRoot = () => isPackage ? root : resolve3(root, "node_modules", "@estiva-app", "ui");
|
|
185
|
+
function checkLoader() {
|
|
186
|
+
const problem = loaderProblem(repositoryOf(root), packageRoot());
|
|
187
|
+
if (problem) {
|
|
188
|
+
process.stderr.write(`claude skill: ${problem}
|
|
189
|
+
`);
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
process.stdout.write("claude skill: the loader reads the package\u2019s text\n");
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
60
195
|
async function buildApp(folder, repo) {
|
|
61
196
|
const { buildAppRegistry } = await appBuilder();
|
|
62
197
|
const registry = buildAppRegistry({ root: folder, repo });
|
|
@@ -66,20 +201,20 @@ async function buildApp(folder, repo) {
|
|
|
66
201
|
return registry;
|
|
67
202
|
}
|
|
68
203
|
function checkLinks(registry, folder) {
|
|
69
|
-
const bin =
|
|
70
|
-
if (!
|
|
204
|
+
const bin = resolve3(folder, "node_modules", ".bin", process.platform === "win32" ? "storybook.cmd" : "storybook");
|
|
205
|
+
if (!existsSync3(resolve3(folder, ".storybook")) || !existsSync3(bin)) {
|
|
71
206
|
process.stdout.write("story links: no Storybook here, nothing to check\n");
|
|
72
207
|
return true;
|
|
73
208
|
}
|
|
74
|
-
const out =
|
|
209
|
+
const out = join3(mkdtempSync(join3(tmpdir(), "estiva-ui-index-")), "index.json");
|
|
75
210
|
const run = process.platform === "win32" ? spawnSync(`"${bin}" index -o "${out}" --quiet`, { cwd: folder, encoding: "utf8", shell: true }) : spawnSync(bin, ["index", "-o", out, "--quiet"], { cwd: folder, encoding: "utf8" });
|
|
76
|
-
if (run.status !== 0 || !
|
|
211
|
+
if (run.status !== 0 || !existsSync3(out)) {
|
|
77
212
|
process.stderr.write(`story links: Storybook could not index its stories, so no link can be trusted:
|
|
78
213
|
${(run.stderr || run.stdout || "").trim()}
|
|
79
214
|
`);
|
|
80
215
|
return false;
|
|
81
216
|
}
|
|
82
|
-
const ids = new Set(Object.keys(JSON.parse(
|
|
217
|
+
const ids = new Set(Object.keys(JSON.parse(readFileSync3(out, "utf8")).entries));
|
|
83
218
|
const dead = linkProblems(registry, ids);
|
|
84
219
|
const count = registry.entries.reduce((n, entry) => n + (entry.storyId ? 1 : 0) + (entry.docsId ? 1 : 0), 0);
|
|
85
220
|
if (dead.length) {
|
|
@@ -131,12 +266,16 @@ ${more.slice(0, 3).join("\n")}${more.length > 3 ? `
|
|
|
131
266
|
`);
|
|
132
267
|
}
|
|
133
268
|
};
|
|
134
|
-
|
|
269
|
+
const here = !isPackage && existsSync3(resolve3(root, "package.json")) ? manifestAt(root).name ?? basename2(root) : null;
|
|
270
|
+
if (here) await add(root, value("repo"));
|
|
271
|
+
if (!values("also").length && !flag("here")) {
|
|
272
|
+
for (const sibling of findSiblings(workspaceOf(root), { skip: here ? [here] : [] })) await add(sibling.folder, sibling.name);
|
|
273
|
+
}
|
|
135
274
|
for (const also of values("also")) {
|
|
136
275
|
const [name, folder] = also.includes("=") ? [also.slice(0, also.indexOf("=")), also.slice(also.indexOf("=") + 1)] : [void 0, also];
|
|
137
|
-
const at =
|
|
138
|
-
if (!
|
|
139
|
-
process.stderr.write(`not searched: ${name ??
|
|
276
|
+
const at = resolve3(root, folder);
|
|
277
|
+
if (!existsSync3(resolve3(at, "package.json"))) {
|
|
278
|
+
process.stderr.write(`not searched: ${name ?? basename2(at)} \u2014 nothing at ${at}
|
|
140
279
|
`);
|
|
141
280
|
continue;
|
|
142
281
|
}
|
|
@@ -154,7 +293,7 @@ ${more.slice(0, 3).join("\n")}${more.length > 3 ? `
|
|
|
154
293
|
return 0;
|
|
155
294
|
}
|
|
156
295
|
case "build": {
|
|
157
|
-
const out =
|
|
296
|
+
const out = resolve3(root, value("out") ?? "registry.json");
|
|
158
297
|
if (isPackage) {
|
|
159
298
|
const { buildRegistry, serializeRegistry: serializeRegistry2 } = await packageBuilder();
|
|
160
299
|
const registry2 = buildRegistry({ root });
|
|
@@ -194,7 +333,8 @@ ${more.slice(0, 3).join("\n")}${more.length > 3 ? `
|
|
|
194
333
|
}
|
|
195
334
|
const linked2 = checkLinks(registry, root);
|
|
196
335
|
const shown2 = await checkGated(registry, root);
|
|
197
|
-
|
|
336
|
+
const loaded2 = checkLoader();
|
|
337
|
+
return broken.length || !linked2 || !shown2 || !loaded2 ? 1 : 0;
|
|
198
338
|
}
|
|
199
339
|
const { buildRegistry, serializeRegistry } = await packageBuilder();
|
|
200
340
|
const path = packageRegistryPath();
|
|
@@ -206,12 +346,12 @@ ${more.slice(0, 3).join("\n")}${more.length > 3 ? `
|
|
|
206
346
|
`);
|
|
207
347
|
return 1;
|
|
208
348
|
}
|
|
209
|
-
if (!
|
|
349
|
+
if (!existsSync3(path)) {
|
|
210
350
|
process.stderr.write(`${path} is missing \u2014 run "npm run registry"
|
|
211
351
|
`);
|
|
212
352
|
return 1;
|
|
213
353
|
}
|
|
214
|
-
if (
|
|
354
|
+
if (readFileSync3(path, "utf8").replace(/\r\n/g, "\n") !== serializeRegistry(built)) {
|
|
215
355
|
process.stderr.write(`${path} is not what the code produces \u2014 run "npm run registry" and commit the result
|
|
216
356
|
`);
|
|
217
357
|
return 1;
|
|
@@ -220,10 +360,32 @@ ${more.slice(0, 3).join("\n")}${more.length > 3 ? `
|
|
|
220
360
|
`);
|
|
221
361
|
const linked = checkLinks(built, root);
|
|
222
362
|
const shown = await checkGated(built, root, ["component"]);
|
|
223
|
-
|
|
363
|
+
const loaded = checkLoader();
|
|
364
|
+
return linked && shown && loaded ? 0 : 1;
|
|
365
|
+
}
|
|
366
|
+
case "skill": {
|
|
367
|
+
const out = value("out");
|
|
368
|
+
const folder = out ? resolve3(out) : repositoryOf(root);
|
|
369
|
+
if (relative2(folder, packageRoot()).startsWith("..")) {
|
|
370
|
+
process.stderr.write(`${folder} does not hold ${packageRoot()}: a loader there could not reach the skill's text
|
|
371
|
+
`);
|
|
372
|
+
return 1;
|
|
373
|
+
}
|
|
374
|
+
const path = loaderPath(folder);
|
|
375
|
+
mkdirSync(dirname3(path), { recursive: true });
|
|
376
|
+
writeFileSync(path, loaderText(folder, packageRoot()), "utf8");
|
|
377
|
+
process.stdout.write(`${path}: the Claude skill, read from ${packageRoot()}
|
|
378
|
+
`);
|
|
379
|
+
const settings = settingsWithSearch(folder);
|
|
380
|
+
if (settings) {
|
|
381
|
+
writeFileSync(settingsPath(folder), settings, "utf8");
|
|
382
|
+
process.stdout.write(`${settingsPath(folder)}: the search allowed without a prompt
|
|
383
|
+
`);
|
|
384
|
+
}
|
|
385
|
+
return 0;
|
|
224
386
|
}
|
|
225
387
|
default:
|
|
226
|
-
process.stderr.write("estiva-ui <find | build | check>\n");
|
|
388
|
+
process.stderr.write("estiva-ui <find | build | check | skill>\n");
|
|
227
389
|
return 1;
|
|
228
390
|
}
|
|
229
391
|
}
|
package/dist/registry/cli.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/registry/cli.ts"],
|
|
4
|
-
"sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `estiva-ui` \u2014 the catalogue as a command (UIG-12, UIG-13; docs/GATES.md \u00A723\n * for why it ships in the package rather than as a script pasted into each repo).\n *\n * estiva-ui find <words\u2026> [--also [name=]<folder>]\u2026 [--json] [--limit n]\n * estiva-ui build [--out <path>]\n * estiva-ui check\n *\n * Every command takes `--root <folder>` (the library it reads; default: here)\n * and `--repo <name>` (what an app's entries are called; default: its package name).\n *\n * **In the package** (`@estiva-app/ui` itself): `build` writes the committed\n * `registry.json`, and `check` rebuilds it and fails on a single byte of drift.\n *\n * **In an app**: nothing is committed (Katerina, 18 September 2026 \u2014 the\n * package is public, the apps are private). `check` builds the app's catalogue\n * and fails when a part has no one-line description or cannot be sorted, or\n * when a reusable part breaks the usage-page contract (UIG-19, `./contract`);\n * CI's `gate` job runs it. `build` writes the catalogue to a file only when asked.\n *\n * `find` searches the package's catalogue \u2014 the one shipped inside the installed\n * package, or this repo's own in the package \u2014 plus, in an app, the app's own,\n * built fresh, plus each `--also` app that sits beside it.\n */\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { basename, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { CONTRACT_KINDS, contractProblems, linkProblems } from './contract'\nimport { findInRegistries, formatFindings } from './find'\nimport { CLASSES, validateRegistry, type Registry } from './schema'\n\n/**\n * The builders, loaded only when they are used.\n *\n * They read TypeScript with TypeScript, and `typescript` is a dev dependency of\n * the app that installs this package \u2014 a real one in Peek, Ship and this repo,\n * and possibly absent elsewhere. A static import made the bundler put the whole\n * builder in the same chunk as `find`, so `npx estiva-ui find \u2026` died with\n * ERR_MODULE_NOT_FOUND before opening the file it needed.\n */\nconst packageBuilder = () => import('./build')\nconst appBuilder = () => import('./app')\nconst gatedScan = () => import('./gated')\n\nconst [command, ...rest] = process.argv.slice(2)\nconst VALUED = ['--limit', '--out', '--file', '--root', '--repo', '--also']\nconst flag = (name: string) => rest.includes(`--${name}`)\nconst value = (name: string) => {\n const at = rest.indexOf(`--${name}`)\n return at === -1 ? undefined : rest[at + 1]\n}\nconst values = (name: string) => rest.flatMap((word, i) => (word === `--${name}` && rest[i + 1] !== undefined ? [rest[i + 1]] : []))\n/** Everything that is not a flag or a flag's value: the words of the question. */\nconst plain = () => {\n const out: string[] = []\n for (let i = 0; i < rest.length; i++) {\n if (rest[i].startsWith('--')) {\n if (VALUED.includes(rest[i])) i += 1\n continue\n }\n out.push(rest[i])\n }\n return out\n}\n\nconst root = resolve(value('root') ?? '.')\nconst manifestAt = (folder: string) => JSON.parse(readFileSync(resolve(folder, 'package.json'), 'utf8')) as { name?: string }\n/** The package itself, or an app that installs it. */\nconst isPackage = existsSync(resolve(root, 'package.json')) && manifestAt(root).name === '@estiva-app/ui'\n\n/** The package's committed catalogue: `--file`, this repo's own in the package, else the copy inside the installed package. */\nfunction packageRegistryPath(): string {\n const named = value('file')\n if (named) return resolve(named)\n if (isPackage) return resolve(root, 'registry.json')\n // dist/registry/cli.js \u2192 the package's own root. Never an app's own folder:\n // a registry.json there is the app's, written by `estiva-ui build`.\n return fileURLToPath(new URL('../../registry.json', import.meta.url))\n}\n\nfunction readPackageRegistry(): Registry {\n const path = packageRegistryPath()\n if (!existsSync(path)) throw new Error(`no registry.json at ${path} \u2014 run \"estiva-ui build\" in the package first`)\n const registry = JSON.parse(readFileSync(path, 'utf8')) as Registry\n const problems = validateRegistry(registry)\n // A malformed catalogue is worse than none: it would answer, wrongly.\n if (problems.length) throw new Error(`${path} does not match the schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\nasync function buildApp(folder: string, repo: string | undefined): Promise<Registry> {\n const { buildAppRegistry } = await appBuilder()\n const registry = buildAppRegistry({ root: folder, repo })\n const problems = validateRegistry(registry)\n if (problems.length) throw new Error(`the catalogue of ${registry.builtFrom.repo} does not match its own schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\n/**\n * Every story and docs link in a catalogue, against the Storybook beside it\n * (UIG-19). `storybook index` writes the ids Storybook itself would serve \u2014 no\n * server, no browser, seconds \u2014 so a renamed story, or a page that no longer\n * compiles, fails here rather than as a dead link somebody finds later.\n * Reports, and says whether it passed. A folder with no Storybook has nothing\n * to check.\n */\nfunction checkLinks(registry: Registry, folder: string): boolean {\n const bin = resolve(folder, 'node_modules', '.bin', process.platform === 'win32' ? 'storybook.cmd' : 'storybook')\n if (!existsSync(resolve(folder, '.storybook')) || !existsSync(bin)) {\n process.stdout.write('story links: no Storybook here, nothing to check\\n')\n return true\n }\n const out = join(mkdtempSync(join(tmpdir(), 'estiva-ui-index-')), 'index.json')\n // A `.cmd` needs a shell on Windows, and Node wants that as one quoted line\n // rather than an argument list. Both paths are ours: the app's folder and a temp file.\n const run =\n process.platform === 'win32'\n ? spawnSync(`\"${bin}\" index -o \"${out}\" --quiet`, { cwd: folder, encoding: 'utf8', shell: true })\n : spawnSync(bin, ['index', '-o', out, '--quiet'], { cwd: folder, encoding: 'utf8' })\n if (run.status !== 0 || !existsSync(out)) {\n process.stderr.write(`story links: Storybook could not index its stories, so no link can be trusted:\\n${(run.stderr || run.stdout || '').trim()}\\n`)\n return false\n }\n const ids = new Set(Object.keys((JSON.parse(readFileSync(out, 'utf8')) as { entries: Record<string, unknown> }).entries))\n const dead = linkProblems(registry, ids)\n const count = registry.entries.reduce((n, entry) => n + (entry.storyId ? 1 : 0) + (entry.docsId ? 1 : 0), 0)\n if (dead.length) {\n process.stderr.write(`story links: ${dead.length} of ${count} lead nowhere:\\n ${dead.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write(`story links: all ${count} open a story or docs page Storybook has\\n`)\n return true\n}\n\n/**\n * What a part draws only when a prop is passed, where no story passes it\n * (UIG-19, `./gated`). Strict, by Katerina's ruling: a part's own stories show\n * everything it can draw. In an app it reads the reusable parts; in the package,\n * every component (`kinds` is the package's one class, `component`).\n */\nasync function checkGated(registry: Registry, folder: string, kinds?: string[]): Promise<boolean> {\n const { gatedFindings } = await gatedScan()\n const found = gatedFindings(registry, folder, { ...(kinds ? { kinds } : {}), pictured: 'own' })\n if (found.length) {\n const lines = found.map((f) => `${f.part} (${f.file}) draws something only when given \\`${f.prop}\\`, and no story that draws it passes \\`${f.prop}\\`: pass it in a story`)\n process.stderr.write(`stories: ${found.length === 1 ? 'one thing a part draws is' : `${found.length} things parts draw are`} never shown:\\n ${lines.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write('stories: everything a part draws behind a prop is shown by a story\\n')\n return true\n}\n\n/** One line per kind, in a fixed order, so two runs read the same. */\nfunction summary(registry: Registry): string {\n const count = (cls: string) => registry.entries.filter((entry) => entry.app?.class === cls).length\n const kinds = CLASSES.map((cls) => `${count(cls)} ${cls}`).join(', ')\n const files = registry.builtFrom.files ?? 0\n return `${registry.builtFrom.repo}: ${registry.entries.length} parts in ${files - registry.filesWithoutParts.length} files (${registry.filesWithoutParts.length} more hold none) \u2014 ${kinds}. Every part is described and sorted.`\n}\n\nasync function main(): Promise<number> {\n switch (command) {\n case 'find': {\n const query = plain().join(' ')\n if (!query.trim()) {\n process.stderr.write('estiva-ui find <words\u2026> \u2014 what the thing you need does, in words\\n')\n return 1\n }\n const registries: Registry[] = [readPackageRegistry()]\n // An app that cannot be read is said out loud, and the rest still answer.\n const add = async (folder: string, repo: string | undefined) => {\n try {\n registries.push(await buildApp(folder, repo))\n } catch (error) {\n const [first, ...more] = String(error instanceof Error ? error.message : error).split('\\n')\n process.stderr.write(`not searched: ${first}${more.length ? `\\n${more.slice(0, 3).join('\\n')}${more.length > 3 ? `\\n \u2026and ${more.length - 3} more \u2014 run \"estiva-ui check\" there` : ''}` : ''}\\n`)\n }\n }\n if (!isPackage) await add(root, value('repo'))\n for (const also of values('also')) {\n const [name, folder] = also.includes('=') ? [also.slice(0, also.indexOf('=')), also.slice(also.indexOf('=') + 1)] : [undefined, also]\n const at = resolve(root, folder)\n if (!existsSync(resolve(at, 'package.json'))) {\n process.stderr.write(`not searched: ${name ?? basename(at)} \u2014 nothing at ${at}\\n`)\n continue\n }\n await add(at, name)\n }\n const limit = Number(value('limit') ?? 5)\n const findings = findInRegistries(registries, query, { limit: Number.isFinite(limit) && limit > 0 ? limit : 5 })\n if (flag('json')) {\n process.stdout.write(`${JSON.stringify(findings.map((finding) => finding.entry), null, 2)}\\n`)\n return 0\n }\n process.stdout.write(`${formatFindings(registries, findings, query)}\\n`)\n // Nothing found is not a failure \u2014 it is the answer that sends a session\n // to ask rather than to invent.\n return 0\n }\n\n case 'build': {\n const out = resolve(root, value('out') ?? 'registry.json')\n if (isPackage) {\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const registry = buildRegistry({ root })\n const problems = validateRegistry(registry)\n if (problems.length) {\n process.stderr.write(`the registry this build produced does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${registry.entries.length} entries from ${registry.builtFrom.exports} exports\\n`)\n return 0\n }\n const { serializeRegistry } = await packageBuilder()\n const registry = await buildApp(root, value('repo'))\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${summary(registry)}\\n`)\n return 0\n }\n\n case 'check': {\n if (!isPackage) {\n const registry = await buildApp(root, value('repo'))\n process.stdout.write(`${summary(registry)}\\n`)\n // The usage-page contract and the story links (UIG-19): the one copy, run by every app's `gate` job.\n const broken = contractProblems(registry, root)\n if (broken.length) {\n process.stderr.write(`${broken.length === 1 ? 'a part breaks' : `${broken.length} parts break`} the usage-page contract:\\n ${broken.join('\\n ')}\\n`)\n } else {\n const owed = registry.entries.filter((entry) => (CONTRACT_KINDS as readonly string[]).includes(entry.app?.class ?? '')).length\n process.stdout.write(`usage pages: all ${owed} reusable parts keep the contract and are drawn somewhere\\n`)\n }\n const linked = checkLinks(registry, root)\n const shown = await checkGated(registry, root)\n return broken.length || !linked || !shown ? 1 : 0\n }\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const path = packageRegistryPath()\n const built = buildRegistry({ root })\n const problems = validateRegistry(built)\n if (problems.length) {\n process.stderr.write(`the registry does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n if (!existsSync(path)) {\n process.stderr.write(`${path} is missing \u2014 run \"npm run registry\"\\n`)\n return 1\n }\n // Byte for byte. The point of the check is that the committed file cannot\n // drift from the code; \"near enough\" is drift.\n if (readFileSync(path, 'utf8').replace(/\\r\\n/g, '\\n') !== serializeRegistry(built)) {\n process.stderr.write(`${path} is not what the code produces \u2014 run \"npm run registry\" and commit the result\\n`)\n return 1\n }\n process.stdout.write(`${path}: current, ${built.entries.length} entries from ${built.builtFrom.exports} exports\\n`)\n const linked = checkLinks(built, root)\n const shown = await checkGated(built, root, ['component'])\n return linked && shown ? 0 : 1\n }\n\n default:\n process.stderr.write('estiva-ui <find | build | check>\\n')\n return 1\n }\n}\n\ntry {\n process.exitCode = await main()\n} catch (error) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`)\n process.exitCode = 1\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["serializeRegistry", "registry", "linked", "shown"]
|
|
3
|
+
"sources": ["../../src/registry/cli.ts", "../../src/registry/siblings.ts", "../../src/registry/skill.ts"],
|
|
4
|
+
"sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `estiva-ui` \u2014 the catalogue as a command (UIG-12, UIG-13; docs/GATES.md \u00A723\n * for why it ships in the package rather than as a script pasted into each repo).\n *\n * estiva-ui find <words\u2026> [--here | --also [name=]<folder>\u2026] [--json] [--limit n]\n * estiva-ui build [--out <path>]\n * estiva-ui check\n * estiva-ui skill [--out <folder>]\n *\n * Every command takes `--root <folder>` (the library it reads; default: here)\n * and `--repo <name>` (what an app's entries are called; default: its package name).\n *\n * **In the package** (`@estiva-app/ui` itself): `build` writes the committed\n * `registry.json`, and `check` rebuilds it and fails on a single byte of drift.\n *\n * **In an app**: nothing is committed (Katerina, 18 September 2026 \u2014 the\n * package is public, the apps are private). `check` builds the app's catalogue\n * and fails when a part has no one-line description or cannot be sorted, or\n * when a reusable part breaks the usage-page contract (UIG-19, `./contract`);\n * CI's `gate` job runs it. `build` writes the catalogue to a file only when asked.\n *\n * `find` searches the package's catalogue \u2014 the one shipped inside the installed\n * package, or this repo's own in the package \u2014 plus, in an app, the app's own,\n * built fresh, plus every app beside it (UIG-20, `./siblings`): found, not\n * named, so one command answers the same from any repository, or from the\n * folder that holds them. `--here` searches no neighbour; `--also` names them.\n *\n * `skill` writes the repository's Claude skill loader (UIG-20, `./skill`), or\n * the one for `--out <folder>`; `check` fails when the repository's is missing\n * or differs.\n */\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { basename, dirname, join, relative, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { CONTRACT_KINDS, contractProblems, linkProblems } from './contract'\nimport { findInRegistries, formatFindings } from './find'\nimport { findSiblings, workspaceOf } from './siblings'\nimport { loaderPath, loaderProblem, loaderText, repositoryOf, settingsPath, settingsWithSearch } from './skill'\nimport { CLASSES, validateRegistry, type Registry } from './schema'\n\n/**\n * The builders, loaded only when they are used.\n *\n * They read TypeScript with TypeScript, and `typescript` is a dev dependency of\n * the app that installs this package \u2014 a real one in Peek, Ship and this repo,\n * and possibly absent elsewhere. A static import made the bundler put the whole\n * builder in the same chunk as `find`, so `npx estiva-ui find \u2026` died with\n * ERR_MODULE_NOT_FOUND before opening the file it needed.\n */\nconst packageBuilder = () => import('./build')\nconst appBuilder = () => import('./app')\nconst gatedScan = () => import('./gated')\n\nconst [command, ...rest] = process.argv.slice(2)\nconst VALUED = ['--limit', '--out', '--file', '--root', '--repo', '--also']\nconst flag = (name: string) => rest.includes(`--${name}`)\nconst value = (name: string) => {\n const at = rest.indexOf(`--${name}`)\n return at === -1 ? undefined : rest[at + 1]\n}\nconst values = (name: string) => rest.flatMap((word, i) => (word === `--${name}` && rest[i + 1] !== undefined ? [rest[i + 1]] : []))\n/** Everything that is not a flag or a flag's value: the words of the question. */\nconst plain = () => {\n const out: string[] = []\n for (let i = 0; i < rest.length; i++) {\n if (rest[i].startsWith('--')) {\n if (VALUED.includes(rest[i])) i += 1\n continue\n }\n out.push(rest[i])\n }\n return out\n}\n\nconst root = resolve(value('root') ?? '.')\nconst manifestAt = (folder: string) => JSON.parse(readFileSync(resolve(folder, 'package.json'), 'utf8')) as { name?: string }\n/** The package itself, or an app that installs it. */\nconst isPackage = existsSync(resolve(root, 'package.json')) && manifestAt(root).name === '@estiva-app/ui'\n\n/** The package's committed catalogue: `--file`, this repo's own in the package, else the copy inside the installed package. */\nfunction packageRegistryPath(): string {\n const named = value('file')\n if (named) return resolve(named)\n if (isPackage) return resolve(root, 'registry.json')\n // dist/registry/cli.js \u2192 the package's own root. Never an app's own folder:\n // a registry.json there is the app's, written by `estiva-ui build`.\n return fileURLToPath(new URL('../../registry.json', import.meta.url))\n}\n\nfunction readPackageRegistry(): Registry {\n const path = packageRegistryPath()\n if (!existsSync(path)) throw new Error(`no registry.json at ${path} \u2014 run \"estiva-ui build\" in the package first`)\n const registry = JSON.parse(readFileSync(path, 'utf8')) as Registry\n const problems = validateRegistry(registry)\n // A malformed catalogue is worse than none: it would answer, wrongly.\n if (problems.length) throw new Error(`${path} does not match the schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\n/** The installed package whose skill text a loader reads: this repo in the package, else the app's install. */\nconst packageRoot = () => (isPackage ? root : resolve(root, 'node_modules', '@estiva-app', 'ui'))\n\n/** The Claude skill loader (UIG-20): reports, and says whether it passed. */\nfunction checkLoader(): boolean {\n const problem = loaderProblem(repositoryOf(root), packageRoot())\n if (problem) {\n process.stderr.write(`claude skill: ${problem}\\n`)\n return false\n }\n process.stdout.write('claude skill: the loader reads the package\u2019s text\\n')\n return true\n}\n\nasync function buildApp(folder: string, repo: string | undefined): Promise<Registry> {\n const { buildAppRegistry } = await appBuilder()\n const registry = buildAppRegistry({ root: folder, repo })\n const problems = validateRegistry(registry)\n if (problems.length) throw new Error(`the catalogue of ${registry.builtFrom.repo} does not match its own schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\n/**\n * Every story and docs link in a catalogue, against the Storybook beside it\n * (UIG-19). `storybook index` writes the ids Storybook itself would serve \u2014 no\n * server, no browser, seconds \u2014 so a renamed story, or a page that no longer\n * compiles, fails here rather than as a dead link somebody finds later.\n * Reports, and says whether it passed. A folder with no Storybook has nothing\n * to check.\n */\nfunction checkLinks(registry: Registry, folder: string): boolean {\n const bin = resolve(folder, 'node_modules', '.bin', process.platform === 'win32' ? 'storybook.cmd' : 'storybook')\n if (!existsSync(resolve(folder, '.storybook')) || !existsSync(bin)) {\n process.stdout.write('story links: no Storybook here, nothing to check\\n')\n return true\n }\n const out = join(mkdtempSync(join(tmpdir(), 'estiva-ui-index-')), 'index.json')\n // A `.cmd` needs a shell on Windows, and Node wants that as one quoted line\n // rather than an argument list. Both paths are ours: the app's folder and a temp file.\n const run =\n process.platform === 'win32'\n ? spawnSync(`\"${bin}\" index -o \"${out}\" --quiet`, { cwd: folder, encoding: 'utf8', shell: true })\n : spawnSync(bin, ['index', '-o', out, '--quiet'], { cwd: folder, encoding: 'utf8' })\n if (run.status !== 0 || !existsSync(out)) {\n process.stderr.write(`story links: Storybook could not index its stories, so no link can be trusted:\\n${(run.stderr || run.stdout || '').trim()}\\n`)\n return false\n }\n const ids = new Set(Object.keys((JSON.parse(readFileSync(out, 'utf8')) as { entries: Record<string, unknown> }).entries))\n const dead = linkProblems(registry, ids)\n const count = registry.entries.reduce((n, entry) => n + (entry.storyId ? 1 : 0) + (entry.docsId ? 1 : 0), 0)\n if (dead.length) {\n process.stderr.write(`story links: ${dead.length} of ${count} lead nowhere:\\n ${dead.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write(`story links: all ${count} open a story or docs page Storybook has\\n`)\n return true\n}\n\n/**\n * What a part draws only when a prop is passed, where no story passes it\n * (UIG-19, `./gated`). Strict, by Katerina's ruling: a part's own stories show\n * everything it can draw. In an app it reads the reusable parts; in the package,\n * every component (`kinds` is the package's one class, `component`).\n */\nasync function checkGated(registry: Registry, folder: string, kinds?: string[]): Promise<boolean> {\n const { gatedFindings } = await gatedScan()\n const found = gatedFindings(registry, folder, { ...(kinds ? { kinds } : {}), pictured: 'own' })\n if (found.length) {\n const lines = found.map((f) => `${f.part} (${f.file}) draws something only when given \\`${f.prop}\\`, and no story that draws it passes \\`${f.prop}\\`: pass it in a story`)\n process.stderr.write(`stories: ${found.length === 1 ? 'one thing a part draws is' : `${found.length} things parts draw are`} never shown:\\n ${lines.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write('stories: everything a part draws behind a prop is shown by a story\\n')\n return true\n}\n\n/** One line per kind, in a fixed order, so two runs read the same. */\nfunction summary(registry: Registry): string {\n const count = (cls: string) => registry.entries.filter((entry) => entry.app?.class === cls).length\n const kinds = CLASSES.map((cls) => `${count(cls)} ${cls}`).join(', ')\n const files = registry.builtFrom.files ?? 0\n return `${registry.builtFrom.repo}: ${registry.entries.length} parts in ${files - registry.filesWithoutParts.length} files (${registry.filesWithoutParts.length} more hold none) \u2014 ${kinds}. Every part is described and sorted.`\n}\n\nasync function main(): Promise<number> {\n switch (command) {\n case 'find': {\n const query = plain().join(' ')\n if (!query.trim()) {\n process.stderr.write('estiva-ui find <words\u2026> \u2014 what the thing you need does, in words\\n')\n return 1\n }\n const registries: Registry[] = [readPackageRegistry()]\n // An app that cannot be read is said out loud, and the rest still answer.\n const add = async (folder: string, repo: string | undefined) => {\n try {\n registries.push(await buildApp(folder, repo))\n } catch (error) {\n const [first, ...more] = String(error instanceof Error ? error.message : error).split('\\n')\n process.stderr.write(`not searched: ${first}${more.length ? `\\n${more.slice(0, 3).join('\\n')}${more.length > 3 ? `\\n \u2026and ${more.length - 3} more \u2014 run \"estiva-ui check\" there` : ''}` : ''}\\n`)\n }\n }\n // The folder it runs in is searched when it is an app; the folder that\n // holds the repositories is not one, and only its neighbours answer.\n const here = !isPackage && existsSync(resolve(root, 'package.json')) ? (manifestAt(root).name ?? basename(root)) : null\n if (here) await add(root, value('repo'))\n if (!values('also').length && !flag('here')) {\n for (const sibling of findSiblings(workspaceOf(root), { skip: here ? [here] : [] })) await add(sibling.folder, sibling.name)\n }\n for (const also of values('also')) {\n const [name, folder] = also.includes('=') ? [also.slice(0, also.indexOf('=')), also.slice(also.indexOf('=') + 1)] : [undefined, also]\n const at = resolve(root, folder)\n if (!existsSync(resolve(at, 'package.json'))) {\n process.stderr.write(`not searched: ${name ?? basename(at)} \u2014 nothing at ${at}\\n`)\n continue\n }\n await add(at, name)\n }\n const limit = Number(value('limit') ?? 5)\n const findings = findInRegistries(registries, query, { limit: Number.isFinite(limit) && limit > 0 ? limit : 5 })\n if (flag('json')) {\n process.stdout.write(`${JSON.stringify(findings.map((finding) => finding.entry), null, 2)}\\n`)\n return 0\n }\n process.stdout.write(`${formatFindings(registries, findings, query)}\\n`)\n // Nothing found is not a failure \u2014 it is the answer that sends a session\n // to ask rather than to invent.\n return 0\n }\n\n case 'build': {\n const out = resolve(root, value('out') ?? 'registry.json')\n if (isPackage) {\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const registry = buildRegistry({ root })\n const problems = validateRegistry(registry)\n if (problems.length) {\n process.stderr.write(`the registry this build produced does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${registry.entries.length} entries from ${registry.builtFrom.exports} exports\\n`)\n return 0\n }\n const { serializeRegistry } = await packageBuilder()\n const registry = await buildApp(root, value('repo'))\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${summary(registry)}\\n`)\n return 0\n }\n\n case 'check': {\n if (!isPackage) {\n const registry = await buildApp(root, value('repo'))\n process.stdout.write(`${summary(registry)}\\n`)\n // The usage-page contract and the story links (UIG-19): the one copy, run by every app's `gate` job.\n const broken = contractProblems(registry, root)\n if (broken.length) {\n process.stderr.write(`${broken.length === 1 ? 'a part breaks' : `${broken.length} parts break`} the usage-page contract:\\n ${broken.join('\\n ')}\\n`)\n } else {\n const owed = registry.entries.filter((entry) => (CONTRACT_KINDS as readonly string[]).includes(entry.app?.class ?? '')).length\n process.stdout.write(`usage pages: all ${owed} reusable parts keep the contract and are drawn somewhere\\n`)\n }\n const linked = checkLinks(registry, root)\n const shown = await checkGated(registry, root)\n const loaded = checkLoader()\n return broken.length || !linked || !shown || !loaded ? 1 : 0\n }\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const path = packageRegistryPath()\n const built = buildRegistry({ root })\n const problems = validateRegistry(built)\n if (problems.length) {\n process.stderr.write(`the registry does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n if (!existsSync(path)) {\n process.stderr.write(`${path} is missing \u2014 run \"npm run registry\"\\n`)\n return 1\n }\n // Byte for byte. The point of the check is that the committed file cannot\n // drift from the code; \"near enough\" is drift.\n if (readFileSync(path, 'utf8').replace(/\\r\\n/g, '\\n') !== serializeRegistry(built)) {\n process.stderr.write(`${path} is not what the code produces \u2014 run \"npm run registry\" and commit the result\\n`)\n return 1\n }\n process.stdout.write(`${path}: current, ${built.entries.length} entries from ${built.builtFrom.exports} exports\\n`)\n const linked = checkLinks(built, root)\n const shown = await checkGated(built, root, ['component'])\n const loaded = checkLoader()\n return linked && shown && loaded ? 0 : 1\n }\n\n case 'skill': {\n const out = value('out')\n const folder = out ? resolve(out) : repositoryOf(root)\n // The loader's path starts where the session does and cannot climb out:\n // Claude Code refuses a `..` in it.\n if (relative(folder, packageRoot()).startsWith('..')) {\n process.stderr.write(`${folder} does not hold ${packageRoot()}: a loader there could not reach the skill's text\\n`)\n return 1\n }\n const path = loaderPath(folder)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, loaderText(folder, packageRoot()), 'utf8')\n process.stdout.write(`${path}: the Claude skill, read from ${packageRoot()}\\n`)\n const settings = settingsWithSearch(folder)\n if (settings) {\n writeFileSync(settingsPath(folder), settings, 'utf8')\n process.stdout.write(`${settingsPath(folder)}: the search allowed without a prompt\\n`)\n }\n return 0\n }\n\n default:\n process.stderr.write('estiva-ui <find | build | check | skill>\\n')\n return 1\n }\n}\n\ntry {\n process.exitCode = await main()\n} catch (error) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`)\n process.exitCode = 1\n}\n", "/// <reference types=\"node\" />\n/**\n * The apps beside this one, found rather than named (UIG-20).\n *\n * `estiva-ui find` with nothing else typed searches the package, the app it is\n * run in, and every app that sits beside it. Katerina's condition for having no\n * shared Storybook is that one search, run unasked from any repository, covers\n * every catalogue; a list of names written into each repository's script would\n * be the drifting copy the gates exist to remove, and would miss the next app.\n *\n * An app is a folder, or one folder inside it, whose `package.json` depends on\n * `@estiva-app/ui` and which has a `src` to read. A service that does not\n * install the package is not an app of this catalogue until it does.\n *\n * Each app is searched once. A second checkout of the same app (a worktree, or\n * somebody's copy) carries the same package name; the app the search runs in\n * wins, then the one whose top folder has the shortest name.\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'\nimport { basename, dirname, join, resolve } from 'node:path'\n\nexport const PACKAGE_NAME = '@estiva-app/ui'\n\nexport interface Sibling {\n /** What its entries are called: its top folder's name. */\n name: string\n /** The folder the catalogue is built from. */\n folder: string\n /** Its `package.json` name, which says two checkouts are one app. */\n packageName: string\n}\n\nfunction manifest(folder: string): { name?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> } | null {\n const file = join(folder, 'package.json')\n if (!existsSync(file)) return null\n try {\n return JSON.parse(readFileSync(file, 'utf8'))\n } catch {\n return null\n }\n}\n\n/** The name of the app in `folder`, or null when the folder is not one. */\nexport function appName(folder: string): string | null {\n const m = manifest(folder)\n if (!m || m.name === PACKAGE_NAME) return null\n const uses = Boolean(m.dependencies?.[PACKAGE_NAME] ?? m.devDependencies?.[PACKAGE_NAME])\n return uses && existsSync(join(folder, 'src')) ? (m.name ?? basename(folder)) : null\n}\n\nconst isDir = (path: string) => {\n try {\n return statSync(path).isDirectory()\n } catch {\n return false\n }\n}\n\n/**\n * The folder that holds the repositories: the parent of the repository `from`\n * is in, or `from` itself when it is in none (a folder that only holds them).\n */\nexport function workspaceOf(from: string): string {\n let at = resolve(from)\n for (;;) {\n if (existsSync(join(at, '.git'))) return dirname(at)\n const up = dirname(at)\n if (up === at) return resolve(from)\n at = up\n }\n}\n\n/** Every app in `workspace`, one per package name, except those named in `skip`. */\nexport function findSiblings(workspace: string, { skip = [] }: { skip?: string[] } = {}): Sibling[] {\n let tops: string[]\n try {\n tops = readdirSync(workspace).filter((name) => !name.startsWith('.') && name !== 'node_modules' && isDir(join(workspace, name)))\n } catch {\n return []\n }\n const found: Sibling[] = []\n for (const top of tops) {\n const folder = join(workspace, top)\n const inside = readdirSync(folder).filter((name) => !name.startsWith('.') && name !== 'node_modules' && isDir(join(folder, name)))\n for (const candidate of [folder, ...inside.map((name) => join(folder, name))]) {\n const packageName = appName(candidate)\n if (packageName) found.push({ name: top, folder: candidate, packageName })\n }\n }\n const byPackage = new Map<string, Sibling>()\n for (const sibling of found.sort((a, b) => a.name.length - b.name.length || a.name.localeCompare(b.name))) {\n if (skip.includes(sibling.packageName) || byPackage.has(sibling.packageName)) continue\n byPackage.set(sibling.packageName, sibling)\n }\n return [...byPackage.values()].sort((a, b) => a.name.localeCompare(b.name))\n}\n", "/// <reference types=\"node\" />\n/**\n * The Claude skill (UIG-20): search the catalogue before building anything.\n *\n * Its text lives once, in this package, as `skill/estiva-ui.md` (Katerina,\n * 17 September 2026: the same gate text pasted into two repositories is a\n * defect). Claude Code only loads a skill from a `SKILL.md` under a folder's\n * `.claude/skills/`, and no plugin source can point into `node_modules`, so each\n * repository commits a loader: the trigger below, and one line that reads the\n * installed package's text into the skill when it opens (`` !`\u2026` `` runs before\n * Claude sees the skill). `estiva-ui skill` writes it; `estiva-ui check` fails\n * when a repository's loader is missing or is not what this writes.\n *\n * Measured with fresh sessions of Claude Code 2.1.215 (UIG-20), three things\n * shape that line and the settings beside it:\n *\n * - An injected command runs without a prompt only when Claude Code can read it\n * statically as read-only: a plain `cat` of a plain path. A path with `..`\n * (\"may follow a symlink outside the working directory\"), a `$(\u2026)`, or a\n * `node -e` are all refused, and a refused command stops the skill opening.\n * So the path starts at `${CLAUDE_PROJECT_DIR}`, the folder the session\n * started in, as the editor gate's hook does: a session started above or\n * below the repository gets neither.\n * - An `allowed-tools` line makes opening the skill itself ask for permission\n * (\"Execute skill\"). So the search is allowed in the repository's committed\n * `.claude/settings.json` instead, and the skill carries no `allowed-tools`.\n * - Claude reads a skill's description before it opens the skill, so the\n * trigger is written into the loader; it is the one part that cannot be\n * pulled in later.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join, relative, resolve, sep } from 'node:path'\n\nexport const SKILL_NAME = 'estiva-ui'\n\n/** When Claude opens the skill by itself. Kept under Claude Code's 1,536 characters. */\nexport const SKILL_DESCRIPTION =\n 'Use before building, adding or changing anything on screen in an app that uses @estiva-app/ui, or in @estiva-app/ui itself: ' +\n 'a screen, panel, list, row, header, dialog, menu, empty state, a new component, or how something looks. ' +\n 'Also when someone asks for a component in plain words (\"I need a\u2026\", \"add a\u2026\", \"is there a\u2026\"). ' +\n \"It searches every catalogue at once, the package's and each app's Storybook, for a part that already does the job, \" +\n 'and lists the mistakes this codebase keeps making.'\n\n/** The search, allowed without a prompt in the folder's committed `.claude/settings.json`. */\nexport const SEARCH_RULES = ['Bash(npm run ui:find *)', 'Bash(npm --prefix * run ui:find *)', 'PowerShell(npm run ui:find *)', 'PowerShell(npm --prefix * run ui:find *)']\n\n/** The skill's text, inside a package folder. */\nexport const skillTextPath = (packageRoot: string) => join(packageRoot, 'skill', `${SKILL_NAME}.md`)\n\n/** Where a folder keeps its loader. */\nexport const loaderPath = (folder: string) => join(folder, '.claude', 'skills', SKILL_NAME, 'SKILL.md')\n\n/** Where a folder keeps the settings that allow the search. */\nexport const settingsPath = (folder: string) => join(folder, '.claude', 'settings.json')\n\n/** The loader for `folder`, reading the text from `packageRoot`, from where a session in `folder` starts. */\nexport function loaderText(folder: string, packageRoot: string): string {\n const fromTop = relative(folder, skillTextPath(packageRoot)).split(sep).join('/')\n return [\n '---',\n `name: ${SKILL_NAME}`,\n `description: ${JSON.stringify(SKILL_DESCRIPTION)}`,\n '---',\n '',\n '<!-- Written by `estiva-ui skill` from @estiva-app/ui; `estiva-ui check` fails when it differs. The text is the installed package\u2019s, read in below. -->',\n '',\n `!\\`cat \"\\${CLAUDE_PROJECT_DIR}/${fromTop}\"\\``,\n '',\n ].join('\\n')\n}\n\ntype Settings = { permissions?: { allow?: string[] } & Record<string, unknown> } & Record<string, unknown>\n\nconst readSettings = (folder: string): Settings | null => {\n const path = settingsPath(folder)\n if (!existsSync(path)) return {}\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Settings\n } catch {\n return null\n }\n}\n\n/** `folder`'s settings with the search allowed, everything else as it was; null when they cannot be read. */\nexport function settingsWithSearch(folder: string): string | null {\n const settings = readSettings(folder)\n if (!settings) return null\n const allow = settings.permissions?.allow ?? []\n const missing = SEARCH_RULES.filter((rule) => !allow.includes(rule))\n if (!missing.length && existsSync(settingsPath(folder))) return null\n settings.permissions = { ...settings.permissions, allow: [...allow, ...missing] }\n return `${JSON.stringify(settings, null, 2)}\\n`\n}\n\n/** The repository `from` is in: the nearest folder with `.git`, or `from` when there is none. */\nexport function repositoryOf(from: string): string {\n let at = resolve(from)\n for (;;) {\n if (existsSync(join(at, '.git'))) return at\n const up = dirname(at)\n if (up === at) return resolve(from)\n at = up\n }\n}\n\n/** What is wrong with `folder`'s loader, or null when it is what `loaderText` writes. */\nexport function loaderProblem(folder: string, packageRoot: string): string | null {\n const path = loaderPath(folder)\n const shown = relative(process.cwd(), path).split(sep).join('/') || path\n if (!existsSync(path)) return `${shown} is missing: run \"estiva-ui skill\" and commit it`\n if (readFileSync(path, 'utf8').replace(/\\r\\n/g, '\\n') !== loaderText(folder, packageRoot)) return `${shown} is not what the package writes: run \"estiva-ui skill\" and commit it`\n if (!existsSync(skillTextPath(packageRoot))) return `${shown} reads ${skillTextPath(packageRoot)}, which is not there`\n const allow = readSettings(folder)?.permissions?.allow ?? []\n const missing = SEARCH_RULES.filter((rule) => !allow.includes(rule))\n if (missing.length) return `.claude/settings.json does not allow the search (${missing.join(', ')}): run \"estiva-ui skill\" and commit it`\n return null\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["existsSync", "readFileSync", "basename", "dirname", "join", "relative", "resolve", "existsSync", "readFileSync", "dirname", "join", "resolve", "packageRoot", "resolve", "readFileSync", "existsSync", "join", "basename", "serializeRegistry", "registry", "linked", "shown", "loaded", "relative", "dirname"]
|
|
7
7
|
}
|
package/dist/registry/find.d.ts
CHANGED
|
@@ -43,6 +43,11 @@ export declare function findInRegistry(registry: Registry, query: string, { limi
|
|
|
43
43
|
* An app's pass-on is not a result of its own — it is the package's part, and
|
|
44
44
|
* the package's entry answers for it, saying which apps hand it on. At the
|
|
45
45
|
* same strength the package's part comes first: it is the one to reach for.
|
|
46
|
+
* And a package part whose name carries one of the words comes before an app's
|
|
47
|
+
* part answering as many words, whatever each scored: "panel header" put an
|
|
48
|
+
* app's own `Header`, whose whole name is one of the words, above the package's
|
|
49
|
+
* `ContainerHeader` (UIG-20). Only by name: a package part the words reach
|
|
50
|
+
* through its notes alone would bury the app part that is the answer.
|
|
46
51
|
*/
|
|
47
52
|
export declare function findInRegistries(registries: Registry[], query: string, { limit }?: {
|
|
48
53
|
limit?: number;
|