@vendoai/vendo 0.4.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 (212) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +28 -0
  3. package/bin/vendo.mjs +4 -0
  4. package/dist/ai-sdk.d.ts +35 -0
  5. package/dist/ai-sdk.js +62 -0
  6. package/dist/auth-presets/auth-js.d.ts +14 -0
  7. package/dist/auth-presets/auth-js.js +91 -0
  8. package/dist/auth-presets/auth0.d.ts +23 -0
  9. package/dist/auth-presets/auth0.js +98 -0
  10. package/dist/auth-presets/clerk.d.ts +23 -0
  11. package/dist/auth-presets/clerk.js +65 -0
  12. package/dist/auth-presets/conformance.d.ts +42 -0
  13. package/dist/auth-presets/conformance.js +139 -0
  14. package/dist/auth-presets/identity.d.ts +56 -0
  15. package/dist/auth-presets/identity.js +152 -0
  16. package/dist/auth-presets/index.d.ts +14 -0
  17. package/dist/auth-presets/index.js +12 -0
  18. package/dist/auth-presets/jwt.d.ts +16 -0
  19. package/dist/auth-presets/jwt.js +54 -0
  20. package/dist/auth-presets/shared.d.ts +35 -0
  21. package/dist/auth-presets/shared.js +1 -0
  22. package/dist/auth-presets/supabase.d.ts +42 -0
  23. package/dist/auth-presets/supabase.js +229 -0
  24. package/dist/byo-approvals.d.ts +37 -0
  25. package/dist/byo-approvals.js +189 -0
  26. package/dist/capability-misses.d.ts +38 -0
  27. package/dist/capability-misses.js +172 -0
  28. package/dist/catalog.d.ts +19 -0
  29. package/dist/catalog.js +152 -0
  30. package/dist/cli/cloud/args.d.ts +3 -0
  31. package/dist/cli/cloud/args.js +32 -0
  32. package/dist/cli/cloud/auth.d.ts +17 -0
  33. package/dist/cli/cloud/auth.js +123 -0
  34. package/dist/cli/cloud/client.d.ts +28 -0
  35. package/dist/cli/cloud/client.js +133 -0
  36. package/dist/cli/cloud/command.d.ts +22 -0
  37. package/dist/cli/cloud/command.js +73 -0
  38. package/dist/cli/cloud/deploy.d.ts +26 -0
  39. package/dist/cli/cloud/deploy.js +187 -0
  40. package/dist/cli/cloud/device-login.d.ts +32 -0
  41. package/dist/cli/cloud/device-login.js +259 -0
  42. package/dist/cli/cloud/index.d.ts +5 -0
  43. package/dist/cli/cloud/index.js +59 -0
  44. package/dist/cli/cloud/keys.d.ts +2 -0
  45. package/dist/cli/cloud/keys.js +31 -0
  46. package/dist/cli/cloud/members.d.ts +3 -0
  47. package/dist/cli/cloud/members.js +25 -0
  48. package/dist/cli/cloud/output.d.ts +4 -0
  49. package/dist/cli/cloud/output.js +12 -0
  50. package/dist/cli/cloud/pending-claim.d.ts +29 -0
  51. package/dist/cli/cloud/pending-claim.js +38 -0
  52. package/dist/cli/cloud/read.d.ts +3 -0
  53. package/dist/cli/cloud/read.js +12 -0
  54. package/dist/cli/cloud/services.d.ts +2 -0
  55. package/dist/cli/cloud/services.js +9 -0
  56. package/dist/cli/cloud/session.d.ts +13 -0
  57. package/dist/cli/cloud/session.js +41 -0
  58. package/dist/cli/cloud-init.d.ts +60 -0
  59. package/dist/cli/cloud-init.js +149 -0
  60. package/dist/cli/dep-versions.d.ts +26 -0
  61. package/dist/cli/dep-versions.js +77 -0
  62. package/dist/cli/doctor-codes.d.ts +58 -0
  63. package/dist/cli/doctor-codes.js +60 -0
  64. package/dist/cli/doctor-live.d.ts +67 -0
  65. package/dist/cli/doctor-live.js +169 -0
  66. package/dist/cli/doctor.d.ts +52 -0
  67. package/dist/cli/doctor.js +467 -0
  68. package/dist/cli/eject.d.ts +29 -0
  69. package/dist/cli/eject.js +141 -0
  70. package/dist/cli/extract/apply.d.ts +26 -0
  71. package/dist/cli/extract/apply.js +123 -0
  72. package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
  73. package/dist/cli/extract/claude-cli-harness.js +132 -0
  74. package/dist/cli/extract/claude-harness.d.ts +14 -0
  75. package/dist/cli/extract/claude-harness.js +127 -0
  76. package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
  77. package/dist/cli/extract/codex-cli-harness.js +154 -0
  78. package/dist/cli/extract/delegate.d.ts +21 -0
  79. package/dist/cli/extract/delegate.js +77 -0
  80. package/dist/cli/extract/extraction.d.ts +65 -0
  81. package/dist/cli/extract/extraction.js +300 -0
  82. package/dist/cli/extract/gateway-fuel.d.ts +69 -0
  83. package/dist/cli/extract/gateway-fuel.js +69 -0
  84. package/dist/cli/extract/harness.d.ts +114 -0
  85. package/dist/cli/extract/harness.js +84 -0
  86. package/dist/cli/extract/index.d.ts +9 -0
  87. package/dist/cli/extract/index.js +9 -0
  88. package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
  89. package/dist/cli/extract/npx-engine-harness.js +225 -0
  90. package/dist/cli/extract/stages.d.ts +149 -0
  91. package/dist/cli/extract/stages.js +398 -0
  92. package/dist/cli/framework.d.ts +8 -0
  93. package/dist/cli/framework.js +36 -0
  94. package/dist/cli/init-auth.d.ts +67 -0
  95. package/dist/cli/init-auth.js +142 -0
  96. package/dist/cli/init-scaffolds.d.ts +28 -0
  97. package/dist/cli/init-scaffolds.js +235 -0
  98. package/dist/cli/init.d.ts +115 -0
  99. package/dist/cli/init.js +1008 -0
  100. package/dist/cli/mcp/index.d.ts +7 -0
  101. package/dist/cli/mcp/index.js +59 -0
  102. package/dist/cli/mcp/registry.d.ts +9 -0
  103. package/dist/cli/mcp/registry.js +117 -0
  104. package/dist/cli/mcp/server-json.d.ts +12 -0
  105. package/dist/cli/mcp/server-json.js +65 -0
  106. package/dist/cli/mcp/server.schema.json +574 -0
  107. package/dist/cli/mcp/verify-domain.d.ts +11 -0
  108. package/dist/cli/mcp/verify-domain.js +48 -0
  109. package/dist/cli/playground/app/embed-entry.d.ts +20 -0
  110. package/dist/cli/playground/app/embed-entry.js +46 -0
  111. package/dist/cli/playground/app/fake-client.d.ts +3 -0
  112. package/dist/cli/playground/app/fake-client.js +215 -0
  113. package/dist/cli/playground/app/fixtures.d.ts +47 -0
  114. package/dist/cli/playground/app/fixtures.js +472 -0
  115. package/dist/cli/playground/app/main.d.ts +1 -0
  116. package/dist/cli/playground/app/main.js +108 -0
  117. package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
  118. package/dist/cli/playground/app/scenario-mount.js +48 -0
  119. package/dist/cli/playground/app/scenarios.d.ts +25 -0
  120. package/dist/cli/playground/app/scenarios.js +152 -0
  121. package/dist/cli/playground/app/theme-editor.d.ts +13 -0
  122. package/dist/cli/playground/app/theme-editor.js +149 -0
  123. package/dist/cli/playground/app/theme-state.d.ts +29 -0
  124. package/dist/cli/playground/app/theme-state.js +151 -0
  125. package/dist/cli/playground/bundle.gen.d.ts +2 -0
  126. package/dist/cli/playground/bundle.gen.js +2 -0
  127. package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
  128. package/dist/cli/playground/embed-bundle.gen.js +2 -0
  129. package/dist/cli/playground.d.ts +33 -0
  130. package/dist/cli/playground.js +120 -0
  131. package/dist/cli/pretty.d.ts +48 -0
  132. package/dist/cli/pretty.js +330 -0
  133. package/dist/cli/refine.d.ts +43 -0
  134. package/dist/cli/refine.js +181 -0
  135. package/dist/cli/semantics.d.ts +31 -0
  136. package/dist/cli/semantics.js +122 -0
  137. package/dist/cli/shared.d.ts +68 -0
  138. package/dist/cli/shared.js +184 -0
  139. package/dist/cli/sync.d.ts +36 -0
  140. package/dist/cli/sync.js +202 -0
  141. package/dist/cli/theme/color.d.ts +15 -0
  142. package/dist/cli/theme/color.js +188 -0
  143. package/dist/cli/theme/css-vars.d.ts +13 -0
  144. package/dist/cli/theme/css-vars.js +40 -0
  145. package/dist/cli/theme/entry-candidates.d.ts +5 -0
  146. package/dist/cli/theme/entry-candidates.js +22 -0
  147. package/dist/cli/theme/extract-theme.d.ts +184 -0
  148. package/dist/cli/theme/extract-theme.js +358 -0
  149. package/dist/cli/theme/walk.d.ts +2 -0
  150. package/dist/cli/theme/walk.js +24 -0
  151. package/dist/cli.d.ts +2 -0
  152. package/dist/cli.js +333 -0
  153. package/dist/cloud-apps.d.ts +17 -0
  154. package/dist/cloud-apps.js +46 -0
  155. package/dist/cloud-console.d.ts +32 -0
  156. package/dist/cloud-console.js +72 -0
  157. package/dist/cloud-tools.d.ts +23 -0
  158. package/dist/cloud-tools.js +196 -0
  159. package/dist/connections.d.ts +71 -0
  160. package/dist/connections.js +192 -0
  161. package/dist/deployment-identity.d.ts +12 -0
  162. package/dist/deployment-identity.js +69 -0
  163. package/dist/dev-creds/model.d.ts +65 -0
  164. package/dist/dev-creds/model.js +159 -0
  165. package/dist/dev-creds/resolve.d.ts +36 -0
  166. package/dist/dev-creds/resolve.js +57 -0
  167. package/dist/hosted-store.d.ts +52 -0
  168. package/dist/hosted-store.js +322 -0
  169. package/dist/index.d.ts +13 -0
  170. package/dist/index.js +1 -0
  171. package/dist/mastra.d.ts +46 -0
  172. package/dist/mastra.js +88 -0
  173. package/dist/react.d.ts +10 -0
  174. package/dist/react.js +32 -0
  175. package/dist/refine.d.ts +244 -0
  176. package/dist/refine.js +599 -0
  177. package/dist/remixable.d.ts +18 -0
  178. package/dist/remixable.js +42 -0
  179. package/dist/runtime-capture.d.ts +18 -0
  180. package/dist/runtime-capture.js +111 -0
  181. package/dist/sandbox-wire.d.ts +101 -0
  182. package/dist/sandbox-wire.js +101 -0
  183. package/dist/sandbox.d.ts +38 -0
  184. package/dist/sandbox.js +389 -0
  185. package/dist/server.d.ts +216 -0
  186. package/dist/server.js +1320 -0
  187. package/dist/sync-impact.d.ts +14 -0
  188. package/dist/sync-impact.js +75 -0
  189. package/dist/turn-liveness.d.ts +33 -0
  190. package/dist/turn-liveness.js +105 -0
  191. package/dist/wire/approvals.d.ts +10 -0
  192. package/dist/wire/approvals.js +59 -0
  193. package/dist/wire/apps.d.ts +5 -0
  194. package/dist/wire/apps.js +155 -0
  195. package/dist/wire/automations.d.ts +5 -0
  196. package/dist/wire/automations.js +57 -0
  197. package/dist/wire/box.d.ts +3 -0
  198. package/dist/wire/box.js +268 -0
  199. package/dist/wire/connections.d.ts +5 -0
  200. package/dist/wire/connections.js +47 -0
  201. package/dist/wire/context.d.ts +22 -0
  202. package/dist/wire/context.js +190 -0
  203. package/dist/wire/doctor.d.ts +19 -0
  204. package/dist/wire/doctor.js +116 -0
  205. package/dist/wire/misc.d.ts +18 -0
  206. package/dist/wire/misc.js +227 -0
  207. package/dist/wire/shared.d.ts +158 -0
  208. package/dist/wire/shared.js +134 -0
  209. package/dist/wire/threads.d.ts +3 -0
  210. package/dist/wire/threads.js +68 -0
  211. package/package.json +151 -0
  212. package/skills/vendo-setup/SKILL.md +128 -0
