@milesroxas/design-hub 0.0.0-stage → 0.1.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.
Files changed (217) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/client.d.ts +6 -0
  4. package/configs/biome/biome.json +63 -0
  5. package/configs/tsconfig.project.json +17 -0
  6. package/core/bytes.test.ts +14 -0
  7. package/core/bytes.ts +14 -0
  8. package/core/catalog.test.ts +44 -0
  9. package/core/catalog.ts +43 -0
  10. package/core/checks.test.ts +23 -0
  11. package/core/checks.ts +72 -0
  12. package/core/contacts.test.ts +31 -0
  13. package/core/contacts.ts +38 -0
  14. package/core/doc.ts +4 -0
  15. package/core/feature.ts +6 -0
  16. package/core/fixtures.test.ts +33 -0
  17. package/core/fixtures.ts +69 -0
  18. package/core/frontmatter.ts +15 -0
  19. package/core/html.ts +3 -0
  20. package/core/image.test.ts +22 -0
  21. package/core/image.ts +37 -0
  22. package/core/jev.test.ts +66 -0
  23. package/core/jev.ts +77 -0
  24. package/core/review/model.ts +189 -0
  25. package/core/review/policy.ts +26 -0
  26. package/core/review/service.test.ts +163 -0
  27. package/core/review/service.ts +309 -0
  28. package/core/review/store.ts +51 -0
  29. package/core/review/triage.test.ts +41 -0
  30. package/core/review/triage.ts +58 -0
  31. package/core/states.test.ts +36 -0
  32. package/core/states.ts +34 -0
  33. package/dist/core/bytes.d.ts +5 -0
  34. package/dist/core/bytes.js +9 -0
  35. package/dist/core/catalog.d.ts +17 -0
  36. package/dist/core/catalog.js +24 -0
  37. package/dist/core/checks.d.ts +41 -0
  38. package/dist/core/checks.js +53 -0
  39. package/dist/core/contacts.d.ts +9 -0
  40. package/dist/core/contacts.js +34 -0
  41. package/dist/core/doc.d.ts +11 -0
  42. package/dist/core/doc.js +1 -0
  43. package/dist/core/feature.d.ts +3 -0
  44. package/dist/core/feature.js +4 -0
  45. package/dist/core/fixtures.d.ts +19 -0
  46. package/dist/core/fixtures.js +52 -0
  47. package/dist/core/frontmatter.d.ts +4 -0
  48. package/dist/core/frontmatter.js +15 -0
  49. package/dist/core/html.d.ts +2 -0
  50. package/dist/core/html.js +2 -0
  51. package/dist/core/image.d.ts +6 -0
  52. package/dist/core/image.js +34 -0
  53. package/dist/core/jev.d.ts +54 -0
  54. package/dist/core/jev.js +52 -0
  55. package/dist/core/review/model.d.ts +85 -0
  56. package/dist/core/review/model.js +112 -0
  57. package/dist/core/review/policy.d.ts +3 -0
  58. package/dist/core/review/policy.js +19 -0
  59. package/dist/core/review/service.d.ts +79 -0
  60. package/dist/core/review/service.js +245 -0
  61. package/dist/core/review/store.d.ts +20 -0
  62. package/dist/core/review/store.js +37 -0
  63. package/dist/core/review/triage.d.ts +6 -0
  64. package/dist/core/review/triage.js +45 -0
  65. package/dist/core/states.d.ts +6 -0
  66. package/dist/core/states.js +29 -0
  67. package/dist/scripts/bootstrap.d.ts +51 -0
  68. package/dist/scripts/bootstrap.js +278 -0
  69. package/dist/scripts/calibrate.d.ts +127 -0
  70. package/dist/scripts/calibrate.js +164 -0
  71. package/dist/scripts/check.d.ts +25 -0
  72. package/dist/scripts/check.js +30 -0
  73. package/dist/scripts/cli.d.ts +2 -0
  74. package/dist/scripts/cli.js +73 -0
  75. package/dist/scripts/commands.d.ts +25 -0
  76. package/dist/scripts/commands.js +49 -0
  77. package/dist/scripts/comments.d.ts +63 -0
  78. package/dist/scripts/comments.js +152 -0
  79. package/dist/scripts/dev.d.ts +46 -0
  80. package/dist/scripts/dev.js +84 -0
  81. package/dist/scripts/elyx.d.ts +61 -0
  82. package/dist/scripts/elyx.js +195 -0
  83. package/dist/scripts/fixtures-pull.d.ts +60 -0
  84. package/dist/scripts/fixtures-pull.js +121 -0
  85. package/dist/scripts/guard.d.ts +58 -0
  86. package/dist/scripts/guard.js +201 -0
  87. package/dist/scripts/hook.d.ts +18 -0
  88. package/dist/scripts/hook.js +46 -0
  89. package/dist/scripts/lib/bin.d.ts +4 -0
  90. package/dist/scripts/lib/bin.js +20 -0
  91. package/dist/scripts/lib/catalog.d.ts +7 -0
  92. package/dist/scripts/lib/catalog.js +18 -0
  93. package/dist/scripts/lib/cli.d.ts +75 -0
  94. package/dist/scripts/lib/cli.js +88 -0
  95. package/dist/scripts/lib/css-tokens.d.ts +13 -0
  96. package/dist/scripts/lib/css-tokens.js +67 -0
  97. package/dist/scripts/lib/judge.d.ts +24 -0
  98. package/dist/scripts/lib/judge.js +60 -0
  99. package/dist/scripts/lib/ports.d.ts +35 -0
  100. package/dist/scripts/lib/ports.js +113 -0
  101. package/dist/scripts/lib/review.d.ts +7 -0
  102. package/dist/scripts/lib/review.js +22 -0
  103. package/dist/scripts/lib/rules.d.ts +12 -0
  104. package/dist/scripts/lib/rules.js +20 -0
  105. package/dist/scripts/lib/vercel.d.ts +17 -0
  106. package/dist/scripts/lib/vercel.js +53 -0
  107. package/dist/scripts/new.d.ts +47 -0
  108. package/dist/scripts/new.js +113 -0
  109. package/dist/scripts/people.d.ts +59 -0
  110. package/dist/scripts/people.js +73 -0
  111. package/dist/scripts/preflight.d.ts +35 -0
  112. package/dist/scripts/preflight.js +251 -0
  113. package/dist/scripts/release-check.d.ts +19 -0
  114. package/dist/scripts/release-check.js +39 -0
  115. package/dist/scripts/skill-hint.d.ts +32 -0
  116. package/dist/scripts/skill-hint.js +103 -0
  117. package/dist/scripts/skills.d.ts +13 -0
  118. package/dist/scripts/skills.js +34 -0
  119. package/dist/scripts/visual-match.d.ts +46 -0
  120. package/dist/scripts/visual-match.js +93 -0
  121. package/dist/server/api.d.ts +22 -0
  122. package/dist/server/api.js +143 -0
  123. package/dist/server/checks.d.ts +4 -0
  124. package/dist/server/checks.js +22 -0
  125. package/dist/server/dev.d.ts +2 -0
  126. package/dist/server/dev.js +54 -0
  127. package/dist/server/env.d.ts +8 -0
  128. package/dist/server/env.js +57 -0
  129. package/dist/server/fake-upstash.d.ts +1 -0
  130. package/dist/server/fake-upstash.js +37 -0
  131. package/dist/server/file-kv.d.ts +2 -0
  132. package/dist/server/file-kv.js +24 -0
  133. package/dist/server/local.d.ts +4 -0
  134. package/dist/server/local.js +25 -0
  135. package/dist/server/paths.d.ts +22 -0
  136. package/dist/server/paths.js +81 -0
  137. package/dist/server/session.d.ts +22 -0
  138. package/dist/server/session.js +110 -0
  139. package/dist/server/upstash.d.ts +7 -0
  140. package/dist/server/upstash.js +41 -0
  141. package/dist/vercel/index.d.ts +5 -0
  142. package/dist/vercel/index.js +12 -0
  143. package/dist/vercel/proxy.d.ts +11 -0
  144. package/dist/vercel/proxy.js +238 -0
  145. package/dist/vite/kit.d.ts +4 -0
  146. package/dist/vite/kit.js +35 -0
  147. package/dist/vite/markdown.d.ts +2 -0
  148. package/dist/vite/markdown.js +212 -0
  149. package/dist/vite/plugin.d.ts +15 -0
  150. package/dist/vite/plugin.js +84 -0
  151. package/dist/vite/registry.d.ts +7 -0
  152. package/dist/vite/registry.js +70 -0
  153. package/kit/Icon.tsx +39 -0
  154. package/kit/Space.tsx +25 -0
  155. package/kit/Variants.tsx +134 -0
  156. package/kit/ViewTools.tsx +34 -0
  157. package/kit/base.css +27 -0
  158. package/kit/dials.css +98 -0
  159. package/kit/dials.tsx +322 -0
  160. package/kit/fixtures.ts +3 -0
  161. package/kit/format.ts +13 -0
  162. package/kit/harness.css +99 -0
  163. package/kit/hooks.ts +35 -0
  164. package/kit/icons.ts +329 -0
  165. package/kit/motion.ts +18 -0
  166. package/kit/prefs.ts +111 -0
  167. package/kit/space.css +12 -0
  168. package/kit/ui/Button.tsx +135 -0
  169. package/kit/ui/Menu.tsx +386 -0
  170. package/kit/ui/Overlay.tsx +300 -0
  171. package/kit/ui/Toast.tsx +187 -0
  172. package/kit/ui/index.ts +13 -0
  173. package/kit/ui/layer.ts +51 -0
  174. package/kit/ui/ui.css +238 -0
  175. package/package.json +87 -3
  176. package/shell/Guide.tsx +160 -0
  177. package/shell/Index.tsx +301 -0
  178. package/shell/Notice.tsx +27 -0
  179. package/shell/Prototype.tsx +31 -0
  180. package/shell/a11y.ts +50 -0
  181. package/shell/doc.d.ts +7 -0
  182. package/shell/doc.ts +3 -0
  183. package/shell/guide.css +133 -0
  184. package/shell/index.html +17 -0
  185. package/shell/main.tsx +53 -0
  186. package/shell/public/favicon.svg +1 -0
  187. package/shell/registry.ts +21 -0
  188. package/shell/registry.virtual.d.ts +10 -0
  189. package/shell/review/Review.tsx +989 -0
  190. package/shell/review/anchor.ts +132 -0
  191. package/shell/review/client.ts +165 -0
  192. package/shell/review/mount.tsx +12 -0
  193. package/shell/review/review.css +296 -0
  194. package/shell/shell.css +160 -0
  195. package/shell/system.css +238 -0
  196. package/skills/miles-conductor/SKILL.md +37 -0
  197. package/skills/miles-deploy/SKILL.md +67 -0
  198. package/skills/miles-design-hub/SKILL.md +62 -0
  199. package/skills/miles-get-hostile/SKILL.md +61 -0
  200. package/skills/miles-new-feature/SKILL.md +40 -0
  201. package/skills/miles-paper-to-prototype/SKILL.md +46 -0
  202. package/skills/miles-port/SKILL.md +59 -0
  203. package/skills/miles-port/references/astro.md +35 -0
  204. package/skills/miles-port/references/next.md +36 -0
  205. package/skills/miles-prototype/SKILL.md +83 -0
  206. package/skills/miles-prototype/references/accessibility.md +48 -0
  207. package/skills/miles-prototype/references/motion.md +59 -0
  208. package/skills/miles-prototype/references/performance.md +45 -0
  209. package/skills/miles-review/SKILL.md +39 -0
  210. package/templates/app/README.md +41 -0
  211. package/templates/app/index.tsx +32 -0
  212. package/templates/app/styles.css +1 -0
  213. package/templates/conductor/settings.toml +29 -0
  214. package/templates/guide.md +38 -0
  215. package/templates/space/README.md +37 -0
  216. package/templates/space/index.tsx +43 -0
  217. package/templates/space/styles.css +13 -0
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ // `design-hub hook <name>`: the Claude Code hooks a project's
4
+ // .claude/settings.json runs. Each reads the hook's JSON on stdin and fails
5
+ // open: any error exits 0 with nothing said, so a broken hook can never be
6
+ // the reason the user is stuck.
7
+ import { readFileSync } from 'node:fs';
8
+ import { basename } from 'node:path';
9
+ import { guardHook } from './guard.js';
10
+ import { skillHintHook } from './skill-hint.js';
11
+ const MD = /\.md$/i;
12
+ /** PostToolUse on a Markdown file: ask for the writing pass before the change is reported done. */
13
+ export function mdWriting(input) {
14
+ const file = input.tool_input?.file_path ?? input.tool_response?.filePath ?? '';
15
+ if (!MD.test(file) || file.includes('/node_modules/'))
16
+ return null;
17
+ return {
18
+ systemMessage: `Markdown changed: ${basename(file)}. Run /emil-writing-skills on it before reporting done.`,
19
+ hookSpecificOutput: {
20
+ hookEventName: 'PostToolUse',
21
+ additionalContext: `You changed the Markdown file ${file}. Before you report this work done, invoke the emil-writing-skills skill (Skill tool, skill: emil-writing-skills) and apply its checklist to that file: every line changes what a reader or agent does, no hedge words, rules carry their why, nothing stale against the current code and decisions, dates and paths it cites still exist. Fix what fails, then say in your reply that the pass ran.`,
22
+ },
23
+ };
24
+ }
25
+ export const HOOKS = ['fidelity-guard', 'md-writing', 'skill-hint'];
26
+ export async function runHook(name) {
27
+ try {
28
+ switch (name) {
29
+ case 'fidelity-guard':
30
+ return await guardHook();
31
+ case 'skill-hint':
32
+ return await skillHintHook();
33
+ case 'md-writing': {
34
+ const output = mdWriting(JSON.parse(readFileSync(0, 'utf8')));
35
+ if (output)
36
+ console.log(JSON.stringify(output));
37
+ return;
38
+ }
39
+ default:
40
+ return;
41
+ }
42
+ }
43
+ catch {
44
+ // Fail open.
45
+ }
46
+ }
@@ -0,0 +1,4 @@
1
+ /** The folder of an installed package, searched the way Node searches, so an exports map never hides it. */
2
+ export declare function packageDir(name: string): string;
3
+ export declare const tscBin: () => string;
4
+ export declare const biomeBin: () => string;
@@ -0,0 +1,20 @@
1
+ /// <reference types="node" />
2
+ // The executables this package brings a project (tsc, biome), found from
3
+ // where Node resolves the package, so a project installs neither and pnpm's
4
+ // layout never matters. A package's exports map may hide its bin and its
5
+ // package.json, so the folder is found by walking up from its entry point.
6
+ import { existsSync } from 'node:fs';
7
+ import { createRequire } from 'node:module';
8
+ import { join } from 'node:path';
9
+ const require = createRequire(import.meta.url);
10
+ /** The folder of an installed package, searched the way Node searches, so an exports map never hides it. */
11
+ export function packageDir(name) {
12
+ for (const dir of require.resolve.paths(name) ?? []) {
13
+ const candidate = join(dir, name);
14
+ if (existsSync(join(candidate, 'package.json')))
15
+ return candidate;
16
+ }
17
+ throw new Error(`${name} is not installed beside design-hub.`);
18
+ }
19
+ export const tscBin = () => join(packageDir('typescript'), 'bin', 'tsc');
20
+ export const biomeBin = () => join(packageDir('@biomejs/biome'), 'bin', 'biome');
@@ -0,0 +1,7 @@
1
+ import { type Meta } from '../../core/catalog.ts';
2
+ export type OnDisk = Meta & {
3
+ dir: string;
4
+ };
5
+ export declare function prototypes(root?: string): OnDisk[];
6
+ /** The feature's live primary prototype, if it has one. */
7
+ export declare const primaryOf: (feature: string, all?: OnDisk[]) => OnDisk | undefined;
@@ -0,0 +1,18 @@
1
+ /// <reference types="node" />
2
+ // The prototypes on disk, read the way the browser index reads them (vite/registry.ts).
3
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { byIndexOrder, isLive, readMeta } from '../../core/catalog.js';
6
+ import { PROTOTYPES } from '../../server/paths.js';
7
+ export function prototypes(root = PROTOTYPES) {
8
+ return readdirSync(root, { withFileTypes: true })
9
+ .filter((d) => d.isDirectory() && !d.name.startsWith('_') && !d.name.startsWith('.') && d.name !== 'node_modules')
10
+ .filter((d) => existsSync(join(root, d.name, 'README.md')))
11
+ .map((d) => ({
12
+ ...readMeta(d.name, readFileSync(join(root, d.name, 'README.md'), 'utf8')),
13
+ dir: join(root, d.name),
14
+ }))
15
+ .sort(byIndexOrder);
16
+ }
17
+ /** The feature's live primary prototype, if it has one. */
18
+ export const primaryOf = (feature, all = prototypes()) => all.find((p) => p.kind === 'sprint' && isLive(p) && p.feature === feature);
@@ -0,0 +1,75 @@
1
+ export type Flag = {
2
+ type: 'string';
3
+ description: string;
4
+ required?: boolean;
5
+ default?: string;
6
+ choices?: readonly string[];
7
+ } | {
8
+ type: 'number';
9
+ description: string;
10
+ required?: boolean;
11
+ default?: number;
12
+ } | {
13
+ type: 'boolean';
14
+ description: string;
15
+ } | {
16
+ type: 'list';
17
+ description: string;
18
+ };
19
+ export type CommandSpec = {
20
+ name: string;
21
+ summary: string;
22
+ flags: Record<string, Flag>;
23
+ positionals?: {
24
+ name: string;
25
+ description: string;
26
+ required?: boolean;
27
+ variadic?: boolean;
28
+ }[];
29
+ output: string;
30
+ };
31
+ type ValueOf<F extends Flag> = F extends {
32
+ type: 'number';
33
+ } ? number : F extends {
34
+ type: 'boolean';
35
+ } ? boolean : F extends {
36
+ type: 'list';
37
+ } ? string[] : string;
38
+ type Defined<F extends Flag> = F extends {
39
+ type: 'boolean' | 'list';
40
+ } ? true : F extends {
41
+ required: true;
42
+ } ? true : F extends {
43
+ default: unknown;
44
+ } ? true : false;
45
+ export type Args<S extends CommandSpec> = {
46
+ flags: {
47
+ [K in keyof S['flags']]: Defined<S['flags'][K]> extends true ? ValueOf<S['flags'][K]> : ValueOf<S['flags'][K]> | undefined;
48
+ };
49
+ positionals: string[];
50
+ };
51
+ export type Result = Record<string, unknown> & {
52
+ ok?: boolean;
53
+ };
54
+ /** A failure the caller can act on: the message, and the fix when there is one. */
55
+ export declare class CliError extends Error {
56
+ detail: Record<string, unknown>;
57
+ constructor(message: string, detail?: Record<string, unknown>);
58
+ }
59
+ export declare function schema(spec: CommandSpec): {
60
+ command: string;
61
+ summary: string;
62
+ positionals: {
63
+ name: string;
64
+ description: string;
65
+ required?: boolean;
66
+ variadic?: boolean;
67
+ }[];
68
+ flags: {
69
+ [k: string]: Flag;
70
+ };
71
+ output: string;
72
+ };
73
+ /** Parses argv, runs the command, prints its JSON and sets the exit code. */
74
+ export declare function runCommand<S extends CommandSpec>(spec: S, main: (args: Args<S>) => Result | Promise<Result>, argv?: string[]): Promise<void>;
75
+ export {};
@@ -0,0 +1,88 @@
1
+ /// <reference types="node" />
2
+ // The contract every design-hub command keeps, because its reader is an
3
+ // agent, not a person: every input is a flag, nothing prompts, stdout is one
4
+ // JSON object, `--help` prints the command's schema as JSON, and a failure is
5
+ // `{ ok: false, error, fix? }` with exit code 1. `design-hub commands` lists every
6
+ // schema, so an agent discovers the commands without reading their source.
7
+ import { parseArgs } from 'node:util';
8
+ /** A failure the caller can act on: the message, and the fix when there is one. */
9
+ export class CliError extends Error {
10
+ detail;
11
+ constructor(message, detail = {}) {
12
+ super(message);
13
+ this.detail = detail;
14
+ }
15
+ }
16
+ export function schema(spec) {
17
+ return {
18
+ command: `design-hub ${spec.name}`,
19
+ summary: spec.summary,
20
+ positionals: spec.positionals ?? [],
21
+ flags: Object.fromEntries(Object.entries(spec.flags).map(([name, flag]) => [`--${name}`, flag])),
22
+ output: spec.output,
23
+ };
24
+ }
25
+ /** One flag's value, typed by its spec; a missing optional flag takes its default. */
26
+ function flagValue(name, f, raw) {
27
+ if (f.type === 'boolean')
28
+ return raw === true;
29
+ if (f.type === 'list')
30
+ return raw ?? [];
31
+ if (raw === undefined) {
32
+ if (f.required)
33
+ throw new CliError(`--${name} is required.`, { fix: f.description });
34
+ return f.default;
35
+ }
36
+ if (f.type === 'number') {
37
+ const n = Number(raw);
38
+ if (!Number.isFinite(n))
39
+ throw new CliError(`--${name} takes a number.`, { got: raw });
40
+ return n;
41
+ }
42
+ if (f.choices && !f.choices.includes(raw))
43
+ throw new CliError(`--${name} takes one of: ${f.choices.join(', ')}.`, { got: raw });
44
+ return raw;
45
+ }
46
+ function parse(spec, argv) {
47
+ const options = Object.fromEntries(Object.entries(spec.flags).map(([name, f]) => [
48
+ name,
49
+ { type: f.type === 'boolean' ? 'boolean' : 'string', multiple: f.type === 'list' },
50
+ ]));
51
+ const help = { fix: `Run design-hub ${spec.name} --help for the flags and arguments.` };
52
+ let parsed;
53
+ try {
54
+ parsed = parseArgs({ args: argv, options, allowPositionals: true, strict: true });
55
+ }
56
+ catch (error) {
57
+ throw new CliError(error instanceof Error ? error.message : String(error), help);
58
+ }
59
+ const flags = Object.fromEntries(Object.entries(spec.flags).map(([name, f]) => [name, flagValue(name, f, parsed.values[name])]));
60
+ const required = (spec.positionals ?? []).filter((p) => p.required).length;
61
+ if (parsed.positionals.length < required)
62
+ throw new CliError(`Missing ${spec.positionals?.[parsed.positionals.length]?.name}.`, help);
63
+ // flagValue gave every flag the type its spec declares, which is what Args<S> describes.
64
+ return { flags, positionals: parsed.positionals };
65
+ }
66
+ const print = (value) => console.log(JSON.stringify(value, null, 2));
67
+ /** Parses argv, runs the command, prints its JSON and sets the exit code. */
68
+ export async function runCommand(spec, main, argv = process.argv.slice(2)) {
69
+ if (argv.includes('--help') || argv.includes('-h'))
70
+ return print(schema(spec));
71
+ try {
72
+ const { ok = true, ...rest } = await main(parse(spec, argv));
73
+ print({ ok, ...rest });
74
+ if (!ok)
75
+ process.exitCode = 1;
76
+ }
77
+ catch (error) {
78
+ // A domain error (the review service's) carries its problems by path; print them the same way.
79
+ const problems = error?.problems;
80
+ const detail = error instanceof CliError
81
+ ? error.detail
82
+ : Array.isArray(problems) && problems.length
83
+ ? { problems }
84
+ : {};
85
+ print({ ok: false, error: error instanceof Error ? error.message : String(error), ...detail });
86
+ process.exitCode = 1;
87
+ }
88
+ }
@@ -0,0 +1,13 @@
1
+ export type TokenKind = 'color' | 'radius' | 'length' | 'font';
2
+ export type Classified = {
3
+ kind: TokenKind;
4
+ value: string;
5
+ } | {
6
+ kind: 'skip';
7
+ why: string;
8
+ };
9
+ /** The custom properties declared in one selector's block, comments stripped. */
10
+ export declare function block(css: string, selector: string): Map<string, string>;
11
+ /** #rgb, #rrggbbaa, rgb() and rgba() as lowercase #rrggbb or #rrggbbaa; null for anything else. */
12
+ export declare const toHex: (value: string) => string | null;
13
+ export declare function classify(name: string, value: string): Classified;
@@ -0,0 +1,67 @@
1
+ // kit/tokens.css read as data: the light values on :root and the dark
2
+ // overrides on .dark-mode, each classified by what an Elyx token can hold.
3
+ // Pure, so the Elyx generator's mapping is tested without the Elyx CLI.
4
+ // The harness is the prototype's own voice, never the product's, so it never enters the design system.
5
+ const EXCLUDED = /^harness-/;
6
+ /** The custom properties declared in one selector's block, comments stripped. */
7
+ export function block(css, selector) {
8
+ const clean = css.replace(/\/\*[\s\S]*?\*\//g, '');
9
+ const at = clean.indexOf(`${selector} {`);
10
+ if (at < 0)
11
+ throw new Error(`tokens.css has no ${selector} block.`);
12
+ const body = clean.slice(clean.indexOf('{', at) + 1, clean.indexOf('}', at));
13
+ const out = new Map();
14
+ for (const decl of body.split(';')) {
15
+ const m = decl.match(/^\s*--([A-Za-z_-][\w-]*)\s*:\s*([\s\S]+?)\s*$/);
16
+ if (m)
17
+ out.set(m[1], m[2].replace(/\s+/g, ' '));
18
+ }
19
+ return out;
20
+ }
21
+ const byte = (n) => Math.round(Math.min(255, Math.max(0, n)))
22
+ .toString(16)
23
+ .padStart(2, '0');
24
+ function hexColor(value) {
25
+ const m = value.match(/^#([0-9a-f]{3,8})$/i);
26
+ if (!m)
27
+ return null;
28
+ const short = m[1].length === 3 || m[1].length === 4;
29
+ const h = short ? [...m[1].toLowerCase()].map((c) => c + c).join('') : m[1].toLowerCase();
30
+ if (h.length !== 6 && h.length !== 8)
31
+ return null;
32
+ return `#${h.length === 8 && h.endsWith('ff') ? h.slice(0, 6) : h}`;
33
+ }
34
+ const channel = (p) => (p.endsWith('%') ? (Number.parseFloat(p) * 255) / 100 : Number.parseFloat(p));
35
+ const alphaOf = (p) => p === undefined ? 1 : p.endsWith('%') ? Number.parseFloat(p) / 100 : Number.parseFloat(p);
36
+ function rgbColor(value) {
37
+ const m = value.match(/^rgba?\(\s*([^)]+)\)$/i);
38
+ const parts = m ? m[1].split(/\s*[,/]\s*|\s+/).filter(Boolean) : [];
39
+ if (parts.length < 3 || parts.length > 4)
40
+ return null;
41
+ const rgb = parts.slice(0, 3).map(channel);
42
+ const alpha = alphaOf(parts[3]);
43
+ if (![...rgb, alpha].every(Number.isFinite))
44
+ return null;
45
+ return `#${rgb.map(byte).join('')}${alpha >= 1 ? '' : byte(alpha * 255)}`;
46
+ }
47
+ /** #rgb, #rrggbbaa, rgb() and rgba() as lowercase #rrggbb or #rrggbbaa; null for anything else. */
48
+ export const toHex = (value) => hexColor(value) ?? rgbColor(value);
49
+ export function classify(name, value) {
50
+ if (EXCLUDED.test(name))
51
+ return { kind: 'skip', why: 'harness material, not the design' };
52
+ const color = toHex(value);
53
+ if (color)
54
+ return { kind: 'color', value: color };
55
+ const px = value.match(/^(-?\d+(?:\.\d+)?)px$/);
56
+ if (px)
57
+ return { kind: name.startsWith('radius-') ? 'radius' : 'length', value: px[1] };
58
+ if (/^font(-|$)/.test(name)) {
59
+ // Elyx binds one family; the first in the stack is the design's, the rest are fallbacks.
60
+ const first = value
61
+ .split(',')[0]
62
+ .trim()
63
+ .replace(/^["']|["']$/g, '');
64
+ return { kind: 'font', value: JSON.stringify(first) };
65
+ }
66
+ return { kind: 'skip', why: 'no Elyx token type (a shadow, ring, easing, duration or keyword)' };
67
+ }
@@ -0,0 +1,24 @@
1
+ import { type Answer, type ImageAsset, type Question } from '../../core/jev.ts';
2
+ export declare const JEV_LOG: string;
3
+ export declare const LABELS: string;
4
+ /** The checks that ask Jev; calibration groups judgments by these. */
5
+ type CheckName = 'fidelity' | 'states' | 'contacts' | 'skillHint' | 'triage' | 'visual';
6
+ export type Judged = {
7
+ answers: Record<string, Answer>;
8
+ ids: Record<string, string>;
9
+ };
10
+ export declare const typesafeKey: () => string;
11
+ export declare function appendJsonl(file: string, entry: Record<string, unknown>): void;
12
+ type Ask = {
13
+ check: CheckName;
14
+ /** What was judged, for the reader of the log: a file, a feature, a prompt. */
15
+ subject: string;
16
+ state: unknown;
17
+ questions: Record<string, Question>;
18
+ assets?: Record<string, ImageAsset>;
19
+ /** Kept in the log so a person labelling the judgment can see what was judged. */
20
+ excerpt?: string;
21
+ };
22
+ /** The answers and one log id per question, or null when Jev is off or failed. */
23
+ export declare function judge({ check, subject, state, questions, assets, excerpt, }: Ask): Promise<Judged | null>;
24
+ export {};
@@ -0,0 +1,60 @@
1
+ /// <reference types="node" />
2
+ // Jev from the Node scripts: the key from .env, the model and timeout from
3
+ // checks.json, and every judgment appended to .design-hub/jev.jsonl with an
4
+ // id, so `design-hub calibrate` can label it right or wrong and thresholds come
5
+ // from data. Off without TYPESAFE_API_KEY, silently: the user chose that.
6
+ import { randomBytes } from 'node:crypto';
7
+ import { appendFileSync, mkdirSync } from 'node:fs';
8
+ import { dirname, join } from 'node:path';
9
+ import { askJev } from '../../core/jev.js';
10
+ import { checks } from '../../server/checks.js';
11
+ import { readEnv } from '../../server/env.js';
12
+ import { ROOT, STATE_DIR } from '../../server/paths.js';
13
+ export const JEV_LOG = join(STATE_DIR, 'jev.jsonl');
14
+ export const LABELS = join(STATE_DIR, 'labels.jsonl');
15
+ // The project's .env only, never the shell: a key in someone's profile must not turn
16
+ // TypeSafe on in a project whose owner did not opt in (design-hub bootstrap --typesafe-key).
17
+ export const typesafeKey = () => readEnv(ROOT).TYPESAFE_API_KEY ?? '';
18
+ export function appendJsonl(file, entry) {
19
+ try {
20
+ mkdirSync(dirname(file), { recursive: true });
21
+ appendFileSync(file, `${JSON.stringify(entry)}\n`);
22
+ }
23
+ catch {
24
+ // A log that cannot be written never stops the check it records.
25
+ }
26
+ }
27
+ /** The answers and one log id per question, or null when Jev is off or failed. */
28
+ export async function judge({ check, subject, state, questions, assets, excerpt, }) {
29
+ const key = typesafeKey();
30
+ if (!key || !checks)
31
+ return null;
32
+ const result = await askJev(key, { model: checks.jev.model, state, questions, assets }, {
33
+ timeoutMs: checks.jev.timeoutMs,
34
+ });
35
+ const at = new Date().toISOString();
36
+ if (!result.ok) {
37
+ appendJsonl(JEV_LOG, { at, check, subject, error: result.error, ms: result.ms });
38
+ return null;
39
+ }
40
+ const ids = {};
41
+ for (const [question, answer] of Object.entries(result.answers)) {
42
+ const id = randomBytes(4).toString('hex');
43
+ ids[question] = id;
44
+ const value = answer.type === 'noul'
45
+ ? { p: answer.noul }
46
+ : { choice: answer.choice, confidence: answer.confidence, probabilities: answer.probabilities };
47
+ appendJsonl(JEV_LOG, {
48
+ id,
49
+ at,
50
+ check,
51
+ question,
52
+ subject,
53
+ model: result.model,
54
+ ms: result.ms,
55
+ ...value,
56
+ excerpt,
57
+ });
58
+ }
59
+ return { answers: result.answers, ids };
60
+ }
@@ -0,0 +1,35 @@
1
+ /** Project folder (absolute) to its port. Claims for folders that no longer exist are dropped. */
2
+ export declare function readClaims(file?: string): Record<string, number>;
3
+ /** Records the claim; written to a temp file and renamed, so a crash never leaves half a registry. */
4
+ export declare function writeClaim(dir: string, port: number, file?: string): void;
5
+ export type Holder = {
6
+ pid: number;
7
+ command: string;
8
+ cwd: string;
9
+ };
10
+ /** The process listening on the port, with its working directory, or null. */
11
+ export declare function holderOf(port: number): Holder | null;
12
+ export type PortChoice = {
13
+ ok: true;
14
+ port: number;
15
+ warnings: string[];
16
+ } | {
17
+ ok: false;
18
+ error: string;
19
+ detail: Record<string, unknown>;
20
+ };
21
+ type PickInput = {
22
+ dir: string;
23
+ wanted?: number;
24
+ current?: number;
25
+ claims: Record<string, number>;
26
+ holder: (port: number) => Holder | null;
27
+ range?: readonly [number, number];
28
+ };
29
+ /**
30
+ * The port for `dir`: the one asked for, else the one it already has, else
31
+ * the first in range nobody holds or claims. Pure apart from `holder`, so the
32
+ * rules are tested without opening sockets.
33
+ */
34
+ export declare function pickPort(input: PickInput): PortChoice;
35
+ export {};
@@ -0,0 +1,113 @@
1
+ /// <reference types="node" />
2
+ // One dev port per project on this machine. A live `lsof` check alone is not
3
+ // enough: a project whose server is stopped today still owns its port, and a
4
+ // second project handed the same one serves the wrong site the day both run.
5
+ // So every project's claim is written to one registry outside any repo, and a
6
+ // port is free only when nothing holds it and no other project claims it.
7
+ import { spawnSync } from 'node:child_process';
8
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { dirname, join } from 'node:path';
11
+ // Off the 3000s that Next, Vite examples and most templates default to, so a
12
+ // project that never ran bootstrap is unlikely to sit on a hub port.
13
+ const PORT_RANGE = [3100, 3999];
14
+ /** `$DESIGN_HUB_PORTS`, else `$XDG_CONFIG_HOME/design-hub/ports.json`, else `~/.config/...`. */
15
+ function registryFile() {
16
+ if (process.env.DESIGN_HUB_PORTS)
17
+ return process.env.DESIGN_HUB_PORTS;
18
+ const config = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
19
+ return join(config, 'design-hub', 'ports.json');
20
+ }
21
+ /** Project folder (absolute) to its port. Claims for folders that no longer exist are dropped. */
22
+ export function readClaims(file = registryFile()) {
23
+ if (!existsSync(file))
24
+ return {};
25
+ let raw;
26
+ try {
27
+ raw = JSON.parse(readFileSync(file, 'utf8'));
28
+ }
29
+ catch {
30
+ // Read as empty, the next claim would overwrite every other project's; stop instead.
31
+ throw new Error(`${file} is not valid JSON. Fix it, or delete it and run pnpm bootstrap in each project again.`);
32
+ }
33
+ return Object.fromEntries(Object.entries(raw.claims ?? {}).filter((entry) => Number.isInteger(entry[1]) && existsSync(entry[0])));
34
+ }
35
+ /** Records the claim; written to a temp file and renamed, so a crash never leaves half a registry. */
36
+ export function writeClaim(dir, port, file = registryFile()) {
37
+ const claims = { ...readClaims(file), [dir]: port };
38
+ mkdirSync(dirname(file), { recursive: true });
39
+ const tmp = `${file}.${process.pid}.tmp`;
40
+ writeFileSync(tmp, `${JSON.stringify({ claims }, null, 2)}\n`);
41
+ renameSync(tmp, file);
42
+ }
43
+ /** The process listening on the port, with its working directory, or null. */
44
+ export function holderOf(port) {
45
+ const out = spawnSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpc'], { encoding: 'utf8' });
46
+ if (out.status !== 0 || !out.stdout)
47
+ return null;
48
+ const pid = Number(out.stdout.match(/^p(\d+)/m)?.[1]);
49
+ const command = out.stdout.match(/^c(.+)/m)?.[1] ?? '';
50
+ const cwd = spawnSync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8' }).stdout.match(/^n(.+)/m)?.[1] ?? '';
51
+ return { pid, command, cwd };
52
+ }
53
+ function owners({ dir, claims, holder }) {
54
+ return {
55
+ claimant: (port) => Object.entries(claims).find(([d, p]) => p === port && d !== dir)?.[0] ?? null,
56
+ foreign: (port) => {
57
+ const held = holder(port);
58
+ return held && held.cwd !== dir ? held : null;
59
+ },
60
+ };
61
+ }
62
+ /** A port asked for by name: refused, naming the owner, rather than swapped for another. */
63
+ function checkWanted(wanted, { claimant, foreign }) {
64
+ if (!Number.isInteger(wanted) || wanted < 1024 || wanted > 65535)
65
+ return { ok: false, error: '--port takes an integer from 1024 to 65535.', detail: { got: wanted } };
66
+ const owner = claimant(wanted);
67
+ if (owner)
68
+ return {
69
+ ok: false,
70
+ error: `Port ${wanted} is claimed by another project.`,
71
+ detail: { port: wanted, claimedBy: owner, fix: 'Pass another --port.' },
72
+ };
73
+ const held = foreign(wanted);
74
+ if (held)
75
+ return {
76
+ ok: false,
77
+ error: `Port ${wanted} is taken.`,
78
+ detail: { port: wanted, holder: held, fix: 'Pass another --port, or stop that server.' },
79
+ };
80
+ return { ok: true, port: wanted, warnings: [] };
81
+ }
82
+ /** Why the port in .env cannot stay, or null when it can. */
83
+ function currentConflict(current, { claimant, foreign }) {
84
+ const owner = claimant(current);
85
+ if (owner)
86
+ return `Port ${current} from .env is claimed by ${owner}; picked a new one.`;
87
+ const held = foreign(current);
88
+ return held
89
+ ? `Port ${current} from .env is held by pid ${held.pid} (${held.command}) in ${held.cwd}; picked a new one.`
90
+ : null;
91
+ }
92
+ /**
93
+ * The port for `dir`: the one asked for, else the one it already has, else
94
+ * the first in range nobody holds or claims. Pure apart from `holder`, so the
95
+ * rules are tested without opening sockets.
96
+ */
97
+ export function pickPort(input) {
98
+ const own = owners(input);
99
+ if (input.wanted !== undefined)
100
+ return checkWanted(input.wanted, own);
101
+ const warnings = [];
102
+ if (input.current !== undefined) {
103
+ const conflict = currentConflict(input.current, own);
104
+ if (!conflict)
105
+ return { ok: true, port: input.current, warnings };
106
+ warnings.push(conflict);
107
+ }
108
+ const [from, to] = input.range ?? PORT_RANGE;
109
+ for (let port = from; port <= to; port++)
110
+ if (!own.claimant(port) && !own.foreign(port))
111
+ return { ok: true, port, warnings };
112
+ return { ok: false, error: `No free port between ${from} and ${to}.`, detail: {} };
113
+ }
@@ -0,0 +1,7 @@
1
+ import { type Review } from '../../core/review/service.ts';
2
+ export type CliReview = {
3
+ review: Review;
4
+ store: 'deployed' | 'local';
5
+ };
6
+ export declare function cliReview(local: boolean): CliReview;
7
+ export declare const cliActor: () => import("../../core/review/model.ts").Person;
@@ -0,0 +1,22 @@
1
+ /// <reference types="node" />
2
+ // The review service from the command line: the deployed store when its
3
+ // credentials are in .env (`design-hub bootstrap --review` puts them there), else
4
+ // the local one the dev server uses. The CLI acts as an editor, because
5
+ // whoever holds the store's credentials can already do anything to it.
6
+ import { createReview } from '../../core/review/service.js';
7
+ import { jevTriager } from '../../core/review/triage.js';
8
+ import { checks } from '../../server/checks.js';
9
+ import { readEnv } from '../../server/env.js';
10
+ import { fileKV } from '../../server/file-kv.js';
11
+ import { LOCAL_STORE, localEditor } from '../../server/local.js';
12
+ import { ROOT } from '../../server/paths.js';
13
+ import { storeConfig, upstashKV } from '../../server/upstash.js';
14
+ export function cliReview(local) {
15
+ const env = readEnv(ROOT);
16
+ const config = local ? null : storeConfig(env);
17
+ const triage = jevTriager(env.TYPESAFE_API_KEY ?? '', checks);
18
+ return config
19
+ ? { review: createReview(upstashKV(config), { triage }), store: 'deployed' }
20
+ : { review: createReview(fileKV(LOCAL_STORE), { triage }), store: 'local' };
21
+ }
22
+ export const cliActor = () => localEditor('cli', 'CLI');
@@ -0,0 +1,12 @@
1
+ export declare const RULE: {
2
+ readonly gravity: 'Low gravity';
3
+ readonly fork: "Fork, don't flex";
4
+ readonly dependency: 'No new dependency without the user';
5
+ readonly fetching: 'Fixtures, never a backend';
6
+ readonly abstraction: 'No abstraction for later';
7
+ readonly handling: 'No handling a fixture cannot reach';
8
+ readonly state: 'Every state is in the README';
9
+ };
10
+ export type SemanticRule = 'fetching' | 'abstraction' | 'handling' | 'state';
11
+ /** Each question reads `change`, `readmeStates` and `fixtureSets` from the state by path. */
12
+ export declare const NOULS: Record<SemanticRule, string>;
@@ -0,0 +1,20 @@
1
+ // The fidelity ceiling's rules, named exactly as the headings in
2
+ // .claude/skills/miles-prototype/SKILL.md, and the Noul each semantic rule
3
+ // asks. The guard asks all four on a write; the pre-flight asks the states
4
+ // one over each sprint prototype before a share link goes out.
5
+ export const RULE = {
6
+ gravity: 'Low gravity',
7
+ fork: "Fork, don't flex",
8
+ dependency: 'No new dependency without the user',
9
+ fetching: 'Fixtures, never a backend',
10
+ abstraction: 'No abstraction for later',
11
+ handling: 'No handling a fixture cannot reach',
12
+ state: 'Every state is in the README',
13
+ };
14
+ /** Each question reads `change`, `readmeStates` and `fixtureSets` from the state by path. */
15
+ export const NOULS = {
16
+ fetching: 'Does the code in `change` introduce data fetching, persistence or a backend call: fetch, XMLHttpRequest, WebSocket, IndexedDB, a server module, or localStorage used as a database rather than as a prototype preference?',
17
+ abstraction: 'Does the code in `change` introduce an abstraction (a custom hook, a React context, a generic or higher-order component, a utility module) that only one place uses?',
18
+ handling: 'Does the code in `change` add error handling, loading state or input validation for a case that none of the fixture sets in `fixtureSets` would exercise?',
19
+ state: 'Does the code in `change` render a user-visible UI state that is not listed in `readmeStates`?',
20
+ };