@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,467 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { stdin, stdout } from "node:process";
4
+ import { cloudDoctor, liveModelTurn, startDevServerForProbe, } from "./doctor-live.js";
5
+ import { installedAiVersion } from "./dep-versions.js";
6
+ import { doctorFixRef } from "./doctor-codes.js";
7
+ import { EJECT_MANIFEST_FILE } from "./eject.js";
8
+ import { detectFramework, detectVendoWiring } from "./framework.js";
9
+ import { walk } from "./theme/walk.js";
10
+ import { remoteUrls, sameUrl, validateRegistryServer } from "./mcp/registry.js";
11
+ import { askYesNo, CLI_VERSION, consoleOutput, exists, normalizeDotEnvValue, readOptional, toolingTelemetry } from "./shared.js";
12
+ async function hasDependency(root) {
13
+ try {
14
+ const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
15
+ return [manifest.dependencies, manifest.devDependencies].some((deps) => deps?.["@vendoai/vendo"] !== undefined || deps?.vendoai !== undefined);
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /** root rides in as the client's cwd: projectIdHash/packageManager and the
22
+ .env.local cloud-key read attribute to the TARGET project, not the shell
23
+ cwd. Seams in options.telemetry win. */
24
+ function telemetryFor(options, output, root) {
25
+ return toolingTelemetry({ cwd: root, ...options.telemetry, log: (message) => output.log(message) });
26
+ }
27
+ async function probeBody(response) {
28
+ try {
29
+ const body = await response.json();
30
+ return typeof body === "object" && body !== null ? body : {};
31
+ }
32
+ catch {
33
+ return {};
34
+ }
35
+ }
36
+ /** Doctor runs standalone, so unlike the dev server it gets no framework
37
+ * dotenv loading — without this, `VENDO_API_KEY` sitting in `.env.local`
38
+ * is invisible to the cloud/live-turn checks and users must export it by
39
+ * hand. Reads `.env` then `.env.local` (local wins); real process env wins
40
+ * over both at the merge site. Minimal KEY=VALUE parser: `export ` prefix,
41
+ * matching single/double quotes, and `#` comment lines. */
42
+ export async function readDotEnvFallback(root) {
43
+ const env = {};
44
+ for (const file of [".env", ".env.local"]) {
45
+ const source = await readOptional(join(root, file));
46
+ if (source === null)
47
+ continue;
48
+ for (const rawLine of source.split(/\r?\n/)) {
49
+ const line = rawLine.trim();
50
+ if (line === "" || line.startsWith("#"))
51
+ continue;
52
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
53
+ if (!match)
54
+ continue;
55
+ env[match[1]] = normalizeDotEnvValue(match[2].trim());
56
+ }
57
+ }
58
+ return env;
59
+ }
60
+ /** Process env wins over the dotenv fallback — except that a blank process
61
+ * value yields to a concrete dotenv one, matching toolingTelemetry's
62
+ * VENDO_API_KEY precedence (an exported empty `VENDO_API_KEY=` must not
63
+ * mask the real key in `.env.local`). */
64
+ export function mergeEnvOverDotEnv(fallback, processEnv) {
65
+ const merged = { ...fallback, ...processEnv };
66
+ for (const [key, value] of Object.entries(processEnv)) {
67
+ if ((value ?? "").trim() === "" && fallback[key] !== undefined)
68
+ merged[key] = fallback[key];
69
+ }
70
+ return merged;
71
+ }
72
+ /** 09-vendo §5 / block-actions A — wiring checks plus live composition,
73
+ present-credential, and actAs mint+verify round-trips. */
74
+ export async function runDoctor(options) {
75
+ const root = resolve(options.targetDir);
76
+ const output = options.output ?? consoleOutput;
77
+ const json = options.json === true;
78
+ const env = options.env ?? mergeEnvOverDotEnv(await readDotEnvFallback(root), process.env);
79
+ const telemetry = telemetryFor(options, output, root);
80
+ let failures = 0;
81
+ let warnings = 0;
82
+ const checks = [];
83
+ // In --json mode nothing but the final object may reach stdout; human lines
84
+ // are suppressed and the same information rides the checks array instead.
85
+ const note = (message) => { if (!json)
86
+ output.log(message); };
87
+ // Human lines stay exactly as before (the fix_ref URL is a machine
88
+ // affordance; --json is the agent surface, so no per-line URL noise here).
89
+ const pass = (id, message) => { checks.push({ id, status: "ok", message }); if (!json)
90
+ output.log(`ok: ${message}`); };
91
+ const fail = (id, code, message) => { failures += 1; checks.push({ id, status: "broken", message, error_code: code, fix_ref: doctorFixRef(code) }); if (!json)
92
+ output.error(`broken: ${message}`); };
93
+ const warn = (id, code, message) => { warnings += 1; checks.push({ id, status: "warning", message, error_code: code, fix_ref: doctorFixRef(code) }); if (!json)
94
+ output.error(`warning: ${message}`); };
95
+ const framework = await detectFramework(root);
96
+ if (framework === "express") {
97
+ const wiring = await detectVendoWiring(root);
98
+ if (wiring.server)
99
+ pass("wiring/express-server", "Express server is wired");
100
+ else
101
+ fail("wiring/express-server", "E-WIRE-001", "Express server is not wired with createVendo from @vendoai/vendo/server");
102
+ if (wiring.client)
103
+ pass("wiring/express-client", "<VendoRoot> wraps the client");
104
+ else
105
+ fail("wiring/express-client", "E-WIRE-002", "Express client is not wrapped in <VendoRoot>");
106
+ }
107
+ else {
108
+ const routeCandidates = [
109
+ join(root, "app", "api", "vendo", "[...vendo]", "route.ts"),
110
+ join(root, "src", "app", "api", "vendo", "[...vendo]", "route.ts"),
111
+ ];
112
+ if ((await Promise.all(routeCandidates.map(exists))).some(Boolean))
113
+ pass("wiring/next-route", "catch-all handler is wired");
114
+ else
115
+ fail("wiring/next-route", "E-WIRE-003", "missing app/api/vendo/[...vendo]/route.ts");
116
+ const layoutCandidates = [join(root, "app", "layout.tsx"), join(root, "src", "app", "layout.tsx")];
117
+ let rootWired = false;
118
+ for (const path of layoutCandidates) {
119
+ try {
120
+ if ((await readFile(path, "utf8")).includes("<VendoRoot"))
121
+ rootWired = true;
122
+ }
123
+ catch {
124
+ // Try the other layout convention.
125
+ }
126
+ }
127
+ if (rootWired)
128
+ pass("wiring/next-root", "<VendoRoot> wraps the app");
129
+ else
130
+ fail("wiring/next-root", "E-WIRE-004", "root layout is not wrapped in <VendoRoot>");
131
+ }
132
+ if (await hasDependency(root))
133
+ pass("wiring/dependency", "@vendoai/vendo dependency is declared");
134
+ else
135
+ fail("wiring/dependency", "E-WIRE-005", "@vendoai/vendo (or vendoai alias) is not declared");
136
+ // #478 short-term — @vendoai/vendo speaks AI SDK v6 to the host's `ai`
137
+ // package (peer `ai >=6 <7`), but npm installs the peer conflict anyway:
138
+ // the static checks all pass and every internal turn then throws
139
+ // AI_InvalidPromptError (v7 removed system-role messages). Fail fast on the
140
+ // installed major. An absent install is the wiring/turn checks' story, and
141
+ // pre-v6 installs predate the peer contract — both skip silently.
142
+ const aiVersion = await installedAiVersion(root);
143
+ const aiMajor = aiVersion === null ? Number.NaN : Number.parseInt(aiVersion, 10);
144
+ if (aiMajor >= 7) {
145
+ fail("deps/ai-sdk-major", "E-DEP-001", `installed ai@${aiVersion} is unsupported — Vendo supports ai@6; downgrade (npm install ai@^6 @ai-sdk/anthropic@^3 @ai-sdk/react@^3) or track github.com/runvendo/vendo/issues/478`);
146
+ }
147
+ else if (aiMajor === 6) {
148
+ pass("deps/ai-sdk-major", `installed ai@${aiVersion} is the supported AI SDK major (v6)`);
149
+ }
150
+ for (const file of ["tools.json", "overrides.json", "policy.json", "brief.md", "theme.json"]) {
151
+ if (await exists(join(root, ".vendo", file)))
152
+ pass(`config/${file}`, `.vendo/${file}`);
153
+ else
154
+ fail(`config/${file}`, "E-CFG-001", `missing .vendo/${file}`);
155
+ }
156
+ if (!await exists(join(root, ".vendo", "data", ".gitignore")))
157
+ warn("config/data-gitignore", "E-CFG-002", ".vendo/data/.gitignore is missing");
158
+ // §4 customization ladder — ejected chrome drift. The ejected pixels are the
159
+ // host's code, so a version gap is awareness (warn), never breakage (fail):
160
+ // the hooks/wire dependency keeps working; only new presentation is missed.
161
+ const installedUi = await readOptional(join(root, "node_modules", "@vendoai", "ui", "package.json"));
162
+ let uiVersion = null;
163
+ try {
164
+ if (installedUi !== null)
165
+ uiVersion = JSON.parse(installedUi).version ?? null;
166
+ }
167
+ catch {
168
+ // Malformed install metadata — skip the drift check rather than fail doctor.
169
+ }
170
+ if (uiVersion !== null) {
171
+ for (const manifestPath of await walk(root, (rel) => rel.endsWith(EJECT_MANIFEST_FILE))) {
172
+ let ejected;
173
+ try {
174
+ ejected = JSON.parse(await readFile(manifestPath, "utf8"));
175
+ }
176
+ catch {
177
+ continue;
178
+ }
179
+ if (ejected.version === uiVersion) {
180
+ pass(`eject/${ejected.surface}`, `ejected ${ejected.surface} matches @vendoai/ui v${uiVersion}`);
181
+ }
182
+ else {
183
+ warn(`eject/${ejected.surface}`, "E-UI-001", `ejected ${ejected.surface} came from @vendoai/ui v${ejected.version} but v${uiVersion} is installed — review the changelog (https://github.com/runvendo/vendo/releases) and \`vendo eject ${ejected.surface} --force\` if you want the new presentation`);
184
+ }
185
+ }
186
+ }
187
+ const statusUrl = options.url
188
+ ?? env.VENDO_URL?.replace(/\/$/, "")
189
+ ?? "http://localhost:3000/api/vendo";
190
+ const fetchImpl = options.fetchImpl ?? fetch;
191
+ // Consent-gated dev-server start (design §5): when nothing is listening on
192
+ // the dev port and doctor is interactive, offer to boot it so the live probes
193
+ // have something to reach. --yes is the documented non-interactive consent
194
+ // (quickstart: "pass --yes to start it non-interactively"), so it bypasses
195
+ // the TTY gate. Skipped in --json runs (stdout carries only the final object).
196
+ const interactive = options.interactive ?? (Boolean(stdout.isTTY) && Boolean(stdin.isTTY));
197
+ const confirm = options.confirm ?? askYesNo;
198
+ let devServerStop = null;
199
+ if (!json && (interactive || options.yes === true)) {
200
+ let listening = false;
201
+ try {
202
+ listening = (await fetchImpl(`${statusUrl}/status`)).ok;
203
+ }
204
+ catch {
205
+ listening = false;
206
+ }
207
+ if (!listening) {
208
+ const go = options.yes === true
209
+ || await confirm("Nothing is listening on the dev port. Start the dev server for the probe?", true);
210
+ if (go) {
211
+ note(`\nStarting the dev server so the probe has a live composition to reach…`);
212
+ const start = options.startDevServer ?? startDevServerForProbe;
213
+ const started = await start({ root, statusUrl, env, fetchImpl });
214
+ if (started.ok) {
215
+ devServerStop = started.stop;
216
+ pass("dev/start", "started the dev server for the probe");
217
+ }
218
+ else
219
+ warn("dev/start", "E-DEV-001", "could not start the dev server for the probe; start it yourself (e.g. `npm run dev`) and re-run `vendo doctor`");
220
+ }
221
+ }
222
+ }
223
+ let mcpEnabled = false;
224
+ let sandboxVenue;
225
+ let liveComposition = false;
226
+ try {
227
+ const response = await fetchImpl(`${statusUrl}/status`, {
228
+ headers: { accept: "application/json" },
229
+ });
230
+ const body = await response.json();
231
+ if (!response.ok || typeof body.posture !== "string" || typeof body.version !== "string"
232
+ || typeof body.blocks !== "object" || body.blocks === null) {
233
+ fail("live/status", "E-LIVE-001", `/status returned an invalid composition response (${response.status})`);
234
+ }
235
+ else {
236
+ pass("live/status", `/status live round-trip (${body.version}, ${body.posture})`);
237
+ liveComposition = true;
238
+ // 10-mcp §1 — the door flag lives under blocks.mcp.
239
+ mcpEnabled = body.blocks.mcp === true;
240
+ sandboxVenue = body.blocks.sandbox;
241
+ if (sandboxVenue === "e2b" || sandboxVenue === "cloud" || sandboxVenue === "custom") {
242
+ pass("live/venue", `execution venue: ${sandboxVenue}`);
243
+ }
244
+ else if (sandboxVenue === false) {
245
+ warn("live/venue", "E-LIVE-004", "install the e2b package and set E2B_API_KEY, or set VENDO_API_KEY for the managed Cloud sandbox, or pass sandbox: to createVendo; without one, server apps (rungs 2-4) return sandbox-unavailable");
246
+ }
247
+ else if (sandboxVenue === undefined) {
248
+ // Older hosts predate blocks.sandbox — version skew, not a broken install.
249
+ warn("live/venue", "E-LIVE-005", "host /status does not report an execution venue; upgrade @vendoai/vendo to enable the venue check");
250
+ }
251
+ else {
252
+ fail("live/venue", "E-LIVE-003", "/status returned an invalid execution venue");
253
+ }
254
+ }
255
+ }
256
+ catch {
257
+ fail("live/status", "E-LIVE-002", `/status is unreachable at ${statusUrl}/status`);
258
+ }
259
+ if (!liveComposition) {
260
+ fail("auth/present", "E-AUTH-003", `present credential probe cannot run; start the dev server at ${statusUrl} and retry`);
261
+ fail("auth/act-as", "E-AUTH-006", `cannot probe actAs; start the dev server at ${statusUrl} and retry`);
262
+ }
263
+ else {
264
+ try {
265
+ const response = await fetchImpl(`${statusUrl}/doctor/present`, {
266
+ method: "POST",
267
+ headers: {
268
+ accept: "application/json",
269
+ "content-type": "application/json",
270
+ authorization: "Bearer vendo-doctor-present",
271
+ cookie: "vendo_doctor_present=1",
272
+ },
273
+ body: "{}",
274
+ });
275
+ const body = await probeBody(response);
276
+ if (response.ok && body.ok === true) {
277
+ pass("auth/present", "present credentials reach the host API");
278
+ }
279
+ else {
280
+ fail("auth/present", "E-AUTH-001", "present credentials did not reach the host API; set VENDO_BASE_URL to the running host origin and restart the dev server");
281
+ }
282
+ }
283
+ catch {
284
+ fail("auth/present", "E-AUTH-002", `present credential probe is unreachable at ${statusUrl}/doctor/present; restart the dev server and verify VENDO_BASE_URL`);
285
+ }
286
+ try {
287
+ const response = await fetchImpl(`${statusUrl}/doctor/act-as`, {
288
+ method: "POST",
289
+ headers: { accept: "application/json", "content-type": "application/json" },
290
+ body: "{}",
291
+ });
292
+ const body = await probeBody(response);
293
+ if (response.ok && body.ok === true) {
294
+ pass("auth/act-as", "actAs mint + host verification live round-trip");
295
+ }
296
+ else if (body.error?.code === "act-as-not-configured") {
297
+ warn("auth/act-as", "E-AUTH-007", "actAs is not configured; pass createVendo({ actAs }) before enabling away host actions");
298
+ }
299
+ else {
300
+ fail("auth/act-as", "E-AUTH-004", "actAs mint + host verification failed; check createVendo({ actAs }), its verifier middleware, and the host principal resolver");
301
+ }
302
+ }
303
+ catch {
304
+ fail("auth/act-as", "E-AUTH-005", `actAs probe is unreachable at ${statusUrl}/doctor/act-as; restart the dev server and check createVendo({ actAs })`);
305
+ }
306
+ }
307
+ // 10-mcp §5 — when the door is open, verify both discovery documents resolve
308
+ // and the server card parses. The metadata is path-inserted (RFC 9728 §3): a
309
+ // door mounted at /api/vendo/mcp serves /.well-known/...-resource/api/vendo/mcp.
310
+ if (mcpEnabled) {
311
+ const origin = new URL(statusUrl).origin;
312
+ const mountPath = `${new URL(statusUrl).pathname.replace(/\/$/, "")}/mcp`;
313
+ const resolves = async (id, code, url, valid, label) => {
314
+ try {
315
+ const response = await fetchImpl(url, { headers: { accept: "application/json" } });
316
+ const body = await response.json();
317
+ if (response.ok && valid(body))
318
+ pass(id, label);
319
+ else
320
+ fail(id, code, `${label} (${response.status})`);
321
+ }
322
+ catch {
323
+ fail(id, code, `${label} is unreachable`);
324
+ }
325
+ };
326
+ await resolves("mcp/protected-resource", "E-MCP-001", `${origin}/.well-known/oauth-protected-resource${mountPath}`, (body) => typeof body.resource === "string", "MCP protected-resource metadata resolves");
327
+ await resolves("mcp/authorization-server", "E-MCP-002", `${origin}/.well-known/oauth-authorization-server${mountPath}`, (body) => typeof body.issuer === "string", "MCP authorization-server metadata resolves");
328
+ await resolves("mcp/server-card", "E-MCP-003", `${origin}/.well-known/mcp/server-card.json`, (body) => typeof body.name === "string" && Array.isArray(body.transports), "MCP server card parses");
329
+ // 10-mcp §5 — the official registry artifact is optional until a host is
330
+ // published, but once present it must describe this live door exactly.
331
+ const serverJson = await readOptional(join(root, "server.json"));
332
+ if (serverJson !== null) {
333
+ try {
334
+ const server = JSON.parse(serverJson);
335
+ const errors = validateRegistryServer(server);
336
+ if (errors.length === 0)
337
+ pass("mcp/server-json", "server.json matches MCP registry discovery requirements");
338
+ else
339
+ fail("mcp/server-json", "E-MCP-004", `server.json is invalid: ${errors.join("; ")}`);
340
+ const liveDoorUrl = `${origin}${mountPath}`;
341
+ if (remoteUrls(server).some((remote) => sameUrl(remote, liveDoorUrl))) {
342
+ pass("mcp/server-json-remote", "server.json remote agrees with the live MCP door");
343
+ }
344
+ else {
345
+ fail("mcp/server-json-remote", "E-MCP-005", `server.json remote does not match the live MCP door ${liveDoorUrl}`);
346
+ }
347
+ }
348
+ catch {
349
+ fail("mcp/server-json", "E-MCP-006", "server.json is invalid JSON");
350
+ }
351
+ }
352
+ const localChallenge = await readOptional(join(root, "public", ".well-known", "mcp-registry-auth"));
353
+ if (localChallenge !== null) {
354
+ if (localChallenge.trim().startsWith("v=MCPv1"))
355
+ pass("mcp/registry-auth-local", "local MCP registry auth challenge parses");
356
+ else
357
+ fail("mcp/registry-auth-local", "E-MCP-007", "local MCP registry auth challenge must start with v=MCPv1");
358
+ }
359
+ try {
360
+ const response = await fetchImpl(`${origin}/.well-known/mcp-registry-auth`, {
361
+ headers: { accept: "text/plain" },
362
+ });
363
+ if (response.ok) {
364
+ const challenge = await response.text();
365
+ if (challenge.trim().startsWith("v=MCPv1"))
366
+ pass("mcp/registry-auth-live", "MCP registry auth challenge parses");
367
+ else
368
+ fail("mcp/registry-auth-live", "E-MCP-008", "MCP registry auth challenge must start with v=MCPv1");
369
+ }
370
+ }
371
+ catch {
372
+ // The HTTP proof is optional; DNS verification may be in use instead.
373
+ }
374
+ }
375
+ // execution-v2 Lane D — machine + schedule REPORTING (no new subcommand):
376
+ // which apps carry a machine, whether a schedule caller is configured for
377
+ // the authenticated /tick surface, and each schedule's last-fired time.
378
+ // /doctor/machines is a dev-only route, so an unreachable or older host
379
+ // simply skips the section (reporting must never break doctor).
380
+ if (liveComposition) {
381
+ try {
382
+ const response = await fetchImpl(`${statusUrl}/doctor/machines`, { headers: { accept: "application/json" } });
383
+ if (response.ok) {
384
+ const body = await response.json();
385
+ const machines = Array.isArray(body.machines) ? body.machines : [];
386
+ pass("machines/apps", machines.length === 0
387
+ ? "no machine-bearing apps"
388
+ : `${machines.length} machine-bearing app${machines.length === 1 ? "" : "s"}`);
389
+ for (const machine of machines) {
390
+ note(` ${machine.appId ?? "?"} (${machine.name ?? "unnamed"}): ${machine.awake === true ? "awake" : "asleep"}`);
391
+ for (const schedule of machine.schedules ?? []) {
392
+ const lastFired = schedule.lastFiredAt === undefined
393
+ ? "never fired"
394
+ : `last fired ${schedule.lastFiredAt}${schedule.lastStatus === "error" ? " (error)" : ""}`;
395
+ note(` ${schedule.cron ?? "?"} -> POST /fn/${schedule.fn ?? "?"} — ${lastFired}`);
396
+ }
397
+ }
398
+ const declaresSchedules = machines.some((machine) => (machine.schedules?.length ?? 0) > 0);
399
+ if (body.scheduleCallerConfigured === true) {
400
+ pass("machines/schedule-caller", "schedule caller configured (VENDO_TICK_SECRET); point an external cron at POST /api/vendo/tick");
401
+ }
402
+ else if (declaresSchedules) {
403
+ warn("machines/schedule-caller", "E-SCHED-001", "apps declare vendo.json schedules but no schedule caller is configured — set VENDO_TICK_SECRET and point an external cron (Vercel cron, GitHub Actions, crontab) at POST /api/vendo/tick");
404
+ }
405
+ else if (machines.length > 0) {
406
+ note(" no schedule caller configured (VENDO_TICK_SECRET unset) — needed once an app declares vendo.json schedules");
407
+ }
408
+ }
409
+ }
410
+ catch {
411
+ // Reporting only — an unreachable machines route never fails doctor.
412
+ }
413
+ }
414
+ note("Ladder: execution venue is checked above; actAs for away host actions; connectors for external tools.");
415
+ // One real model turn through the wired route (design §5). Exit 0 == a user
416
+ // would have gotten an answer. Reuses the resolver + devModel: the running
417
+ // dev server serves the turn over the same credential doctor reports.
418
+ let liveTurn;
419
+ if (liveComposition) {
420
+ liveTurn = await (options.liveTurn ?? ((base) => liveModelTurn({
421
+ base,
422
+ fetchImpl,
423
+ env,
424
+ })))(statusUrl);
425
+ if (liveTurn.ok) {
426
+ pass("turn/model", `live model turn answered over ${liveTurn.credential} (${liveTurn.elapsedMs}ms)`);
427
+ if (liveTurn.reply !== undefined)
428
+ note(`\n ${liveTurn.reply.trim()}\n`);
429
+ }
430
+ else {
431
+ fail("turn/model", "E-TURN-001", `live model turn did not answer over ${liveTurn.credential}: ${liveTurn.error ?? "no reply"}`);
432
+ }
433
+ }
434
+ else {
435
+ liveTurn = { attempted: false, ok: false, rung: "none", credential: "n/a", elapsedMs: 0, error: "dev server unreachable" };
436
+ fail("turn/model", "E-TURN-002", `live model turn cannot run; start the dev server at ${statusUrl} and retry`);
437
+ }
438
+ // VENDO_API_KEY local shape check + what Cloud unlocks (design §5-6). Key
439
+ // problems surface on the first real service call — no validate round-trip.
440
+ const cloud = await (options.cloudProbe ?? cloudDoctor)({ env });
441
+ if (cloud.present && cloud.ok) {
442
+ pass("cloud/key", "Vendo Cloud key present and well-formed");
443
+ }
444
+ else if (cloud.present) {
445
+ warn("cloud/key", "E-CLOUD-001", `VENDO_API_KEY is set but not usable: ${cloud.error ?? "malformed"}`);
446
+ }
447
+ else {
448
+ note(`Vendo Cloud (optional): no VENDO_API_KEY. A key unlocks ${cloud.unlocks.join("; ")}. Run \`vendo login\` to start.`);
449
+ }
450
+ if (devServerStop !== null)
451
+ devServerStop();
452
+ const wired = failures === 0;
453
+ await telemetry.track("doctor_run", { failures, warnings, wired });
454
+ if (json) {
455
+ output.log(JSON.stringify({
456
+ vendo: "doctor",
457
+ version: CLI_VERSION,
458
+ wired,
459
+ exit: wired ? 0 : 1,
460
+ checks,
461
+ liveTurn,
462
+ cloud,
463
+ summary: { failures, warnings },
464
+ }, null, 2));
465
+ }
466
+ return wired ? 0 : 1;
467
+ }
@@ -0,0 +1,29 @@
1
+ import { type Output, type TelemetryOptions } from "./shared.js";
2
+ export interface EjectOptions {
3
+ targetDir: string;
4
+ surface?: string;
5
+ list?: boolean;
6
+ force?: boolean;
7
+ output?: Output;
8
+ /** Injectable telemetry deps (matches init/doctor). */
9
+ telemetry?: TelemetryOptions;
10
+ }
11
+ export declare const EJECT_MANIFEST_FILE = ".vendo-eject.json";
12
+ export interface EjectedManifest {
13
+ surface: string;
14
+ package: "@vendoai/ui";
15
+ version: string;
16
+ }
17
+ /**
18
+ * Rewrite one template source for life in the host repo. Relative specifiers
19
+ * resolve against the surface's src/-relative sourceBase: imports landing in
20
+ * the surface's own directory stay relative (dropping the .js extension for
21
+ * host bundlers), everything else maps to the public package subpath it is
22
+ * exported from (tree/ → @vendoai/ui/tree, chrome/ → @vendoai/ui/chrome,
23
+ * hooks/context/theme → @vendoai/ui).
24
+ */
25
+ export declare function rewriteTemplateSource(source: string, shape: {
26
+ sourceBase: string;
27
+ sourceDir?: string;
28
+ }): string;
29
+ export declare function runEject(options: EjectOptions): Promise<number>;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * `vendo eject <surface>` — copy a shipped chrome surface's presentation
3
+ * source out of the installed @vendoai/ui into the host repo as code the
4
+ * developer owns, shadcn-style (§4 customization ladder, eject rung).
5
+ *
6
+ * Pixels are copied; data/wire logic stays a package dependency: the
7
+ * templates' package-internal relative imports are rewritten to the public
8
+ * @vendoai/ui subpaths, so protocol updates keep flowing to ejected code.
9
+ * A `.vendo-eject.json` manifest in the ejected directory records surface +
10
+ * package version for doctor's drift check.
11
+ */
12
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
13
+ import { createRequire } from "node:module";
14
+ import { dirname, join, posix, relative } from "node:path";
15
+ import { consoleOutput, exists, withCommandRun } from "./shared.js";
16
+ export const EJECT_MANIFEST_FILE = ".vendo-eject.json";
17
+ /** dist/eject-templates of the installed @vendoai/ui — host install first
18
+ (its version is what the host runs), the CLI's own copy as fallback for
19
+ strict layouts where the host doesn't depend on @vendoai/ui directly. */
20
+ function templatesRoot(targetDir) {
21
+ const anchors = [join(targetDir, "package.json"), import.meta.url];
22
+ for (const anchor of anchors) {
23
+ try {
24
+ const entry = createRequire(anchor).resolve("@vendoai/ui");
25
+ return join(dirname(entry), "eject-templates");
26
+ }
27
+ catch {
28
+ // try the next anchor
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ /**
34
+ * Rewrite one template source for life in the host repo. Relative specifiers
35
+ * resolve against the surface's src/-relative sourceBase: imports landing in
36
+ * the surface's own directory stay relative (dropping the .js extension for
37
+ * host bundlers), everything else maps to the public package subpath it is
38
+ * exported from (tree/ → @vendoai/ui/tree, chrome/ → @vendoai/ui/chrome,
39
+ * hooks/context/theme → @vendoai/ui).
40
+ */
41
+ export function rewriteTemplateSource(source, shape) {
42
+ // from-clauses, side-effect imports, and dynamic import() all carry
43
+ // specifiers; the quote never follows "import" directly in a from-clause,
44
+ // so the alternation cannot double-match.
45
+ return source.replace(/(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(["'])([^"']+)\2/g, (whole, lead, quote, specifier) => {
46
+ if (!specifier.startsWith("."))
47
+ return whole;
48
+ const resolved = posix.normalize(posix.join(shape.sourceBase, specifier)).replace(/\.js$/, "");
49
+ let rewritten;
50
+ if (shape.sourceDir !== undefined &&
51
+ (resolved === shape.sourceDir || resolved.startsWith(`${shape.sourceDir}/`))) {
52
+ rewritten = specifier.replace(/\.js$/, "");
53
+ }
54
+ else if (resolved.startsWith("tree/"))
55
+ rewritten = "@vendoai/ui/tree";
56
+ else if (resolved.startsWith("chrome/"))
57
+ rewritten = "@vendoai/ui/chrome";
58
+ else
59
+ rewritten = "@vendoai/ui";
60
+ return `${lead}${quote}${rewritten}${quote}`;
61
+ });
62
+ }
63
+ /** components/vendo/<surface>, under src/ when the host keeps its app there
64
+ (same layout probe as init's appDirectory). `srcLayout` also picks the
65
+ swap hint's import form: src hosts import via their @/ alias. */
66
+ async function destinationDir(root, surface) {
67
+ const srcLayout = await exists(join(root, "src", "app"));
68
+ const base = srcLayout ? join(root, "src") : root;
69
+ return { destination: join(base, "components", "vendo", surface), srcLayout };
70
+ }
71
+ export async function runEject(options) {
72
+ const output = options.output ?? consoleOutput;
73
+ const root = options.targetDir;
74
+ return withCommandRun({
75
+ command: "eject",
76
+ root,
77
+ ...(options.telemetry === undefined ? {} : { telemetry: options.telemetry }),
78
+ }, (failure) => eject(options, output, root, failure));
79
+ }
80
+ async function eject(options, output, root, failure) {
81
+ const templates = templatesRoot(root);
82
+ if (templates === null || !(await exists(join(templates, "templates.json")))) {
83
+ failure.failedStep = "templates";
84
+ output.error("vendo eject: could not find an installed @vendoai/ui with eject templates — install vendoai (or @vendoai/ui) first.");
85
+ return 1;
86
+ }
87
+ const manifest = JSON.parse(await readFile(join(templates, "templates.json"), "utf8"));
88
+ const available = Object.keys(manifest.surfaces).sort();
89
+ if (options.list === true) {
90
+ output.log(`Ejectable surfaces (@vendoai/ui v${manifest.version}):`);
91
+ for (const surface of available) {
92
+ output.log(` ${surface} ${manifest.surfaces[surface].description}`);
93
+ }
94
+ output.log("\nRun `vendo eject <surface>` to copy one into your repo.");
95
+ return 0;
96
+ }
97
+ const surface = options.surface;
98
+ if (surface === undefined || surface === "") {
99
+ failure.failedStep = "surface";
100
+ output.error(`vendo eject: name a surface (${available.join(", ")}) or use --list.`);
101
+ return 1;
102
+ }
103
+ const entry = manifest.surfaces[surface];
104
+ if (entry === undefined) {
105
+ failure.failedStep = "surface";
106
+ output.error(`vendo eject: unknown surface "${surface}" — available: ${available.join(", ")}.`);
107
+ return 1;
108
+ }
109
+ const { destination, srcLayout } = await destinationDir(root, surface);
110
+ if (await exists(destination)) {
111
+ if (options.force !== true) {
112
+ failure.failedStep = "exists";
113
+ output.error(`vendo eject: ${relative(root, destination)} already exists — it may hold your edits. Re-run with --force to overwrite.`);
114
+ return 1;
115
+ }
116
+ await rm(destination, { recursive: true, force: true });
117
+ }
118
+ await mkdir(destination, { recursive: true });
119
+ const shape = { sourceBase: entry.sourceBase, ...(entry.sourceDir === undefined ? {} : { sourceDir: entry.sourceDir }) };
120
+ for (const file of entry.files) {
121
+ const source = await readFile(join(templates, surface, file), "utf8");
122
+ await writeFile(join(destination, file), rewriteTemplateSource(source, shape), "utf8");
123
+ }
124
+ const ejected = { surface, package: "@vendoai/ui", version: manifest.version };
125
+ await writeFile(join(destination, EJECT_MANIFEST_FILE), `${JSON.stringify(ejected, null, 2)}\n`, "utf8");
126
+ const relDir = relative(root, destination);
127
+ const componentName = entry.component;
128
+ // src hosts import through their @/ alias; "./src/…" would mislead (Devin
129
+ // ANALYSIS_0001 — an importer under src/ never writes the src/ segment).
130
+ const importHint = srcLayout ? `@/components/vendo/${surface}` : `./components/vendo/${surface}`;
131
+ output.log(`Ejected ${surface} → ${relDir} (${entry.files.length} files, @vendoai/ui v${manifest.version})`);
132
+ output.log("");
133
+ output.log("Swap it in:");
134
+ output.log(` import { ${componentName} } from "${importHint}";`);
135
+ output.log(surface === "thread"
136
+ ? ` <VendoOverlay thread={${componentName}} />`
137
+ : ` <${componentName} /> (in place of the @vendoai/ui/chrome import)`);
138
+ output.log("");
139
+ output.log("The files are yours to edit. Data and wire logic still flow from @vendoai/ui.");
140
+ return 0;
141
+ }
@@ -0,0 +1,26 @@
1
+ import { type Output, type TelemetryOptions } from "../shared.js";
2
+ /**
3
+ * `vendo extract --apply <draft.json>` — the delegation surface. An external
4
+ * coding agent read the codebase against the aiPolish contract from
5
+ * `vendo init --agent` and wrote a draft; this command parses it (the same
6
+ * parseDraft init's built-in pass uses), runs the SAME deterministic guards
7
+ * (applyDraft), writes the same artifacts, re-syncs, and prints the same
8
+ * summary. Non-interactive safe; exits non-zero on an unusable draft.
9
+ */
10
+ export interface ExtractApplyOptions {
11
+ targetDir: string;
12
+ /** Path to the draft file (cwd-relative or absolute). */
13
+ apply: string;
14
+ force?: boolean;
15
+ output?: Output;
16
+ /** Test seam (matches sync.ts): the re-sync implementation. */
17
+ sync?: (input: {
18
+ root: string;
19
+ out: string;
20
+ }) => Promise<{
21
+ warnings: string[];
22
+ }>;
23
+ /** Injectable telemetry deps (matches init/doctor). */
24
+ telemetry?: TelemetryOptions;
25
+ }
26
+ export declare function runExtractApply(options: ExtractApplyOptions): Promise<number>;