@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,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>;
|