@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.
- package/LICENSE +202 -0
- package/README.md +28 -0
- package/bin/vendo.mjs +4 -0
- package/dist/ai-sdk.d.ts +35 -0
- package/dist/ai-sdk.js +62 -0
- package/dist/auth-presets/auth-js.d.ts +14 -0
- package/dist/auth-presets/auth-js.js +91 -0
- package/dist/auth-presets/auth0.d.ts +23 -0
- package/dist/auth-presets/auth0.js +98 -0
- package/dist/auth-presets/clerk.d.ts +23 -0
- package/dist/auth-presets/clerk.js +65 -0
- package/dist/auth-presets/conformance.d.ts +42 -0
- package/dist/auth-presets/conformance.js +139 -0
- package/dist/auth-presets/identity.d.ts +56 -0
- package/dist/auth-presets/identity.js +152 -0
- package/dist/auth-presets/index.d.ts +14 -0
- package/dist/auth-presets/index.js +12 -0
- package/dist/auth-presets/jwt.d.ts +16 -0
- package/dist/auth-presets/jwt.js +54 -0
- package/dist/auth-presets/shared.d.ts +35 -0
- package/dist/auth-presets/shared.js +1 -0
- package/dist/auth-presets/supabase.d.ts +42 -0
- package/dist/auth-presets/supabase.js +229 -0
- package/dist/byo-approvals.d.ts +37 -0
- package/dist/byo-approvals.js +189 -0
- package/dist/capability-misses.d.ts +38 -0
- package/dist/capability-misses.js +172 -0
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.js +152 -0
- package/dist/cli/cloud/args.d.ts +3 -0
- package/dist/cli/cloud/args.js +32 -0
- package/dist/cli/cloud/auth.d.ts +17 -0
- package/dist/cli/cloud/auth.js +123 -0
- package/dist/cli/cloud/client.d.ts +28 -0
- package/dist/cli/cloud/client.js +133 -0
- package/dist/cli/cloud/command.d.ts +22 -0
- package/dist/cli/cloud/command.js +73 -0
- package/dist/cli/cloud/deploy.d.ts +26 -0
- package/dist/cli/cloud/deploy.js +187 -0
- package/dist/cli/cloud/device-login.d.ts +32 -0
- package/dist/cli/cloud/device-login.js +259 -0
- package/dist/cli/cloud/index.d.ts +5 -0
- package/dist/cli/cloud/index.js +59 -0
- package/dist/cli/cloud/keys.d.ts +2 -0
- package/dist/cli/cloud/keys.js +31 -0
- package/dist/cli/cloud/members.d.ts +3 -0
- package/dist/cli/cloud/members.js +25 -0
- package/dist/cli/cloud/output.d.ts +4 -0
- package/dist/cli/cloud/output.js +12 -0
- package/dist/cli/cloud/pending-claim.d.ts +29 -0
- package/dist/cli/cloud/pending-claim.js +38 -0
- package/dist/cli/cloud/read.d.ts +3 -0
- package/dist/cli/cloud/read.js +12 -0
- package/dist/cli/cloud/services.d.ts +2 -0
- package/dist/cli/cloud/services.js +9 -0
- package/dist/cli/cloud/session.d.ts +13 -0
- package/dist/cli/cloud/session.js +41 -0
- package/dist/cli/cloud-init.d.ts +60 -0
- package/dist/cli/cloud-init.js +149 -0
- package/dist/cli/dep-versions.d.ts +26 -0
- package/dist/cli/dep-versions.js +77 -0
- package/dist/cli/doctor-codes.d.ts +58 -0
- package/dist/cli/doctor-codes.js +60 -0
- package/dist/cli/doctor-live.d.ts +67 -0
- package/dist/cli/doctor-live.js +169 -0
- package/dist/cli/doctor.d.ts +52 -0
- package/dist/cli/doctor.js +467 -0
- package/dist/cli/eject.d.ts +29 -0
- package/dist/cli/eject.js +141 -0
- package/dist/cli/extract/apply.d.ts +26 -0
- package/dist/cli/extract/apply.js +123 -0
- package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
- package/dist/cli/extract/claude-cli-harness.js +132 -0
- package/dist/cli/extract/claude-harness.d.ts +14 -0
- package/dist/cli/extract/claude-harness.js +127 -0
- package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
- package/dist/cli/extract/codex-cli-harness.js +154 -0
- package/dist/cli/extract/delegate.d.ts +21 -0
- package/dist/cli/extract/delegate.js +77 -0
- package/dist/cli/extract/extraction.d.ts +65 -0
- package/dist/cli/extract/extraction.js +300 -0
- package/dist/cli/extract/gateway-fuel.d.ts +69 -0
- package/dist/cli/extract/gateway-fuel.js +69 -0
- package/dist/cli/extract/harness.d.ts +114 -0
- package/dist/cli/extract/harness.js +84 -0
- package/dist/cli/extract/index.d.ts +9 -0
- package/dist/cli/extract/index.js +9 -0
- package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
- package/dist/cli/extract/npx-engine-harness.js +225 -0
- package/dist/cli/extract/stages.d.ts +149 -0
- package/dist/cli/extract/stages.js +398 -0
- package/dist/cli/framework.d.ts +8 -0
- package/dist/cli/framework.js +36 -0
- package/dist/cli/init-auth.d.ts +67 -0
- package/dist/cli/init-auth.js +142 -0
- package/dist/cli/init-scaffolds.d.ts +28 -0
- package/dist/cli/init-scaffolds.js +235 -0
- package/dist/cli/init.d.ts +115 -0
- package/dist/cli/init.js +1008 -0
- package/dist/cli/mcp/index.d.ts +7 -0
- package/dist/cli/mcp/index.js +59 -0
- package/dist/cli/mcp/registry.d.ts +9 -0
- package/dist/cli/mcp/registry.js +117 -0
- package/dist/cli/mcp/server-json.d.ts +12 -0
- package/dist/cli/mcp/server-json.js +65 -0
- package/dist/cli/mcp/server.schema.json +574 -0
- package/dist/cli/mcp/verify-domain.d.ts +11 -0
- package/dist/cli/mcp/verify-domain.js +48 -0
- package/dist/cli/playground/app/embed-entry.d.ts +20 -0
- package/dist/cli/playground/app/embed-entry.js +46 -0
- package/dist/cli/playground/app/fake-client.d.ts +3 -0
- package/dist/cli/playground/app/fake-client.js +215 -0
- package/dist/cli/playground/app/fixtures.d.ts +47 -0
- package/dist/cli/playground/app/fixtures.js +472 -0
- package/dist/cli/playground/app/main.d.ts +1 -0
- package/dist/cli/playground/app/main.js +108 -0
- package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
- package/dist/cli/playground/app/scenario-mount.js +48 -0
- package/dist/cli/playground/app/scenarios.d.ts +25 -0
- package/dist/cli/playground/app/scenarios.js +152 -0
- package/dist/cli/playground/app/theme-editor.d.ts +13 -0
- package/dist/cli/playground/app/theme-editor.js +149 -0
- package/dist/cli/playground/app/theme-state.d.ts +29 -0
- package/dist/cli/playground/app/theme-state.js +151 -0
- package/dist/cli/playground/bundle.gen.d.ts +2 -0
- package/dist/cli/playground/bundle.gen.js +2 -0
- package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
- package/dist/cli/playground/embed-bundle.gen.js +2 -0
- package/dist/cli/playground.d.ts +33 -0
- package/dist/cli/playground.js +120 -0
- package/dist/cli/pretty.d.ts +48 -0
- package/dist/cli/pretty.js +330 -0
- package/dist/cli/refine.d.ts +43 -0
- package/dist/cli/refine.js +181 -0
- package/dist/cli/semantics.d.ts +31 -0
- package/dist/cli/semantics.js +122 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +184 -0
- package/dist/cli/sync.d.ts +36 -0
- package/dist/cli/sync.js +202 -0
- package/dist/cli/theme/color.d.ts +15 -0
- package/dist/cli/theme/color.js +188 -0
- package/dist/cli/theme/css-vars.d.ts +13 -0
- package/dist/cli/theme/css-vars.js +40 -0
- package/dist/cli/theme/entry-candidates.d.ts +5 -0
- package/dist/cli/theme/entry-candidates.js +22 -0
- package/dist/cli/theme/extract-theme.d.ts +184 -0
- package/dist/cli/theme/extract-theme.js +358 -0
- package/dist/cli/theme/walk.d.ts +2 -0
- package/dist/cli/theme/walk.js +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +333 -0
- package/dist/cloud-apps.d.ts +17 -0
- package/dist/cloud-apps.js +46 -0
- package/dist/cloud-console.d.ts +32 -0
- package/dist/cloud-console.js +72 -0
- package/dist/cloud-tools.d.ts +23 -0
- package/dist/cloud-tools.js +196 -0
- package/dist/connections.d.ts +71 -0
- package/dist/connections.js +192 -0
- package/dist/deployment-identity.d.ts +12 -0
- package/dist/deployment-identity.js +69 -0
- package/dist/dev-creds/model.d.ts +65 -0
- package/dist/dev-creds/model.js +159 -0
- package/dist/dev-creds/resolve.d.ts +36 -0
- package/dist/dev-creds/resolve.js +57 -0
- package/dist/hosted-store.d.ts +52 -0
- package/dist/hosted-store.js +322 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -0
- package/dist/mastra.d.ts +46 -0
- package/dist/mastra.js +88 -0
- package/dist/react.d.ts +10 -0
- package/dist/react.js +32 -0
- package/dist/refine.d.ts +244 -0
- package/dist/refine.js +599 -0
- package/dist/remixable.d.ts +18 -0
- package/dist/remixable.js +42 -0
- package/dist/runtime-capture.d.ts +18 -0
- package/dist/runtime-capture.js +111 -0
- package/dist/sandbox-wire.d.ts +101 -0
- package/dist/sandbox-wire.js +101 -0
- package/dist/sandbox.d.ts +38 -0
- package/dist/sandbox.js +389 -0
- package/dist/server.d.ts +216 -0
- package/dist/server.js +1320 -0
- package/dist/sync-impact.d.ts +14 -0
- package/dist/sync-impact.js +75 -0
- package/dist/turn-liveness.d.ts +33 -0
- package/dist/turn-liveness.js +105 -0
- package/dist/wire/approvals.d.ts +10 -0
- package/dist/wire/approvals.js +59 -0
- package/dist/wire/apps.d.ts +5 -0
- package/dist/wire/apps.js +155 -0
- package/dist/wire/automations.d.ts +5 -0
- package/dist/wire/automations.js +57 -0
- package/dist/wire/box.d.ts +3 -0
- package/dist/wire/box.js +268 -0
- package/dist/wire/connections.d.ts +5 -0
- package/dist/wire/connections.js +47 -0
- package/dist/wire/context.d.ts +22 -0
- package/dist/wire/context.js +190 -0
- package/dist/wire/doctor.d.ts +19 -0
- package/dist/wire/doctor.js +116 -0
- package/dist/wire/misc.d.ts +18 -0
- package/dist/wire/misc.js +227 -0
- package/dist/wire/shared.d.ts +158 -0
- package/dist/wire/shared.js +134 -0
- package/dist/wire/threads.d.ts +3 -0
- package/dist/wire/threads.js +68 -0
- package/package.json +151 -0
- 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,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