@@ -0,0 +1,358 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { contrastingText, normalizeColor, normalizeLength, resolveCssVarRefs } from "./color.js";
5
+ import { parseCssVars } from "./css-vars.js";
6
+ import { ENTRY_FILE_CANDIDATES } from "./entry-candidates.js";
7
+ import { walk } from "./walk.js";
8
+ /** Key ORDER is load-bearing: assembleTheme derives accentText from the
9
+ * already-settled accent and headingFamily from fontFamily within one pass
10
+ * over SLOT_KEYS, so accent must precede accentText and fontFamily must
11
+ * precede headingFamily. Do not alphabetize. */
12
+ const DEFAULT_THEME_SLOTS = {
13
+ accent: "#2563eb",
14
+ accentText: "#ffffff",
15
+ background: "#ffffff",
16
+ border: "#e2e8f0",
17
+ danger: "#dc2626",
18
+ surface: "#f8fafc",
19
+ text: "#0f172a",
20
+ mutedText: "#64748b",
21
+ radius: "8px",
22
+ fontFamily: "system-ui, sans-serif",
23
+ headingFamily: "system-ui, sans-serif",
24
+ baseSize: "16px",
25
+ density: "comfortable",
26
+ motion: "full",
27
+ };
28
+ const CSS_IMPORT_RE = /\bimport\s+(?:[^"']+\s+from\s+)?["']([^"']+\.css)["']|\brequire\(\s*["']([^"']+\.css)["']\s*\)/g;
29
+ const CSS_AT_IMPORT_RE = /@import\s+(?:url\(\s*)?["']?([^"')\s;]+)["']?\s*\)?/g;
30
+ const CSS_FALLBACK_NAME = /^(?:globals?|app|main|index|tokens?|theme|styles?)\.css$/;
31
+ const MAX_CSS_FILES = 12;
32
+ const MAX_FILE_BYTES = 24_000;
33
+ async function readCapped(file) {
34
+ try {
35
+ const content = await fs.readFile(file, "utf8");
36
+ return content.length > MAX_FILE_BYTES ? content.slice(0, MAX_FILE_BYTES) : content;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ async function exists(file) {
43
+ return fs.access(file).then(() => true, () => false);
44
+ }
45
+ function resolveLocalSpec(spec, fromDir, targetDir) {
46
+ if (spec.startsWith(".") || spec.startsWith("/"))
47
+ return path.resolve(fromDir, spec);
48
+ // The near-universal `@/` alias; package-specifier CSS (vendor sheets) is
49
+ // deliberately not chased — host brand tokens live in the host's own tree.
50
+ if (spec.startsWith("@/"))
51
+ return path.join(targetDir, "src", spec.slice(2));
52
+ return null;
53
+ }
54
+ async function collectCss(layout, targetDir) {
55
+ const files = [];
56
+ const seen = new Set();
57
+ const visit = async (absolute, depth) => {
58
+ if (seen.has(absolute) || files.length >= MAX_CSS_FILES || depth > 3)
59
+ return;
60
+ seen.add(absolute);
61
+ const content = await readCapped(absolute);
62
+ if (content === null)
63
+ return;
64
+ files.push({ path: path.relative(targetDir, absolute), content });
65
+ for (const match of content.matchAll(CSS_AT_IMPORT_RE)) {
66
+ const spec = match[1];
67
+ if (spec === "tailwindcss" || spec.startsWith("http"))
68
+ continue;
69
+ const resolved = resolveLocalSpec(spec, path.dirname(absolute), targetDir);
70
+ if (resolved === null)
71
+ continue;
72
+ const candidate = resolved.endsWith(".css") ? resolved : `${resolved}.css`;
73
+ if (await exists(candidate))
74
+ await visit(candidate, depth + 1);
75
+ else if (spec.startsWith("@/") && await exists(path.join(targetDir, spec.slice(2)))) {
76
+ await visit(path.join(targetDir, spec.slice(2)), depth + 1);
77
+ }
78
+ }
79
+ };
80
+ if (layout !== null) {
81
+ const layoutDir = path.dirname(path.join(targetDir, layout.path));
82
+ for (const match of layout.content.matchAll(CSS_IMPORT_RE)) {
83
+ const spec = (match[1] ?? match[2]);
84
+ let resolved = resolveLocalSpec(spec, layoutDir, targetDir);
85
+ if (resolved !== null && spec.startsWith("@/") && !(await exists(resolved))) {
86
+ resolved = path.join(targetDir, spec.slice(2));
87
+ }
88
+ if (resolved !== null && await exists(resolved))
89
+ await visit(resolved, 0);
90
+ }
91
+ }
92
+ if (files.length === 0) {
93
+ // No layout-imported CSS found: fall back to conventionally named sheets.
94
+ const all = await walk(targetDir, (rel) => rel.endsWith(".css"), 500);
95
+ const named = all.filter((file) => CSS_FALLBACK_NAME.test(path.basename(file)));
96
+ for (const file of (named.length > 0 ? named : all).slice(0, 4))
97
+ await visit(file, 0);
98
+ }
99
+ return files;
100
+ }
101
+ async function gatherContext(targetDir) {
102
+ let layout = null;
103
+ for (const candidate of ENTRY_FILE_CANDIDATES) {
104
+ const content = await readCapped(path.join(targetDir, candidate));
105
+ if (content !== null) {
106
+ layout = { path: candidate, content };
107
+ break;
108
+ }
109
+ }
110
+ let tailwindConfig = null;
111
+ for (const name of ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs", "tailwind.config.cjs"]) {
112
+ const content = await readCapped(path.join(targetDir, name));
113
+ if (content !== null) {
114
+ tailwindConfig = { path: name, content };
115
+ break;
116
+ }
117
+ }
118
+ return { layout, css: await collectCss(layout, targetDir), tailwindConfig };
119
+ }
120
+ function evidencePathsOf(context) {
121
+ return [
122
+ ...(context.layout ? [context.layout.path] : []),
123
+ ...context.css.map((file) => file.path),
124
+ ...(context.tailwindConfig ? [context.tailwindConfig.path] : []),
125
+ ];
126
+ }
127
+ /**
128
+ * The shadcn theme-variable vocabulary (ui.shadcn.com/docs/theming), mapped
129
+ * only where the shadcn semantic IS the Vendo slot semantic. Notably absent:
130
+ * shadcn's `--accent` (a hover wash, not the brand color — `--primary` is),
131
+ * `--muted` (a muted surface, not muted text), `--secondary`, `--popover`,
132
+ * `--input`, `--ring`, `--chart-*`, `--sidebar-*`. Each name is also accepted
133
+ * with the Tailwind-v4 `@theme` namespace prefix (`--color-primary`, ...).
134
+ */
135
+ const EXACT_COLOR_TOKENS = [
136
+ ["background", ["--background"]],
137
+ ["text", ["--foreground"]],
138
+ ["surface", ["--card"]],
139
+ ["accent", ["--primary"]],
140
+ ["accentText", ["--primary-foreground"]],
141
+ ["mutedText", ["--muted-foreground"]],
142
+ ["border", ["--border"]],
143
+ ["danger", ["--destructive"]],
144
+ ];
145
+ /** Non-color conventions: shadcn `--radius`; Tailwind v4 `--font-*` and
146
+ * `--text-base` namespaces; Vendo's own documented `--density`/`--motion`. */
147
+ const FONT_TOKENS = [
148
+ ["fontFamily", ["--font-sans"]],
149
+ ["headingFamily", ["--font-heading", "--font-display"]],
150
+ ];
151
+ function lastLightDecl(vars, names) {
152
+ for (const name of names) {
153
+ const spellings = [name, `--color-${name.slice(2)}`];
154
+ const hit = [...vars].reverse().find((v) => !v.darkScope && spellings.includes(v.name));
155
+ if (hit)
156
+ return hit;
157
+ }
158
+ return undefined;
159
+ }
160
+ function normalizeFontStack(value) {
161
+ // Quotes are optional CSS syntax around family names, not identity: "Outfit"
162
+ // and Outfit are the same family (unquoted multi-word names are valid too).
163
+ const stack = value.split(",")
164
+ .map((part) => part.trim().replace(/^(["'])(.*)\1$/, "$2").trim())
165
+ .filter(Boolean)
166
+ .join(", ");
167
+ return /(?:^|,\s*)(?:sans-serif|serif|monospace|cursive|fantasy)(?:\s*,|$)/i.test(stack)
168
+ ? stack
169
+ : `${stack}, sans-serif`;
170
+ }
171
+ /** Fully-resolved font stack: no var() refs, no CSS structural characters. */
172
+ function isSafeFontStack(value) {
173
+ return value.length > 0 && !value.includes("var(") && !/[{};\n]/.test(value);
174
+ }
175
+ function readExact(vars) {
176
+ const values = {};
177
+ const matched = {};
178
+ const put = (slot, decl, value) => {
179
+ if (decl === undefined || value === null)
180
+ return;
181
+ values[slot] = value;
182
+ matched[slot] = decl.name;
183
+ };
184
+ for (const [slot, names] of EXACT_COLOR_TOKENS) {
185
+ const decl = lastLightDecl(vars, names);
186
+ const resolved = decl === undefined ? null : resolveCssVarRefs(decl.value, vars);
187
+ put(slot, decl, resolved === null ? null : normalizeColor(resolved));
188
+ }
189
+ for (const [slot, names] of FONT_TOKENS) {
190
+ const decl = lastLightDecl(vars, names);
191
+ const resolved = decl === undefined ? null : resolveCssVarRefs(decl.value, vars);
192
+ put(slot, decl, resolved !== null && isSafeFontStack(resolved) ? normalizeFontStack(resolved) : null);
193
+ }
194
+ const radius = lastLightDecl(vars, ["--radius"]);
195
+ const radiusResolved = radius === undefined ? null : resolveCssVarRefs(radius.value, vars);
196
+ put("radius", radius, radiusResolved === null ? null : normalizeLength(radiusResolved));
197
+ const baseSize = lastLightDecl(vars, ["--font-size", "--text-base"]);
198
+ const baseResolved = baseSize === undefined ? null : resolveCssVarRefs(baseSize.value, vars);
199
+ put("baseSize", baseSize, baseResolved === null ? null : normalizeLength(baseResolved));
200
+ const density = lastLightDecl(vars, ["--density"]);
201
+ if (density && /^(?:compact|comfortable)$/.test(density.value.trim())) {
202
+ put("density", density, density.value.trim());
203
+ }
204
+ const motion = lastLightDecl(vars, ["--motion"]);
205
+ if (motion && /^(?:full|reduced)$/.test(motion.value.trim())) {
206
+ put("motion", motion, motion.value.trim());
207
+ }
208
+ return { values, matched };
209
+ }
210
+ // ---------------------------------------------------------------------------
211
+ // Staged-pass artifact schema — parsed by `runStagedExtraction`'s theme stage
212
+ // (../extract/stages.ts), merged back here by `applyThemeDraft`.
213
+ // ---------------------------------------------------------------------------
214
+ /** Brand-defining slots: any of these missing after the exact pass triggers
215
+ * the staged theme pass, and only doubt about these becomes an init
216
+ * question. The remaining slots (accentText, headingFamily, baseSize,
217
+ * density, motion) derive or default safely — accentText derives from the
218
+ * accent by WCAG contrast, headingFamily inherits fontFamily, and the rest
219
+ * have safe brand-neutral defaults — so they never trigger a call or a
220
+ * question on their own. */
221
+ export const BRAND_SLOTS = [
222
+ "accent", "background", "surface", "text", "mutedText",
223
+ "border", "danger", "radius", "fontFamily",
224
+ ];
225
+ const SLOT_KEYS = Object.keys(DEFAULT_THEME_SLOTS);
226
+ export const modelThemeSchema = z.object({
227
+ slots: z.object({
228
+ accent: z.string().optional(),
229
+ accentText: z.string().optional(),
230
+ background: z.string().optional(),
231
+ border: z.string().optional(),
232
+ danger: z.string().optional(),
233
+ surface: z.string().optional(),
234
+ text: z.string().optional(),
235
+ mutedText: z.string().optional(),
236
+ radius: z.string().optional(),
237
+ fontFamily: z.string().optional(),
238
+ headingFamily: z.string().optional(),
239
+ baseSize: z.string().optional(),
240
+ density: z.enum(["compact", "comfortable"]).optional(),
241
+ motion: z.enum(["full", "reduced"]).optional(),
242
+ }),
243
+ uncertain: z.array(z.object({ slot: z.string(), note: z.string() })).optional(),
244
+ });
245
+ /** Deterministic validation of a proposed slot value (model or human). */
246
+ export function validateSlotValue(slot, raw) {
247
+ const value = raw.trim();
248
+ switch (slot) {
249
+ case "radius":
250
+ case "baseSize":
251
+ return normalizeLength(value);
252
+ case "density":
253
+ return /^(?:compact|comfortable)$/.test(value) ? value : null;
254
+ case "motion":
255
+ return /^(?:full|reduced)$/.test(value) ? value : null;
256
+ case "fontFamily":
257
+ case "headingFamily":
258
+ return isSafeFontStack(value) ? normalizeFontStack(value) : null;
259
+ default:
260
+ return normalizeColor(value);
261
+ }
262
+ }
263
+ function assembleTheme(values, matchedExact, fromModel) {
264
+ const slots = { ...DEFAULT_THEME_SLOTS };
265
+ const matched = {};
266
+ const defaulted = [];
267
+ for (const slot of SLOT_KEYS) {
268
+ const exactValue = values[slot];
269
+ const modelValue = fromModel[slot];
270
+ if (exactValue !== undefined) {
271
+ slots[slot] = exactValue;
272
+ matched[slot] = matchedExact[slot];
273
+ }
274
+ else if (modelValue !== undefined) {
275
+ slots[slot] = modelValue;
276
+ matched[slot] = "(model)";
277
+ }
278
+ else if (slot === "accentText" && (values.accent !== undefined || fromModel.accent !== undefined)) {
279
+ slots.accentText = contrastingText(slots.accent);
280
+ matched[slot] = "(contrast) accent";
281
+ }
282
+ else if (slot === "headingFamily" && (values.fontFamily !== undefined || fromModel.fontFamily !== undefined)) {
283
+ slots.headingFamily = slots.fontFamily;
284
+ matched[slot] = "(inherit) fontFamily";
285
+ }
286
+ else {
287
+ defaulted.push(slot);
288
+ }
289
+ }
290
+ return { slots, matched, defaulted };
291
+ }
292
+ // ---------------------------------------------------------------------------
293
+ // Exact-only extraction
294
+ // ---------------------------------------------------------------------------
295
+ export async function extractTheme(targetDir) {
296
+ const context = await gatherContext(targetDir);
297
+ const vars = context.css.flatMap((file) => parseCssVars(file.content, file.path));
298
+ const exact = readExact(vars);
299
+ const needed = SLOT_KEYS.filter((slot) => exact.values[slot] === undefined);
300
+ const { slots, matched, defaulted } = assembleTheme(exact.values, exact.matched, {});
301
+ return {
302
+ slots,
303
+ matched,
304
+ defaulted,
305
+ uncertain: [],
306
+ usedModel: false,
307
+ errors: [],
308
+ hasDarkVariant: vars.some((v) => v.darkScope),
309
+ needed,
310
+ evidencePaths: evidencePathsOf(context),
311
+ };
312
+ }
313
+ /**
314
+ * Merges a parsed theme-stage artifact (`modelThemeSchema`) onto an
315
+ * exact-only `ThemeSummary` — the precedence law (kill-list §B2, Task 2):
316
+ * exact reads are never overwritten; only `needed` slots may be filled; every
317
+ * proposed value passes through `validateSlotValue` (invalid values are
318
+ * ignored, the slot stays defaulted/derived); a model-provided accentText
319
+ * stands; accentText re-derives by contrast only when the model filled
320
+ * accent but not accentText and accentText itself wasn't an exact read;
321
+ * headingFamily inherits fontFamily under the mirror condition.
322
+ */
323
+ export function applyThemeDraft(summary, draft) {
324
+ const neededSet = new Set(summary.needed);
325
+ const fromModel = {};
326
+ for (const slot of summary.needed) {
327
+ const raw = draft.slots[slot];
328
+ if (raw === undefined)
329
+ continue;
330
+ const value = validateSlotValue(slot, String(raw));
331
+ if (value !== null)
332
+ fromModel[slot] = value;
333
+ }
334
+ // Reconstruct the "already exact" values/provenance: everything NOT in
335
+ // `needed` was an exact read (or, for a re-applied summary, already
336
+ // settled) and must never be reconsidered here.
337
+ const exactValues = {};
338
+ const exactMatched = {};
339
+ for (const slot of SLOT_KEYS) {
340
+ if (!neededSet.has(slot)) {
341
+ exactValues[slot] = summary.slots[slot];
342
+ exactMatched[slot] = summary.matched[slot];
343
+ }
344
+ }
345
+ const { slots, matched, defaulted } = assembleTheme(exactValues, exactMatched, fromModel);
346
+ const usedModel = Object.keys(fromModel).length > 0;
347
+ const uncertain = (draft.uncertain ?? [])
348
+ .filter((entry) => BRAND_SLOTS.includes(entry.slot))
349
+ .filter((entry) => neededSet.has(entry.slot));
350
+ return {
351
+ ...summary,
352
+ slots,
353
+ matched,
354
+ defaulted,
355
+ uncertain,
356
+ usedModel,
357
+ };
358
+ }
@@ -0,0 +1,2 @@
1
+ /** Bounded, deterministic source walk that avoids generated dependency trees. */
2
+ export declare function walk(root: string, keep: (relativePath: string) => boolean, maxFiles?: number): Promise<string[]>;
@@ -0,0 +1,24 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ const SKIP_DIRECTORIES = new Set(["node_modules", "dist", ".next"]);
4
+ /** Bounded, deterministic source walk that avoids generated dependency trees. */
5
+ export async function walk(root, keep, maxFiles = 5_000) {
6
+ const files = [];
7
+ async function visit(directory) {
8
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
9
+ for (const entry of entries) {
10
+ if (files.length >= maxFiles)
11
+ return;
12
+ const full = path.join(directory, entry.name);
13
+ if (entry.isDirectory()) {
14
+ if (!entry.name.startsWith(".") && !SKIP_DIRECTORIES.has(entry.name))
15
+ await visit(full);
16
+ }
17
+ else if (keep(path.relative(root, full))) {
18
+ files.push(full);
19
+ }
20
+ }
21
+ }
22
+ await visit(root);
23
+ return files.sort();
24
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function main(argv: string[]): Promise<number>;
2
+ export declare function isCliEntrypoint(metaUrl: string, argv1: string | undefined): boolean;