@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,17 @@
1
+ export declare const ENVIRONMENTS: readonly ['production', 'preview'];
2
+ export type Target = (typeof ENVIRONMENTS)[number] | 'development';
3
+ /** What the Upstash for Redis integration injects (Vercel Marketplace). */
4
+ export declare const STORE_ENV: readonly ['KV_REST_API_URL', 'KV_REST_API_TOKEN'];
5
+ export type Run = {
6
+ status: number;
7
+ stdout: string;
8
+ stderr: string;
9
+ };
10
+ export declare function vercel(cwd: string, args: string[], input?: string): Run | null;
11
+ export declare function linkedProject(cwd: string): string | null;
12
+ /** The variable names set for a target, read from `vercel env ls`'s table. */
13
+ export declare function envNames(cwd: string, target: Target): Set<string> | null;
14
+ /** Sets one variable for one target; `force` replaces a value that is already there. */
15
+ export declare function setEnv(cwd: string, name: string, target: Target, value: string, force: boolean): Run | null;
16
+ /** The gate variables missing from production or preview, or null when the CLI cannot say. */
17
+ export declare function gateEnvMissing(cwd: string): string[] | null;
@@ -0,0 +1,53 @@
1
+ /// <reference types="node" />
2
+ // The Vercel CLI calls the scripts make, in one place. Each returns null when
3
+ // the CLI is missing or signed out, so a caller decides whether that is a
4
+ // failure (bootstrap --vercel) or a skipped check (preflight).
5
+ import { spawnSync } from 'node:child_process';
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ export const ENVIRONMENTS = ['production', 'preview'];
9
+ const GATE_ENV = ['GATE_PASSWORD', 'GATE_SECRET'];
10
+ /** What the Upstash for Redis integration injects (Vercel Marketplace). */
11
+ export const STORE_ENV = ['KV_REST_API_URL', 'KV_REST_API_TOKEN'];
12
+ export function vercel(cwd, args, input) {
13
+ const out = spawnSync('vercel', args, { cwd, encoding: 'utf8', input, env: process.env });
14
+ if (out.error)
15
+ return null;
16
+ return { status: out.status ?? 1, stdout: out.stdout ?? '', stderr: out.stderr ?? '' };
17
+ }
18
+ export function linkedProject(cwd) {
19
+ const file = join(cwd, '.vercel', 'project.json');
20
+ if (!existsSync(file))
21
+ return null;
22
+ try {
23
+ return JSON.parse(readFileSync(file, 'utf8')).projectName ?? null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /** The variable names set for a target, read from `vercel env ls`'s table. */
30
+ export function envNames(cwd, target) {
31
+ const out = vercel(cwd, ['env', 'ls', target]);
32
+ if (out?.status !== 0)
33
+ return null;
34
+ return new Set([...out.stdout.matchAll(/^\s*([A-Z][A-Z0-9_]*)\s/gm)].map((m) => m[1]));
35
+ }
36
+ /** Sets one variable for one target; `force` replaces a value that is already there. */
37
+ export function setEnv(cwd, name, target, value, force) {
38
+ // The value goes on stdin, never argv, so it stays out of the process list.
39
+ return vercel(cwd, ['env', 'add', name, target, ...(force ? ['--force'] : []), '--yes'], value);
40
+ }
41
+ /** The gate variables missing from production or preview, or null when the CLI cannot say. */
42
+ export function gateEnvMissing(cwd) {
43
+ const missing = [];
44
+ for (const target of ENVIRONMENTS) {
45
+ const names = envNames(cwd, target);
46
+ if (!names)
47
+ return null;
48
+ for (const name of GATE_ENV)
49
+ if (!names.has(name))
50
+ missing.push(`${name}:${target}`);
51
+ }
52
+ return missing;
53
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ import { type Args } from './lib/cli.ts';
3
+ export declare const spec: {
4
+ readonly name: 'new';
5
+ readonly summary: 'Scaffold a prototype folder, or with --guide a feature walkthrough.';
6
+ readonly positionals: [{
7
+ readonly name: 'slug';
8
+ readonly description: 'kebab-case folder name (with --guide: the feature)';
9
+ readonly required: true;
10
+ }];
11
+ readonly flags: {
12
+ readonly app: {
13
+ readonly type: 'boolean';
14
+ readonly description: 'Inside the product or site shell instead of the Space.';
15
+ };
16
+ readonly sprint: {
17
+ readonly type: 'boolean';
18
+ readonly description: "The feature's one primary prototype. Needs --feature.";
19
+ };
20
+ readonly alternate: {
21
+ readonly type: 'boolean';
22
+ readonly description: 'Another take on the primary. Needs --feature and a primary.';
23
+ };
24
+ readonly feature: {
25
+ readonly type: 'string';
26
+ readonly description: 'The feature slug: the folder under docs/sprints/.';
27
+ };
28
+ readonly title: {
29
+ readonly type: 'string';
30
+ readonly description: 'Display title. Default: the slug in sentence case.';
31
+ };
32
+ readonly guide: {
33
+ readonly type: 'boolean';
34
+ readonly description: 'Scaffold docs/sprints/<feature>/guide.md instead.';
35
+ };
36
+ };
37
+ readonly output: '{ ok, created, url, next }';
38
+ };
39
+ type Kind = 'sprint' | 'alternate' | 'exploration';
40
+ /** The kind the flags ask for, refused when the feature's state does not allow it. */
41
+ export declare function kindFor(flags: Pick<Args<typeof spec>['flags'], 'sprint' | 'alternate'>, feature: string, primary?: (f: string) => string | undefined): Kind;
42
+ export declare function main(args: Args<typeof spec>): {
43
+ created: string;
44
+ url: string;
45
+ next: string;
46
+ };
47
+ export {};
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ // Scaffolds a prototype from the package's templates/ (a Space, or --app inside the
4
+ // shell), or a feature's walkthrough at docs/sprints/<feature>/guide.md.
5
+ // A feature has one primary prototype (--sprint); --alternate is refused
6
+ // until it exists, so the index always has a primary to show first.
7
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
8
+ import { dirname, join, relative } from 'node:path';
9
+ import { featureTitle, guideHref, SLUG } from '../core/feature.js';
10
+ import { devPort, readEnv } from '../server/env.js';
11
+ import { DOCS, PROTOTYPES, ROOT, TEMPLATES } from '../server/paths.js';
12
+ import { primaryOf } from './lib/catalog.js';
13
+ import { CliError } from './lib/cli.js';
14
+ export const spec = {
15
+ name: 'new',
16
+ summary: 'Scaffold a prototype folder, or with --guide a feature walkthrough.',
17
+ positionals: [
18
+ { name: 'slug', description: 'kebab-case folder name (with --guide: the feature)', required: true },
19
+ ],
20
+ flags: {
21
+ app: { type: 'boolean', description: 'Inside the product or site shell instead of the Space.' },
22
+ sprint: { type: 'boolean', description: "The feature's one primary prototype. Needs --feature." },
23
+ alternate: {
24
+ type: 'boolean',
25
+ description: 'Another take on the primary. Needs --feature and a primary.',
26
+ },
27
+ feature: { type: 'string', description: 'The feature slug: the folder under docs/sprints/.' },
28
+ title: { type: 'string', description: 'Display title. Default: the slug in sentence case.' },
29
+ guide: { type: 'boolean', description: 'Scaffold docs/sprints/<feature>/guide.md instead.' },
30
+ },
31
+ output: '{ ok, created, url, next }',
32
+ };
33
+ const created = () => new Date().toISOString().slice(0, 10);
34
+ const localUrl = (path) => `http://localhost:${devPort(readEnv(ROOT)) ?? '<port>'}${path}`;
35
+ /** Copies a template file or folder and fills its {{placeholders}}. */
36
+ function scaffold(src, dest, vars) {
37
+ if (existsSync(dest))
38
+ throw new CliError(`${relative(ROOT, dest)} already exists.`);
39
+ mkdirSync(dirname(dest), { recursive: true });
40
+ cpSync(src, dest, { recursive: true });
41
+ const files = statSync(dest).isDirectory() ? readdirSync(dest).map((f) => join(dest, f)) : [dest];
42
+ for (const path of files) {
43
+ let text = readFileSync(path, 'utf8');
44
+ for (const [key, value] of Object.entries(vars))
45
+ text = text.replaceAll(`{{${key}}}`, () => value);
46
+ writeFileSync(path, text);
47
+ }
48
+ }
49
+ function guide(feature) {
50
+ if (!SLUG.test(feature))
51
+ throw new CliError('The feature is a kebab-case slug.', { got: feature });
52
+ const dest = join(DOCS, 'sprints', feature, 'guide.md');
53
+ scaffold(join(TEMPLATES, 'guide.md'), dest, {
54
+ feature,
55
+ title: featureTitle(feature),
56
+ created: created(),
57
+ });
58
+ return {
59
+ created: relative(ROOT, dest),
60
+ url: localUrl(guideHref(feature)),
61
+ next: `Write it from the States table. Screenshots go in docs/sprints/${feature}/assets/guide/.`,
62
+ };
63
+ }
64
+ /** The kind the flags ask for, refused when the feature's state does not allow it. */
65
+ export function kindFor(flags, feature, primary = (f) => primaryOf(f)?.slug) {
66
+ if (flags.sprint && flags.alternate)
67
+ throw new CliError('Pick one of --sprint and --alternate.');
68
+ const kind = flags.sprint ? 'sprint' : flags.alternate ? 'alternate' : 'exploration';
69
+ if (kind === 'exploration')
70
+ return kind;
71
+ if (!feature)
72
+ throw new CliError(`A ${kind} prototype names its feature.`, { fix: 'Add --feature <feature>.' });
73
+ const existing = primary(feature);
74
+ if (kind === 'sprint' && existing)
75
+ throw new CliError(`${feature} already has its primary prototype, ${existing}/.`, {
76
+ fix: 'Use --alternate for another take on it.',
77
+ });
78
+ if (kind === 'alternate' && !existing)
79
+ throw new CliError(`${feature} has no primary prototype yet.`, {
80
+ fix: 'Start it with --sprint, then add alternates.',
81
+ });
82
+ return kind;
83
+ }
84
+ function prototype(slug, { flags }) {
85
+ if (!SLUG.test(slug))
86
+ throw new CliError('The slug is kebab-case.', { got: slug });
87
+ if (slug === 'guide')
88
+ throw new CliError('guide/ is the walkthrough route; pick another slug.');
89
+ const feature = flags.feature ?? '';
90
+ if (feature && !SLUG.test(feature))
91
+ throw new CliError('--feature takes a kebab-case slug.', { got: feature });
92
+ const kind = kindFor(flags, feature);
93
+ const context = flags.app ? 'app' : 'space';
94
+ scaffold(join(TEMPLATES, context), join(PROTOTYPES, slug), {
95
+ title: flags.title ?? featureTitle(slug),
96
+ slug,
97
+ created: created(),
98
+ kind,
99
+ feature,
100
+ });
101
+ return {
102
+ created: `prototypes/${slug}/`,
103
+ context,
104
+ kind,
105
+ feature,
106
+ url: localUrl(`/${slug}/`),
107
+ next: `Write the Problem, Question and States in prototypes/${slug}/README.md before any code.`,
108
+ };
109
+ }
110
+ export function main(args) {
111
+ const [slug] = args.positionals;
112
+ return args.flags.guide ? guide(slug) : prototype(slug, args);
113
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { type Args } from './lib/cli.ts';
3
+ export declare const spec: {
4
+ readonly name: 'people';
5
+ readonly summary: 'List, invite, reissue or revoke the people who comment on the deployed prototypes.';
6
+ readonly positionals: [{
7
+ readonly name: 'action';
8
+ readonly description: 'list (default): everyone who may comment. invite: a new person and their link. reissue <id>: a new link; the old one and every session stop. revoke <id>.';
9
+ }, {
10
+ readonly name: 'id';
11
+ readonly description: 'The person, for reissue and revoke';
12
+ }];
13
+ readonly flags: {
14
+ readonly name: {
15
+ readonly type: 'string';
16
+ readonly description: 'For invite: their name as it shows on comments.';
17
+ };
18
+ readonly team: {
19
+ readonly type: 'string';
20
+ readonly description: "For invite: the studio, or the client's company.";
21
+ };
22
+ readonly role: {
23
+ readonly type: 'string';
24
+ readonly default: 'reviewer';
25
+ readonly choices: readonly ["editor", "reviewer"];
26
+ readonly description: 'reviewer comments; editor also resolves, sorts and invites.';
27
+ };
28
+ readonly site: {
29
+ readonly type: 'string';
30
+ readonly description: 'The deployed URL, so the invite link prints whole.';
31
+ };
32
+ readonly local: {
33
+ readonly type: 'boolean';
34
+ readonly description: 'Use the local dev store even when the deployed one is configured.';
35
+ };
36
+ };
37
+ readonly output: '{ ok, store, people } or { ok, store, person, link }';
38
+ };
39
+ export declare function main({ positionals: [action, id], flags }: Args<typeof spec>): Promise<{
40
+ store: "deployed" | "local";
41
+ people: Omit<import("../core/review/model.ts").Person, "epoch">[];
42
+ person?: undefined;
43
+ } | {
44
+ link: string;
45
+ next: string;
46
+ people?: undefined;
47
+ store: "deployed" | "local";
48
+ person: Omit<import("../core/review/model.ts").Person, "epoch">;
49
+ } | {
50
+ next?: undefined;
51
+ link: string;
52
+ people?: undefined;
53
+ store: "deployed" | "local";
54
+ person: Omit<import("../core/review/model.ts").Person, "epoch">;
55
+ } | {
56
+ people?: undefined;
57
+ store: "deployed" | "local";
58
+ person: Omit<import("../core/review/model.ts").Person, "epoch">;
59
+ }>;
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ // The people who comment on the deployed prototypes. The first editor is
4
+ // invited from here; after that, editors invite from the People tab.
5
+ import { ROLES } from '../core/review/model.js';
6
+ import { CliError } from './lib/cli.js';
7
+ import { cliActor, cliReview } from './lib/review.js';
8
+ export const spec = {
9
+ name: 'people',
10
+ summary: 'List, invite, reissue or revoke the people who comment on the deployed prototypes.',
11
+ positionals: [
12
+ {
13
+ name: 'action',
14
+ description: 'list (default): everyone who may comment. invite: a new person and their link. reissue <id>: a new link; the old one and every session stop. revoke <id>.',
15
+ },
16
+ { name: 'id', description: 'The person, for reissue and revoke' },
17
+ ],
18
+ flags: {
19
+ name: { type: 'string', description: 'For invite: their name as it shows on comments.' },
20
+ team: { type: 'string', description: "For invite: the studio, or the client's company." },
21
+ role: {
22
+ type: 'string',
23
+ default: 'reviewer',
24
+ choices: ROLES,
25
+ description: 'reviewer comments; editor also resolves, sorts and invites.',
26
+ },
27
+ site: { type: 'string', description: 'The deployed URL, so the invite link prints whole.' },
28
+ local: {
29
+ type: 'boolean',
30
+ description: 'Use the local dev store even when the deployed one is configured.',
31
+ },
32
+ },
33
+ output: '{ ok, store, people } or { ok, store, person, link }',
34
+ };
35
+ const linkFor = (token, site) => {
36
+ const path = `/invite/${token}`;
37
+ if (!site)
38
+ return { link: path, next: 'Prefix the link with the deployed URL, or pass --site <url>.' };
39
+ try {
40
+ return { link: new URL(path, site).href };
41
+ }
42
+ catch {
43
+ throw new CliError('--site takes a URL.', { got: site });
44
+ }
45
+ };
46
+ export async function main({ positionals: [action = 'list', id], flags }) {
47
+ const { review, store } = cliReview(flags.local);
48
+ const actor = cliActor();
49
+ const strip = ({ epoch: _e, ...rest }) => rest;
50
+ switch (action) {
51
+ case 'list':
52
+ return { store, people: (await review.listPeople(actor)).map(strip) };
53
+ case 'invite': {
54
+ const { person, token } = await review.invite(actor, {
55
+ name: flags.name,
56
+ team: flags.team,
57
+ role: flags.role,
58
+ });
59
+ return { store, person: strip(person), ...linkFor(token, flags.site) };
60
+ }
61
+ case 'reissue':
62
+ case 'revoke': {
63
+ if (!id)
64
+ throw new CliError(`Name the person: pnpm people ${action} <id>.`, { fix: 'pnpm people lists ids.' });
65
+ if (action === 'revoke')
66
+ return { store, person: strip(await review.revoke(actor, id)) };
67
+ const { person, token } = await review.reissue(actor, id);
68
+ return { store, person: strip(person), ...linkFor(token, flags.site) };
69
+ }
70
+ default:
71
+ throw new CliError(`Unknown action ${action}.`, { fix: 'list, invite, reissue or revoke' });
72
+ }
73
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { type OnDisk } from './lib/catalog.ts';
3
+ import type { Args } from './lib/cli.ts';
4
+ import { judge } from './lib/judge.ts';
5
+ export declare const STEPS: readonly ['checks', 'types', 'lint', 'build', 'elyx', 'guides', 'states', 'contacts', 'gate', 'secrets'];
6
+ type StepName = (typeof STEPS)[number];
7
+ export declare const spec: {
8
+ readonly name: 'preflight';
9
+ readonly summary: 'Run every check a share link needs. Exit 1 when any step fails.';
10
+ readonly flags: {
11
+ readonly skip: {
12
+ readonly type: 'list';
13
+ readonly description: `A step to skip: ${string}.`;
14
+ };
15
+ readonly 'allow-contact': {
16
+ readonly type: 'list';
17
+ readonly description: 'A contact the user confirmed is a business, added to .design-hub/checks.json contacts.allow.';
18
+ };
19
+ };
20
+ readonly output: '{ ok, steps: [{ step, ok, skipped?, detail?, fix? }] }';
21
+ };
22
+ type Step = {
23
+ step: StepName;
24
+ ok: boolean;
25
+ skipped?: string;
26
+ detail?: unknown;
27
+ fix?: string;
28
+ };
29
+ export declare function states(all: OnDisk[], ask?: typeof judge): Promise<Step>;
30
+ export declare function contacts(all: OnDisk[], allow: Set<string>, ask?: typeof judge): Promise<Step>;
31
+ export declare function main({ flags }: Args<typeof spec>): Promise<{
32
+ ok: boolean;
33
+ steps: Step[];
34
+ }>;
35
+ export {};
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ // Everything that must be true before a share link goes out, in one run, so
4
+ // no step depends on Claude remembering it. Every step runs, so one failure
5
+ // never hides the next; the JSON says which failed and the fix.
6
+ import { spawnSync } from 'node:child_process';
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { dirname, join, relative } from 'node:path';
9
+ import { isLive } from '../core/catalog.js';
10
+ import { DEFAULT_CHECKS, reaches } from '../core/checks.js';
11
+ import { contactsInJson } from '../core/contacts.js';
12
+ import { noulOf } from '../core/jev.js';
13
+ import { checkProblems, checks } from '../server/checks.js';
14
+ import { CHECKS_FILE, DOCS, ROOT } from '../server/paths.js';
15
+ import { lint, typecheck } from './check.js';
16
+ import { build } from './dev.js';
17
+ import * as elyxCommand from './elyx.js';
18
+ import { prototypes } from './lib/catalog.js';
19
+ import { judge } from './lib/judge.js';
20
+ import { NOULS } from './lib/rules.js';
21
+ import { gateEnvMissing } from './lib/vercel.js';
22
+ export const STEPS = [
23
+ 'checks',
24
+ 'types',
25
+ 'lint',
26
+ 'build',
27
+ 'elyx',
28
+ 'guides',
29
+ 'states',
30
+ 'contacts',
31
+ 'gate',
32
+ 'secrets',
33
+ ];
34
+ export const spec = {
35
+ name: 'preflight',
36
+ summary: 'Run every check a share link needs. Exit 1 when any step fails.',
37
+ flags: {
38
+ skip: { type: 'list', description: `A step to skip: ${STEPS.join(', ')}.` },
39
+ 'allow-contact': {
40
+ type: 'list',
41
+ description: 'A contact the user confirmed is a business, added to .design-hub/checks.json contacts.allow.',
42
+ },
43
+ },
44
+ output: '{ ok, steps: [{ step, ok, skipped?, detail?, fix? }] }',
45
+ };
46
+ const MAX_CODE_CHARS = 12_000;
47
+ const outcome = (step, run, fix) => run.ok ? { step, ok: true } : { step, ok: false, detail: run.output ?? run.error, fix };
48
+ /** Prototypes a reviewer is sent to: live sprints and their alternates. */
49
+ const reviewed = (all) => all.filter((p) => isLive(p) && p.kind !== 'exploration');
50
+ function guides(all) {
51
+ const features = [
52
+ ...new Set(reviewed(all)
53
+ .map((p) => p.feature)
54
+ .filter(Boolean)),
55
+ ];
56
+ const missing = features.filter((f) => !existsSync(join(DOCS, 'sprints', f, 'guide.md')));
57
+ return missing.length
58
+ ? {
59
+ step: 'guides',
60
+ ok: false,
61
+ detail: { missing },
62
+ fix: 'design-hub guide <feature>, written from the States table.',
63
+ }
64
+ : { step: 'guides', ok: true, detail: { features } };
65
+ }
66
+ function code(dir) {
67
+ return readdirSync(dir)
68
+ .filter((f) => /\.tsx?$/.test(f) && !f.endsWith('.test.ts'))
69
+ .map((f) => `// ${f}\n${readFileSync(join(dir, f), 'utf8')}`)
70
+ .join('\n\n')
71
+ .slice(0, MAX_CODE_CHARS);
72
+ }
73
+ export async function states(all, ask = judge) {
74
+ const problems = [];
75
+ for (const p of reviewed(all)) {
76
+ if (!p.states?.length) {
77
+ problems.push({ prototype: p.slug, problem: 'README has no States rows' });
78
+ continue;
79
+ }
80
+ if (!checks)
81
+ continue;
82
+ const judged = await ask({
83
+ check: 'states',
84
+ subject: p.slug,
85
+ state: {
86
+ prototype: p.slug,
87
+ readmeStates: p.states.map((s) => s.name),
88
+ change: { content: code(p.dir) },
89
+ },
90
+ questions: { state: { type: 'noul', instructions: NOULS.state } },
91
+ });
92
+ const prob = judged ? noulOf(judged.answers, 'state') : null;
93
+ if (prob !== null && reaches(prob, checks.states.blockAt))
94
+ problems.push({
95
+ prototype: p.slug,
96
+ problem: 'renders a state the States table does not list',
97
+ p: prob,
98
+ id: judged?.ids.state,
99
+ });
100
+ }
101
+ return problems.length
102
+ ? {
103
+ step: 'states',
104
+ ok: false,
105
+ detail: problems,
106
+ fix: 'Add a row per state, with how to reach it, to the README.',
107
+ }
108
+ : { step: 'states', ok: true };
109
+ }
110
+ export async function contacts(all, allow, ask = judge) {
111
+ const flagged = [];
112
+ for (const p of all) {
113
+ const file = join(p.dir, 'fixtures.json');
114
+ if (!existsSync(file))
115
+ continue;
116
+ const found = contactsInJson(JSON.parse(readFileSync(file, 'utf8'))).filter((c) => !allow.has(c.value));
117
+ if (!found.length)
118
+ continue;
119
+ const rel = relative(ROOT, file);
120
+ // With Jev, only a private person's details stop the share; without it, every match does.
121
+ const judged = checks &&
122
+ (await ask({
123
+ check: 'contacts',
124
+ subject: rel,
125
+ state: { contacts: found.map(({ kind, value, path }) => ({ kind, value, path })) },
126
+ questions: Object.fromEntries(found.map((_, i) => [
127
+ `private:${i}`,
128
+ {
129
+ type: 'noul',
130
+ instructions: `Is \`contacts[${i}].value\` the personal contact of a private individual, rather than a business, organisation or role address?`,
131
+ },
132
+ ])),
133
+ }));
134
+ found.forEach((c, i) => {
135
+ const prob = judged ? noulOf(judged.answers, `private:${i}`) : null;
136
+ if (prob === null || !checks || reaches(prob, checks.contacts.privateAt))
137
+ flagged.push({
138
+ ...c,
139
+ file: rel,
140
+ private: prob ?? undefined,
141
+ id: judged ? judged.ids[`private:${i}`] : undefined,
142
+ });
143
+ });
144
+ }
145
+ return flagged.length
146
+ ? {
147
+ step: 'contacts',
148
+ ok: false,
149
+ detail: flagged,
150
+ fix: 'Show each to the user. Replace it, or after they confirm it is a business contact: design-hub preflight --allow-contact <value>.',
151
+ }
152
+ : { step: 'contacts', ok: true };
153
+ }
154
+ // Only for a project that took the Elyx add-on; without the CLI on this machine the check cannot run.
155
+ function elyx() {
156
+ if (!existsSync(join(ROOT, 'design-system', 'elyx.json')))
157
+ return { step: 'elyx', ok: true, skipped: 'no design-system/' };
158
+ if (spawnSync('elyx', ['--version']).error)
159
+ return { step: 'elyx', ok: true, skipped: 'Elyx CLI not installed' };
160
+ try {
161
+ const result = elyxCommand.main({ flags: { check: true }, positionals: [] });
162
+ return result.ok
163
+ ? { step: 'elyx', ok: true }
164
+ : {
165
+ step: 'elyx',
166
+ ok: false,
167
+ detail: result.drift,
168
+ fix: 'design-hub elyx regenerates design-system/ from tokens.css.',
169
+ };
170
+ }
171
+ catch (error) {
172
+ return {
173
+ step: 'elyx',
174
+ ok: false,
175
+ detail: error instanceof Error ? error.message : String(error),
176
+ fix: 'design-hub elyx',
177
+ };
178
+ }
179
+ }
180
+ function gate() {
181
+ if (!existsSync(join(ROOT, '.vercel', 'project.json')))
182
+ return {
183
+ step: 'gate',
184
+ ok: true,
185
+ skipped: 'not linked to Vercel yet',
186
+ fix: 'design-hub bootstrap --vercel <project>',
187
+ };
188
+ const missing = gateEnvMissing(ROOT);
189
+ if (missing === null)
190
+ return { step: 'gate', ok: true, skipped: 'vercel CLI unavailable or signed out' };
191
+ return missing.length
192
+ ? {
193
+ step: 'gate',
194
+ ok: false,
195
+ detail: { missing },
196
+ fix: 'design-hub bootstrap --vercel <project> sets them.',
197
+ }
198
+ : { step: 'gate', ok: true };
199
+ }
200
+ function secrets() {
201
+ const ignored = spawnSync('git', ['-C', ROOT, 'check-ignore', '-q', '.env']).status === 0;
202
+ const tracked = (spawnSync('git', ['-C', ROOT, 'ls-files'], { encoding: 'utf8' }).stdout ?? '')
203
+ .split('\n')
204
+ .filter((f) => /(^|\/)\.env(\.|$)/.test(f) && !f.endsWith('.env.example'));
205
+ if (ignored && !tracked.length)
206
+ return { step: 'secrets', ok: true };
207
+ return {
208
+ step: 'secrets',
209
+ ok: false,
210
+ detail: { envIgnored: ignored, tracked },
211
+ fix: 'Keep .env in .gitignore; git rm --cached any tracked env file.',
212
+ };
213
+ }
214
+ function allowContacts(values) {
215
+ const current = (existsSync(CHECKS_FILE) ? JSON.parse(readFileSync(CHECKS_FILE, 'utf8')) : structuredClone(DEFAULT_CHECKS));
216
+ const allow = new Set([...(current.contacts.allow ?? []), ...values]);
217
+ if (values.length) {
218
+ current.contacts.allow = [...allow].sort();
219
+ mkdirSync(dirname(CHECKS_FILE), { recursive: true });
220
+ writeFileSync(CHECKS_FILE, `${JSON.stringify(current, null, 2)}\n`);
221
+ }
222
+ return allow;
223
+ }
224
+ export async function main({ flags }) {
225
+ const skip = new Set(flags.skip);
226
+ const all = prototypes();
227
+ const allow = allowContacts(flags['allow-contact']);
228
+ const run = {
229
+ checks: () => checkProblems.length
230
+ ? {
231
+ step: 'checks',
232
+ ok: false,
233
+ detail: checkProblems,
234
+ fix: 'Fix .design-hub/checks.json; every Jev check is off until then.',
235
+ }
236
+ : { step: 'checks', ok: true },
237
+ types: () => outcome('types', typecheck(), 'Fix what tsc names.'),
238
+ lint: () => outcome('lint', lint(false), 'design-hub check --fix for formatting; the rest by hand.'),
239
+ build: async () => outcome('build', await build(), 'A missing walkthrough image fails here first.'),
240
+ elyx,
241
+ guides: () => guides(all),
242
+ states: () => states(all),
243
+ contacts: () => contacts(all, allow),
244
+ gate,
245
+ secrets,
246
+ };
247
+ const steps = [];
248
+ for (const name of STEPS)
249
+ steps.push(skip.has(name) ? { step: name, ok: true, skipped: 'skipped by --skip' } : await run[name]());
250
+ return { ok: steps.every((s) => s.ok), steps };
251
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { type Args } from './lib/cli.ts';
3
+ export declare const spec: {
4
+ readonly name: 'release-check';
5
+ readonly summary: 'Fail when any tracked file contains one of the given terms.';
6
+ readonly flags: {
7
+ readonly term: {
8
+ readonly type: 'list';
9
+ readonly description: 'A term to refuse, case-insensitive. Repeat for more.';
10
+ };
11
+ };
12
+ readonly output: '{ ok, terms, files, hits: { <file>: [terms] } }';
13
+ };
14
+ export declare function main({ flags }: Args<typeof spec>): {
15
+ ok: boolean;
16
+ terms: string[];
17
+ files: number;
18
+ hits: Record<string, string[]>;
19
+ };