@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,202 @@
1
+ import { join, resolve } from "node:path";
2
+ import { vendoSync } from "@vendoai/actions";
3
+ import { pushSyncReport } from "./cloud/services.js";
4
+ import { syncSemantics } from "./semantics.js";
5
+ import { consoleOutput, withCommandRun } from "./shared.js";
6
+ function impactResponse(value) {
7
+ if (typeof value !== "object" || value === null || !Array.isArray(value.impact)) {
8
+ throw new Error("invalid sync impact response");
9
+ }
10
+ const impact = value.impact;
11
+ for (const entry of impact) {
12
+ if (typeof entry !== "object" || entry === null)
13
+ throw new Error("invalid sync impact response");
14
+ const candidate = entry;
15
+ if (typeof candidate.tool !== "string" || !Array.isArray(candidate.apps)
16
+ || !Array.isArray(candidate.automations) || typeof candidate.grants !== "number") {
17
+ throw new Error("invalid sync impact response");
18
+ }
19
+ }
20
+ return impact;
21
+ }
22
+ function printImpact(output, impact) {
23
+ for (const entry of impact) {
24
+ const categories = [
25
+ [entry.automations.length, "automation"],
26
+ [entry.apps.length, "app"],
27
+ [entry.grants, "grant"],
28
+ ];
29
+ const references = categories
30
+ .filter(([count]) => count > 0)
31
+ .map(([count, label]) => `${count} ${label}${count === 1 ? "" : "s"}`);
32
+ output.log(references.length === 0
33
+ ? `impact: ${entry.tool} no saved references`
34
+ : `impact: ${entry.tool} breaks ${references.join(", ")}`);
35
+ }
36
+ }
37
+ function nonzero(entry) {
38
+ return entry.apps.length > 0 || entry.automations.length > 0 || entry.grants > 0;
39
+ }
40
+ /** 04-actions §1 / 09-vendo §5 — fail-soft extraction, strict CI gate. */
41
+ export async function runSync(options) {
42
+ return withCommandRun({
43
+ command: "sync",
44
+ root: options.targetDir,
45
+ ...(options.telemetry === undefined ? {} : { telemetry: options.telemetry }),
46
+ }, () => sync(options));
47
+ }
48
+ async function sync(options) {
49
+ const output = options.output ?? consoleOutput;
50
+ const json = options.json === true;
51
+ // In --json mode, human lines that duplicate report fields are dropped and
52
+ // CLI-level events collect into `notes`; stdout carries exactly one object.
53
+ const notes = [];
54
+ const note = (message) => { if (json)
55
+ notes.push(message);
56
+ else
57
+ output.log(message); };
58
+ const noteError = (message) => { if (json)
59
+ notes.push(message);
60
+ else
61
+ output.error(message); };
62
+ try {
63
+ const root = resolve(options.targetDir);
64
+ const report = await (options.sync ?? vendoSync)({
65
+ root,
66
+ out: join(root, ".vendo"),
67
+ // The CLI needs the report to compute exit 2 vs 3; it applies strictness below.
68
+ strict: false,
69
+ });
70
+ if (!json) {
71
+ for (const warning of report.warnings)
72
+ output.error(`warning: ${warning}`);
73
+ output.log(`tools: +${report.tools.added.length} -${report.tools.removed.length} ~${report.tools.changed.length}`);
74
+ output.log(`pins: ${report.pins.captured.length} captured, ${report.pins.drifted.length} drifted`);
75
+ output.log(`catalog.json: ${report.catalog.discovered} discovered, ${report.catalog.registered} registered`);
76
+ if (report.pins.drifted.length > 0) {
77
+ // 06-apps §8 — drift never auto-rebases: the fork's owner decides.
78
+ output.log(`drifted: ${report.pins.drifted.join(", ")} — existing forks stay on the old capture until each owner rebases (POST /apps/:id/rebase-pin or the vendo_apps_rebase_pin agent tool)`);
79
+ }
80
+ }
81
+ // Unresolved slots fail the run regardless of --strict (silent remix skips
82
+ // are eliminated), but impact analysis, the report push, and the breaking
83
+ // gate below still execute so the most severe exit code wins. In --json
84
+ // mode the human lines are dropped: the pins ride in report.unresolvedPins.
85
+ let unresolvedExit = 0;
86
+ if (report.unresolvedPins.length > 0) {
87
+ if (!json) {
88
+ output.error("unresolved remixable slots:");
89
+ for (const pin of report.unresolvedPins) {
90
+ output.error(` ${pin.slot} [${pin.reason}]: ${pin.hint}`);
91
+ }
92
+ }
93
+ unresolvedExit = 2;
94
+ }
95
+ const wireUrl = (options.url ?? process.env.VENDO_URL ?? "http://localhost:3000/api/vendo").replace(/\/+$/, "");
96
+ // W3 — field semantics + domain manifest into .vendo/semantics.json
97
+ // (one-time inference via the dev server; host edits preserved).
98
+ // Fail-soft like everything else in sync.
99
+ try {
100
+ await syncSemantics({
101
+ vendoDir: join(root, ".vendo"),
102
+ url: wireUrl,
103
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
104
+ note,
105
+ });
106
+ }
107
+ catch (error) {
108
+ note(`semantics sync failed soft: ${error instanceof Error ? error.message : "unknown error"}`);
109
+ }
110
+ const tools = [...new Set([
111
+ ...report.breaking.map((breaking) => breaking.tool),
112
+ ...report.tools.changed,
113
+ ])];
114
+ let impact;
115
+ if (tools.length > 0) {
116
+ const impactUrl = wireUrl;
117
+ try {
118
+ const response = await (options.fetchImpl ?? fetch)(`${impactUrl}/sync/impact`, {
119
+ method: "POST",
120
+ headers: { accept: "application/json", "content-type": "application/json" },
121
+ body: JSON.stringify({ tools }),
122
+ });
123
+ if (!response.ok)
124
+ throw new Error(`sync impact returned ${response.status}`);
125
+ impact = impactResponse(await response.json());
126
+ if (!json)
127
+ printImpact(output, impact);
128
+ }
129
+ catch {
130
+ note(`impact unknown — dev server not reachable at ${impactUrl}`);
131
+ }
132
+ }
133
+ if (options.report === true) {
134
+ const apiKey = options.apiKey ?? process.env.VENDO_API_KEY;
135
+ if (!apiKey) {
136
+ noteError("--report requires VENDO_API_KEY or --key");
137
+ }
138
+ else {
139
+ const payload = {
140
+ report,
141
+ ...(impact === undefined ? {} : { impact }),
142
+ at: new Date().toISOString(),
143
+ };
144
+ try {
145
+ if (options.push !== undefined)
146
+ await options.push(payload);
147
+ else
148
+ await pushSyncReport(payload, {
149
+ apiKey,
150
+ ...(options.apiUrl === undefined ? {} : { apiUrl: options.apiUrl }),
151
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
152
+ });
153
+ }
154
+ catch (error) {
155
+ noteError(`warning: failed to push sync report: ${error instanceof Error ? error.message : "unknown error"}`);
156
+ }
157
+ }
158
+ }
159
+ // Unresolved slots set the floor; the strict breaking gate overrides with
160
+ // its own (equal-or-worse) code, so the most severe exit wins.
161
+ let exitCode = unresolvedExit;
162
+ if (options.strict === true && report.breaking.length > 0) {
163
+ if (!json)
164
+ for (const breaking of report.breaking)
165
+ output.error(`breaking: ${breaking.tool} ${breaking.change}`);
166
+ const breakingTools = new Set(report.breaking.map((breaking) => breaking.tool));
167
+ exitCode = impact?.some((entry) => breakingTools.has(entry.tool) && nonzero(entry)) === true ? 3 : 2;
168
+ }
169
+ if (json) {
170
+ const result = {
171
+ ok: exitCode === 0,
172
+ exitCode,
173
+ report,
174
+ // Nothing changed → nothing could be impacted; changes without a
175
+ // reachable dev server → unknown, surfaced as null plus a note.
176
+ impact: impact ?? (tools.length === 0 ? [] : null),
177
+ notes,
178
+ };
179
+ output.log(JSON.stringify(result, null, 2));
180
+ }
181
+ return exitCode;
182
+ }
183
+ catch (error) {
184
+ const message = `sync failed soft: ${error instanceof Error ? error.message : "unknown error"}`;
185
+ const exitCode = options.strict === true ? 2 : 0;
186
+ if (json) {
187
+ const result = {
188
+ ok: exitCode === 0,
189
+ exitCode,
190
+ report: { tools: { added: [], removed: [], changed: [] }, breaking: [], pins: { captured: [], drifted: [] }, unresolvedPins: [], catalog: { discovered: 0, registered: 0 }, warnings: [] },
191
+ impact: null,
192
+ notes,
193
+ error: message,
194
+ };
195
+ output.log(JSON.stringify(result, null, 2));
196
+ }
197
+ else {
198
+ output.error(`warning: ${message}`);
199
+ }
200
+ return exitCode;
201
+ }
202
+ }
@@ -0,0 +1,15 @@
1
+ import type { CssVarDecl } from "./css-vars.js";
2
+ /** Any supported CSS color form to lowercase 6-digit hex; null when not one.
3
+ * A trailing `!important` is declaration noise, not color — stripped. */
4
+ export declare function normalizeColor(value: string): string | null;
5
+ /**
6
+ * Resolve `var(--x)` / `var(--x, fallback)` references against the collected
7
+ * declarations (bounded depth). Returns null when a reference cannot be
8
+ * resolved — theme.json carries fully-resolved primitives only (frozen theme
9
+ * contract), so an unresolved reference means the slot is not exactly readable.
10
+ */
11
+ export declare function resolveCssVarRefs(value: string, vars: CssVarDecl[], depth?: number): string | null;
12
+ /** px/rem length to canonical px; null for any other unit or expression. */
13
+ export declare function normalizeLength(value: string): string | null;
14
+ /** Pick black or white by the larger WCAG contrast ratio against the accent. */
15
+ export declare function contrastingText(accent: string): "#000000" | "#ffffff";
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Deterministic CSS color/length normalization for exact token reads. Every
3
+ * conversion here is published spec math (CSS Color 4), not inference: a
4
+ * conventional token's declared value is turned into the concrete hex/px the
5
+ * frozen VendoTheme contract carries. Values with real transparency are
6
+ * rejected (theme slots are opaque paints), as is anything unparseable —
7
+ * an unreadable value is treated as absent, never guessed at.
8
+ */
9
+ const HEX = /^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
10
+ function normalizeHex(value) {
11
+ const trimmed = value.trim();
12
+ if (!HEX.test(trimmed))
13
+ return null;
14
+ let h = trimmed.slice(1);
15
+ if (h.length === 3 || h.length === 4)
16
+ h = [...h].map((c) => c + c).join("");
17
+ if (h.length === 8) {
18
+ const alpha = parseInt(h.slice(6, 8), 16);
19
+ if (alpha !== 255)
20
+ return null;
21
+ h = h.slice(0, 6);
22
+ }
23
+ return `#${h.slice(0, 6).toLowerCase()}`;
24
+ }
25
+ function clamp01(value) {
26
+ return Math.min(1, Math.max(0, value));
27
+ }
28
+ function byteToHex(value) {
29
+ return Math.round(clamp01(value) * 255).toString(16).padStart(2, "0");
30
+ }
31
+ function rgbToHex(r, g, b) {
32
+ return `#${byteToHex(r)}${byteToHex(g)}${byteToHex(b)}`;
33
+ }
34
+ function parseAlpha(value) {
35
+ if (!value)
36
+ return 1;
37
+ const trimmed = value.trim();
38
+ if (trimmed.endsWith("%"))
39
+ return Number(trimmed.slice(0, -1)) / 100;
40
+ return Number(trimmed);
41
+ }
42
+ function hslToHex(hue, saturation, lightness) {
43
+ const h = (((hue % 360) + 360) % 360) / 360;
44
+ const s = clamp01(saturation / 100);
45
+ const l = clamp01(lightness / 100);
46
+ if (s === 0)
47
+ return rgbToHex(l, l, l);
48
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
49
+ const p = 2 * l - q;
50
+ const channel = (offset) => {
51
+ let t = h + offset;
52
+ if (t < 0)
53
+ t += 1;
54
+ if (t > 1)
55
+ t -= 1;
56
+ if (t < 1 / 6)
57
+ return p + (q - p) * 6 * t;
58
+ if (t < 1 / 2)
59
+ return q;
60
+ if (t < 2 / 3)
61
+ return p + (q - p) * (2 / 3 - t) * 6;
62
+ return p;
63
+ };
64
+ return rgbToHex(channel(1 / 3), channel(0), channel(-1 / 3));
65
+ }
66
+ /** `hsl()`/`hsla()` plus the bare shadcn triplet form (`222.2 47.4% 11.2%`). */
67
+ function parseHsl(value) {
68
+ const trimmed = value.trim();
69
+ const fn = trimmed.match(/^hsla?\(([\s\S]+)\)$/i);
70
+ const body = fn?.[1]?.trim() ?? trimmed;
71
+ const slashParts = body.split("/");
72
+ if (slashParts.length > 2)
73
+ return null;
74
+ const rawParts = slashParts[0].includes(",")
75
+ ? slashParts[0].split(",").map((part) => part.trim())
76
+ : slashParts[0].trim().split(/\s+/);
77
+ const alpha = parseAlpha(slashParts[1] ?? (rawParts.length === 4 ? rawParts[3] : undefined));
78
+ if (!Number.isFinite(alpha) || alpha < 0.999)
79
+ return null;
80
+ const parts = rawParts.slice(0, 3);
81
+ if (parts.length !== 3 || !parts[1]?.endsWith("%") || !parts[2]?.endsWith("%"))
82
+ return null;
83
+ const hue = Number(parts[0]);
84
+ const saturation = Number(parts[1].slice(0, -1));
85
+ const lightness = Number(parts[2].slice(0, -1));
86
+ if (![hue, saturation, lightness].every(Number.isFinite))
87
+ return null;
88
+ return hslToHex(hue, saturation, lightness);
89
+ }
90
+ /** Bare space-separated `R G B` byte triplet (Tailwind `rgb(var(--x)) `style). */
91
+ function parseRgbTriplet(value) {
92
+ const body = value.trim().replace(/^rgba?\(([\s\S]+)\)$/i, "$1");
93
+ const slashParts = body.split("/");
94
+ if (slashParts.length > 2)
95
+ return null;
96
+ const alpha = parseAlpha(slashParts[1]);
97
+ if (!Number.isFinite(alpha) || alpha < 0.999)
98
+ return null;
99
+ const parts = slashParts[0].trim().split(/[\s,]+/);
100
+ if (parts.length !== 3 || parts.some((part) => part.endsWith("%")))
101
+ return null;
102
+ const channels = parts.map((part) => Number(part));
103
+ if (channels.some((channel) => !Number.isFinite(channel) || channel < 0 || channel > 255))
104
+ return null;
105
+ return `#${channels.map((channel) => Math.round(channel).toString(16).padStart(2, "0")).join("")}`;
106
+ }
107
+ function linearToSrgb(value) {
108
+ const v = clamp01(value);
109
+ return v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
110
+ }
111
+ /** CSS Color 4 §"OKLCH" — the spec's published OKLab→sRGB constants. */
112
+ function parseOklch(value) {
113
+ const trimmed = value.trim();
114
+ const match = trimmed.match(/^oklch\(\s*([+-]?(?:\d+|\d*\.\d+)%?)\s+([+-]?(?:\d+|\d*\.\d+))\s+([+-]?(?:\d+|\d*\.\d+)(?:deg)?)\s*(?:\/\s*([^)]+))?\)$/i);
115
+ if (!match)
116
+ return null;
117
+ const alpha = parseAlpha(match[4]);
118
+ if (!Number.isFinite(alpha) || alpha < 0.999)
119
+ return null;
120
+ const lRaw = match[1];
121
+ const lightness = lRaw.endsWith("%") ? Number(lRaw.slice(0, -1)) / 100 : Number(lRaw);
122
+ const chroma = Number(match[2]);
123
+ const hue = Number(match[3].replace(/deg$/i, ""));
124
+ if (![lightness, chroma, hue].every(Number.isFinite))
125
+ return null;
126
+ const a = chroma * Math.cos((hue * Math.PI) / 180);
127
+ const b = chroma * Math.sin((hue * Math.PI) / 180);
128
+ const lPrime = lightness + 0.3963377774 * a + 0.2158037573 * b;
129
+ const mPrime = lightness - 0.1055613458 * a - 0.0638541728 * b;
130
+ const sPrime = lightness - 0.0894841775 * a - 1.291485548 * b;
131
+ const l = lPrime ** 3;
132
+ const m = mPrime ** 3;
133
+ const s = sPrime ** 3;
134
+ return rgbToHex(linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s), linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s), linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s));
135
+ }
136
+ /** Any supported CSS color form to lowercase 6-digit hex; null when not one.
137
+ * A trailing `!important` is declaration noise, not color — stripped. */
138
+ export function normalizeColor(value) {
139
+ const bare = value.replace(/\s*!important\s*$/i, "");
140
+ return normalizeHex(bare) ?? parseRgbTriplet(bare) ?? parseHsl(bare) ?? parseOklch(bare);
141
+ }
142
+ /**
143
+ * Resolve `var(--x)` / `var(--x, fallback)` references against the collected
144
+ * declarations (bounded depth). Returns null when a reference cannot be
145
+ * resolved — theme.json carries fully-resolved primitives only (frozen theme
146
+ * contract), so an unresolved reference means the slot is not exactly readable.
147
+ */
148
+ export function resolveCssVarRefs(value, vars, depth = 6) {
149
+ if (!value.includes("var("))
150
+ return value;
151
+ if (depth <= 0)
152
+ return null;
153
+ const byName = new Map(vars.filter((v) => !v.darkScope).map((v) => [v.name, v.value]));
154
+ const substituted = value.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]+))?\)/g, (_m, name, fallback) => {
155
+ return byName.get(name) ?? fallback?.trim() ?? "var()";
156
+ });
157
+ if (substituted.includes("var()"))
158
+ return null; // unresolvable reference
159
+ return resolveCssVarRefs(substituted, vars, depth - 1);
160
+ }
161
+ /** px/rem length to canonical px; null for any other unit or expression. */
162
+ export function normalizeLength(value) {
163
+ const trimmed = value.replace(/\s*!important\s*$/i, "").trim();
164
+ const px = trimmed.match(/^((?:\d+|\d*\.\d+))px$/);
165
+ if (px)
166
+ return `${Number(px[1])}px`;
167
+ const rem = trimmed.match(/^((?:\d+|\d*\.\d+))rem$/);
168
+ if (rem)
169
+ return `${Number(rem[1]) * 16}px`;
170
+ return null;
171
+ }
172
+ function relativeLuminance(hex) {
173
+ const normalized = normalizeHex(hex);
174
+ if (!normalized)
175
+ return 0;
176
+ const channels = [1, 3, 5].map((index) => parseInt(normalized.slice(index, index + 2), 16) / 255);
177
+ const [r, g, b] = channels.map((channel) => channel <= 0.04045
178
+ ? channel / 12.92
179
+ : ((channel + 0.055) / 1.055) ** 2.4);
180
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
181
+ }
182
+ /** Pick black or white by the larger WCAG contrast ratio against the accent. */
183
+ export function contrastingText(accent) {
184
+ const luminance = relativeLuminance(accent);
185
+ const blackContrast = (luminance + 0.05) / 0.05;
186
+ const whiteContrast = 1.05 / (luminance + 0.05);
187
+ return blackContrast >= whiteContrast ? "#000000" : "#ffffff";
188
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Minimal CSS custom-property declaration scanner. Not a full CSS parser —
3
+ * tracks brace depth and whether the enclosing block/at-rule looks dark-scoped
4
+ * (`.dark`, `[data-theme="dark"]`, `@media (prefers-color-scheme: dark)`).
5
+ * Good enough for design-token sheets; anything it misses is hand-editable output.
6
+ */
7
+ export interface CssVarDecl {
8
+ name: string;
9
+ value: string;
10
+ file: string;
11
+ darkScope: boolean;
12
+ }
13
+ export declare function parseCssVars(css: string, file: string): CssVarDecl[];
@@ -0,0 +1,40 @@
1
+ const DARK_SELECTOR = /(\.dark\b|\[data-theme=["']?dark["']?\]|prefers-color-scheme:\s*dark)/;
2
+ export function parseCssVars(css, file) {
3
+ const out = [];
4
+ // Strip comments first.
5
+ const src = css.replace(/\/\*[\s\S]*?\*\//g, "");
6
+ // Stack of "is this block dark-scoped" flags.
7
+ const darkStack = [];
8
+ let selectorBuf = "";
9
+ // A declaration ends at `;`, at the closing `}` of its block (trailing
10
+ // semicolons are optional in CSS and absent in minified sheets), or at
11
+ // end-of-input.
12
+ const flushDecl = () => {
13
+ const decl = selectorBuf.trim();
14
+ const m = decl.match(/^(--[\w-]+)\s*:\s*(.+)$/s);
15
+ if (m && m[1] && m[2]) {
16
+ out.push({ name: m[1], value: m[2].trim(), file, darkScope: darkStack.some(Boolean) });
17
+ }
18
+ selectorBuf = "";
19
+ };
20
+ for (let i = 0; i < src.length; i++) {
21
+ const ch = src[i];
22
+ if (ch === "{") {
23
+ const parentDark = darkStack.some(Boolean);
24
+ darkStack.push(parentDark || DARK_SELECTOR.test(selectorBuf));
25
+ selectorBuf = "";
26
+ }
27
+ else if (ch === "}") {
28
+ flushDecl();
29
+ darkStack.pop();
30
+ }
31
+ else if (ch === ";") {
32
+ flushDecl();
33
+ }
34
+ else {
35
+ selectorBuf += ch;
36
+ }
37
+ }
38
+ flushDecl();
39
+ return out;
40
+ }
@@ -0,0 +1,5 @@
1
+ /** Conventional Next.js entry-file paths (app-router layouts + pages-router
2
+ * `_app`), relative to a target repo's root. extract-theme.ts reads the
3
+ * first hit as the root layout: the anchor for the CSS import graph and part
4
+ * of the LLM pass's evidence (next/font imports live here). */
5
+ export declare const ENTRY_FILE_CANDIDATES: string[];
@@ -0,0 +1,22 @@
1
+ /** Conventional Next.js entry-file paths (app-router layouts + pages-router
2
+ * `_app`), relative to a target repo's root. extract-theme.ts reads the
3
+ * first hit as the root layout: the anchor for the CSS import graph and part
4
+ * of the LLM pass's evidence (next/font imports live here). */
5
+ export const ENTRY_FILE_CANDIDATES = [
6
+ "app/layout.tsx",
7
+ "app/layout.jsx",
8
+ "app/layout.ts",
9
+ "app/layout.js",
10
+ "src/app/layout.tsx",
11
+ "src/app/layout.jsx",
12
+ "src/app/layout.ts",
13
+ "src/app/layout.js",
14
+ "pages/_app.tsx",
15
+ "pages/_app.jsx",
16
+ "pages/_app.ts",
17
+ "pages/_app.js",
18
+ "src/pages/_app.tsx",
19
+ "src/pages/_app.jsx",
20
+ "src/pages/_app.ts",
21
+ "src/pages/_app.js",
22
+ ];
@@ -0,0 +1,184 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Theme extraction, exact-then-staged (kill-list §B2, re-derived Task 2/4):
4
+ *
5
+ * 1. Allowlist fast-path — conventional shadcn/Tailwind tokens are read
6
+ * EXACTLY (`--primary`, `--background`, `--font-sans`, ... and their
7
+ * Tailwind-v4 `--color-*` spellings). No name scoring, no inference. This
8
+ * file does ONLY this: `extractTheme` is fully deterministic — no model
9
+ * call, no network, no credential.
10
+ * 2. Whatever the allowlist leaves unfilled rides init's consent-gated AI
11
+ * pass (`runAiExtraction` → `runStagedExtraction`'s theme stage, in
12
+ * `../extract/stages.ts`) — the SAME harness seam as the tool-description
13
+ * polish, over Read/Glob/Grep instead of a fixed evidence-file set.
14
+ * `applyThemeDraft` below merges that stage's parsed artifact back onto
15
+ * this file's exact-only summary.
16
+ * 3. Anything neither path fills falls back to neutral defaults and is
17
+ * reported as defaulted — a miss is visible, never a silent wrong brand.
18
+ *
19
+ * theme.json stays the editable source of truth; init shows the palette for
20
+ * a one-glance confirm and asks only about model-flagged uncertainty.
21
+ */
22
+ export interface ThemeSlotValues {
23
+ accent: string;
24
+ accentText: string;
25
+ background: string;
26
+ border: string;
27
+ danger: string;
28
+ surface: string;
29
+ text: string;
30
+ mutedText: string;
31
+ radius: string;
32
+ fontFamily: string;
33
+ headingFamily: string;
34
+ baseSize: string;
35
+ density: "compact" | "comfortable";
36
+ motion: "full" | "reduced";
37
+ }
38
+ export interface ThemeUncertainty {
39
+ slot: keyof ThemeSlotValues;
40
+ note: string;
41
+ }
42
+ export interface ThemeSummary {
43
+ /** Fully resolved values used to assemble the frozen VendoTheme contract. */
44
+ slots: ThemeSlotValues;
45
+ /** slot -> provenance: the exact token name, "(model)", or a derivation. */
46
+ matched: Record<string, string>;
47
+ /** Slots that fell back to neutral defaults (reported, never silent). */
48
+ defaulted: string[];
49
+ /** Model-flagged genuine uncertainty — init's only theme question trigger. */
50
+ uncertain: ThemeUncertainty[];
51
+ usedModel: boolean;
52
+ errors: string[];
53
+ hasDarkVariant: boolean;
54
+ /** Slots the exact allowlist pass did not read exactly — the staged theme
55
+ * pass's `needed` input, and the only slots `applyThemeDraft` may fill. */
56
+ needed: SlotKey[];
57
+ /** The CSS/layout/tailwind-config paths the context gatherer collected,
58
+ * repo-relative — seeded as evidence-path hints for the staged pass. */
59
+ evidencePaths: string[];
60
+ }
61
+ type SlotKey = keyof ThemeSlotValues;
62
+ /** Brand-defining slots: any of these missing after the exact pass triggers
63
+ * the staged theme pass, and only doubt about these becomes an init
64
+ * question. The remaining slots (accentText, headingFamily, baseSize,
65
+ * density, motion) derive or default safely — accentText derives from the
66
+ * accent by WCAG contrast, headingFamily inherits fontFamily, and the rest
67
+ * have safe brand-neutral defaults — so they never trigger a call or a
68
+ * question on their own. */
69
+ export declare const BRAND_SLOTS: readonly SlotKey[];
70
+ export declare const modelThemeSchema: z.ZodObject<{
71
+ slots: z.ZodObject<{
72
+ accent: z.ZodOptional<z.ZodString>;
73
+ accentText: z.ZodOptional<z.ZodString>;
74
+ background: z.ZodOptional<z.ZodString>;
75
+ border: z.ZodOptional<z.ZodString>;
76
+ danger: z.ZodOptional<z.ZodString>;
77
+ surface: z.ZodOptional<z.ZodString>;
78
+ text: z.ZodOptional<z.ZodString>;
79
+ mutedText: z.ZodOptional<z.ZodString>;
80
+ radius: z.ZodOptional<z.ZodString>;
81
+ fontFamily: z.ZodOptional<z.ZodString>;
82
+ headingFamily: z.ZodOptional<z.ZodString>;
83
+ baseSize: z.ZodOptional<z.ZodString>;
84
+ density: z.ZodOptional<z.ZodEnum<["compact", "comfortable"]>>;
85
+ motion: z.ZodOptional<z.ZodEnum<["full", "reduced"]>>;
86
+ }, "strip", z.ZodTypeAny, {
87
+ text?: string | undefined;
88
+ radius?: string | undefined;
89
+ density?: "compact" | "comfortable" | undefined;
90
+ motion?: "full" | "reduced" | undefined;
91
+ background?: string | undefined;
92
+ surface?: string | undefined;
93
+ accent?: string | undefined;
94
+ accentText?: string | undefined;
95
+ danger?: string | undefined;
96
+ border?: string | undefined;
97
+ fontFamily?: string | undefined;
98
+ headingFamily?: string | undefined;
99
+ baseSize?: string | undefined;
100
+ mutedText?: string | undefined;
101
+ }, {
102
+ text?: string | undefined;
103
+ radius?: string | undefined;
104
+ density?: "compact" | "comfortable" | undefined;
105
+ motion?: "full" | "reduced" | undefined;
106
+ background?: string | undefined;
107
+ surface?: string | undefined;
108
+ accent?: string | undefined;
109
+ accentText?: string | undefined;
110
+ danger?: string | undefined;
111
+ border?: string | undefined;
112
+ fontFamily?: string | undefined;
113
+ headingFamily?: string | undefined;
114
+ baseSize?: string | undefined;
115
+ mutedText?: string | undefined;
116
+ }>;
117
+ uncertain: z.ZodOptional<z.ZodArray<z.ZodObject<{
118
+ slot: z.ZodString;
119
+ note: z.ZodString;
120
+ }, "strip", z.ZodTypeAny, {
121
+ slot: string;
122
+ note: string;
123
+ }, {
124
+ slot: string;
125
+ note: string;
126
+ }>, "many">>;
127
+ }, "strip", z.ZodTypeAny, {
128
+ slots: {
129
+ text?: string | undefined;
130
+ radius?: string | undefined;
131
+ density?: "compact" | "comfortable" | undefined;
132
+ motion?: "full" | "reduced" | undefined;
133
+ background?: string | undefined;
134
+ surface?: string | undefined;
135
+ accent?: string | undefined;
136
+ accentText?: string | undefined;
137
+ danger?: string | undefined;
138
+ border?: string | undefined;
139
+ fontFamily?: string | undefined;
140
+ headingFamily?: string | undefined;
141
+ baseSize?: string | undefined;
142
+ mutedText?: string | undefined;
143
+ };
144
+ uncertain?: {
145
+ slot: string;
146
+ note: string;
147
+ }[] | undefined;
148
+ }, {
149
+ slots: {
150
+ text?: string | undefined;
151
+ radius?: string | undefined;
152
+ density?: "compact" | "comfortable" | undefined;
153
+ motion?: "full" | "reduced" | undefined;
154
+ background?: string | undefined;
155
+ surface?: string | undefined;
156
+ accent?: string | undefined;
157
+ accentText?: string | undefined;
158
+ danger?: string | undefined;
159
+ border?: string | undefined;
160
+ fontFamily?: string | undefined;
161
+ headingFamily?: string | undefined;
162
+ baseSize?: string | undefined;
163
+ mutedText?: string | undefined;
164
+ };
165
+ uncertain?: {
166
+ slot: string;
167
+ note: string;
168
+ }[] | undefined;
169
+ }>;
170
+ /** Deterministic validation of a proposed slot value (model or human). */
171
+ export declare function validateSlotValue(slot: SlotKey, raw: string): string | null;
172
+ export declare function extractTheme(targetDir: string): Promise<ThemeSummary>;
173
+ /**
174
+ * Merges a parsed theme-stage artifact (`modelThemeSchema`) onto an
175
+ * exact-only `ThemeSummary` — the precedence law (kill-list §B2, Task 2):
176
+ * exact reads are never overwritten; only `needed` slots may be filled; every
177
+ * proposed value passes through `validateSlotValue` (invalid values are
178
+ * ignored, the slot stays defaulted/derived); a model-provided accentText
179
+ * stands; accentText re-derives by contrast only when the model filled
180
+ * accent but not accentText and accentText itself wasn't an exact read;
181
+ * headingFamily inherits fontFamily under the mirror condition.
182
+ */
183
+ export declare function applyThemeDraft(summary: ThemeSummary, draft: z.infer<typeof modelThemeSchema>): ThemeSummary;
184
+ export {};