@vendoai/vendo 0.4.2 → 0.4.3
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/dist/cli/cloud/device-login.d.ts +11 -0
- package/dist/cli/cloud/device-login.js +52 -1
- package/dist/cli/doctor-codes.d.ts +3 -0
- package/dist/cli/doctor-codes.js +3 -0
- package/dist/cli/doctor-live.js +11 -1
- package/dist/cli/doctor.js +70 -10
- package/dist/cli/framework.d.ts +17 -2
- package/dist/cli/framework.js +32 -5
- package/dist/cli/init-scaffolds.d.ts +22 -0
- package/dist/cli/init-scaffolds.js +60 -8
- package/dist/cli/init.d.ts +11 -1
- package/dist/cli/init.js +160 -39
- package/dist/cli/playground/bundle.gen.d.ts +1 -1
- package/dist/cli/playground/bundle.gen.js +1 -1
- package/dist/cli/playground/embed-bundle.gen.d.ts +1 -1
- package/dist/cli/playground/embed-bundle.gen.js +1 -1
- package/dist/cli/shared.d.ts +1 -1
- package/dist/cli/shared.js +1 -1
- package/dist/mastra.js +71 -2
- package/dist/react.d.ts +1 -0
- package/dist/react.js +7 -0
- package/dist/wire/apps.js +30 -1
- package/dist/wire/shared.d.ts +1 -1
- package/dist/wire/shared.js +1 -1
- package/package.json +10 -10
|
@@ -19,6 +19,17 @@ export interface DeviceLoginOptions {
|
|
|
19
19
|
file that lets a fresh run resume a still-open ceremony (#479). */
|
|
20
20
|
home?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Write-preflight (0.4.1 E2E cert M4): prove `.env.local` is writable BEFORE
|
|
24
|
+
* any claim is opened or redeemed. Sandboxed agent runs — headless Claude
|
|
25
|
+
* Code protects env files even under --dangerously-skip-permissions — can
|
|
26
|
+
* deny the write; without this check the ceremony redeems the single-use
|
|
27
|
+
* claim, the key mints server-side, and the write failure loses it. A real
|
|
28
|
+
* append-mode open is the probe (permission checks like `access(W_OK)` don't
|
|
29
|
+
* see sandbox policies, which deny at open time); a probe-created empty file
|
|
30
|
+
* is removed again. Returns the failure detail, or null when writable.
|
|
31
|
+
*/
|
|
32
|
+
export declare function preflightEnvLocalWrite(root: string): Promise<string | null>;
|
|
22
33
|
/**
|
|
23
34
|
* `vendo login` — the top-level command surface: the identical ceremony
|
|
24
35
|
* wrapped in one `command_run` row (command "login", TELEMETRY.md). The
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { open, stat, unlink } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { option } from "./args.js";
|
|
4
5
|
import { isVendoKey, resolveCloudBaseUrl } from "./client.js";
|
|
@@ -27,6 +28,30 @@ function defaultOpenBrowser(url) {
|
|
|
27
28
|
const { command, args } = browserOpenCommand(process.platform, url);
|
|
28
29
|
execFile(command, args, () => undefined); // best-effort: the printed URL is the fallback
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Write-preflight (0.4.1 E2E cert M4): prove `.env.local` is writable BEFORE
|
|
33
|
+
* any claim is opened or redeemed. Sandboxed agent runs — headless Claude
|
|
34
|
+
* Code protects env files even under --dangerously-skip-permissions — can
|
|
35
|
+
* deny the write; without this check the ceremony redeems the single-use
|
|
36
|
+
* claim, the key mints server-side, and the write failure loses it. A real
|
|
37
|
+
* append-mode open is the probe (permission checks like `access(W_OK)` don't
|
|
38
|
+
* see sandbox policies, which deny at open time); a probe-created empty file
|
|
39
|
+
* is removed again. Returns the failure detail, or null when writable.
|
|
40
|
+
*/
|
|
41
|
+
export async function preflightEnvLocalWrite(root) {
|
|
42
|
+
const path = join(root, ".env.local");
|
|
43
|
+
const existedBefore = await stat(path).then(() => true, () => false);
|
|
44
|
+
try {
|
|
45
|
+
const handle = await open(path, "a");
|
|
46
|
+
await handle.close();
|
|
47
|
+
if (!existedBefore)
|
|
48
|
+
await unlink(path).catch(() => undefined);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return error instanceof Error ? error.message : String(error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
30
55
|
function ceremonyFrom(value) {
|
|
31
56
|
const body = value;
|
|
32
57
|
if (typeof body?.claim_token !== "string"
|
|
@@ -92,6 +117,18 @@ export async function runDeviceLogin(args, options = {}) {
|
|
|
92
117
|
const waitMs = waitSeconds === undefined ? undefined : waitSeconds * 1000;
|
|
93
118
|
const pendingHome = options.home === undefined ? {} : { home: options.home };
|
|
94
119
|
const claimCwd = options.root ?? process.cwd();
|
|
120
|
+
// Prove the key's landing file is writable BEFORE the ceremony touches the
|
|
121
|
+
// network: a claim is single-use, so a post-approval write failure loses
|
|
122
|
+
// the minted key (M4). This is a WRITE failure, distinct from a timeout —
|
|
123
|
+
// the error says so instead of the generic polling copy.
|
|
124
|
+
const writeProblem = await preflightEnvLocalWrite(claimCwd);
|
|
125
|
+
if (writeProblem !== null) {
|
|
126
|
+
output.error(`Cannot write ${join(claimCwd, ".env.local")} (${writeProblem}). ` +
|
|
127
|
+
"`vendo login` lands the minted key there, so the ceremony was NOT started and no key was minted. " +
|
|
128
|
+
"This is a file-write problem, not a timeout: fix write access (sandboxed agent runs often deny env-file " +
|
|
129
|
+
"writes — re-run outside the sandbox or have your human run `vendo login`), then re-run.");
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
95
132
|
try {
|
|
96
133
|
// A still-open claim from a dead process (#479): resume polling it so the
|
|
97
134
|
// human's late approval — against the code they were already shown —
|
|
@@ -176,7 +213,21 @@ export async function runDeviceLogin(args, options = {}) {
|
|
|
176
213
|
if (typeof key !== "string" || !isVendoKey(key)) {
|
|
177
214
|
throw new Error("Vendo Cloud returned an invalid credential");
|
|
178
215
|
}
|
|
179
|
-
|
|
216
|
+
try {
|
|
217
|
+
await upsertEnvLocal(root, "VENDO_API_KEY", key);
|
|
218
|
+
}
|
|
219
|
+
catch (writeError) {
|
|
220
|
+
// The claim is consumed and the key minted — a resume can never
|
|
221
|
+
// succeed, so the pending file goes; and this must read as a WRITE
|
|
222
|
+
// failure, never the timeout/polling copy (M4). The key is never
|
|
223
|
+
// printed, so the minted key is unrecoverable here: name the
|
|
224
|
+
// console cleanup explicitly.
|
|
225
|
+
await deletePendingClaim(claimCwd, pendingHome);
|
|
226
|
+
throw new Error(`Approved and the key was minted, but writing it to ${join(root, ".env.local")} failed ` +
|
|
227
|
+
`(${writeError instanceof Error ? writeError.message : String(writeError)}). ` +
|
|
228
|
+
"The key was NOT saved and is not shown anywhere: revoke it in the console (Keys page), " +
|
|
229
|
+
"fix write access to .env.local, and run `vendo login` again.");
|
|
230
|
+
}
|
|
180
231
|
await deletePendingClaim(claimCwd, pendingHome);
|
|
181
232
|
// Never print the key itself — .env.local is the hand-off, last4 the
|
|
182
233
|
// receipt. A resumed run names the full path: it may differ from cwd.
|
|
@@ -17,9 +17,11 @@ export declare const DOCTOR_ERROR_CODES: {
|
|
|
17
17
|
readonly "E-WIRE-003": "the Next.js catch-all handler app/api/vendo/[...vendo]/route.ts is missing";
|
|
18
18
|
readonly "E-WIRE-004": "the Next.js root layout is not wrapped in <VendoRoot>";
|
|
19
19
|
readonly "E-WIRE-005": "the @vendoai/vendo (or vendoai alias) dependency is not declared";
|
|
20
|
+
readonly "E-WIRE-006": "no visible agent surface is mounted (<VendoRoot> alone renders nothing)";
|
|
20
21
|
readonly "E-CFG-001": "a required .vendo/ config file is missing";
|
|
21
22
|
readonly "E-CFG-002": ".vendo/data/.gitignore is missing";
|
|
22
23
|
readonly "E-DEP-001": "the installed ai package is a major version @vendoai/vendo does not support";
|
|
24
|
+
readonly "E-DEP-002": "the running wire serves a different @vendoai/vendo version than this CLI (split-brain install)";
|
|
23
25
|
readonly "E-UI-001": "an ejected surface predates the installed @vendoai/ui";
|
|
24
26
|
readonly "E-DEV-001": "the dev server could not be started for the probe";
|
|
25
27
|
readonly "E-LIVE-001": "/status returned an invalid composition response";
|
|
@@ -27,6 +29,7 @@ export declare const DOCTOR_ERROR_CODES: {
|
|
|
27
29
|
readonly "E-LIVE-003": "/status returned an invalid execution venue";
|
|
28
30
|
readonly "E-LIVE-004": "no execution venue is configured";
|
|
29
31
|
readonly "E-LIVE-005": "the host /status does not report an execution venue (version skew)";
|
|
32
|
+
readonly "E-LIVE-006": "the app's root page returns a server error while the wire answers";
|
|
30
33
|
readonly "E-AUTH-001": "present credentials did not reach the host API";
|
|
31
34
|
readonly "E-AUTH-002": "the present credential probe is unreachable";
|
|
32
35
|
readonly "E-AUTH-003": "the present credential probe cannot run while the dev server is down";
|
package/dist/cli/doctor-codes.js
CHANGED
|
@@ -18,9 +18,11 @@ export const DOCTOR_ERROR_CODES = {
|
|
|
18
18
|
"E-WIRE-003": "the Next.js catch-all handler app/api/vendo/[...vendo]/route.ts is missing",
|
|
19
19
|
"E-WIRE-004": "the Next.js root layout is not wrapped in <VendoRoot>",
|
|
20
20
|
"E-WIRE-005": "the @vendoai/vendo (or vendoai alias) dependency is not declared",
|
|
21
|
+
"E-WIRE-006": "no visible agent surface is mounted (<VendoRoot> alone renders nothing)",
|
|
21
22
|
"E-CFG-001": "a required .vendo/ config file is missing",
|
|
22
23
|
"E-CFG-002": ".vendo/data/.gitignore is missing",
|
|
23
24
|
"E-DEP-001": "the installed ai package is a major version @vendoai/vendo does not support",
|
|
25
|
+
"E-DEP-002": "the running wire serves a different @vendoai/vendo version than this CLI (split-brain install)",
|
|
24
26
|
"E-UI-001": "an ejected surface predates the installed @vendoai/ui",
|
|
25
27
|
"E-DEV-001": "the dev server could not be started for the probe",
|
|
26
28
|
"E-LIVE-001": "/status returned an invalid composition response",
|
|
@@ -28,6 +30,7 @@ export const DOCTOR_ERROR_CODES = {
|
|
|
28
30
|
"E-LIVE-003": "/status returned an invalid execution venue",
|
|
29
31
|
"E-LIVE-004": "no execution venue is configured",
|
|
30
32
|
"E-LIVE-005": "the host /status does not report an execution venue (version skew)",
|
|
33
|
+
"E-LIVE-006": "the app's root page returns a server error while the wire answers",
|
|
31
34
|
"E-AUTH-001": "present credentials did not reach the host API",
|
|
32
35
|
"E-AUTH-002": "the present credential probe is unreachable",
|
|
33
36
|
"E-AUTH-003": "the present credential probe cannot run while the dev server is down",
|
package/dist/cli/doctor-live.js
CHANGED
|
@@ -158,7 +158,17 @@ export async function startDevServerForProbe(options) {
|
|
|
158
158
|
log.push(`spawn error: ${error.message}\n`);
|
|
159
159
|
failSpawn();
|
|
160
160
|
});
|
|
161
|
-
|
|
161
|
+
// Destroy the piped streams too: `npm run dev` grandchildren (the actual
|
|
162
|
+
// next/vite process) can survive the SIGTERM and keep the inherited pipe
|
|
163
|
+
// fds open, and the live 'data' listeners would then hold doctor's event
|
|
164
|
+
// loop — the process would hang after printing its verdict instead of
|
|
165
|
+
// exiting with it (exit-code honesty, 0.4.1 E2E cert defect 13 family).
|
|
166
|
+
const stop = () => {
|
|
167
|
+
child.kill("SIGTERM");
|
|
168
|
+
child.stdout?.destroy();
|
|
169
|
+
child.stderr?.destroy();
|
|
170
|
+
child.unref?.(); // injected test doubles may not implement it
|
|
171
|
+
};
|
|
162
172
|
const up = await Promise.race([
|
|
163
173
|
waitForStatus(options.statusUrl, fetchImpl, options.timeoutMs ?? 120_000),
|
|
164
174
|
spawnFailed,
|
package/dist/cli/doctor.js
CHANGED
|
@@ -93,8 +93,26 @@ export async function runDoctor(options) {
|
|
|
93
93
|
const warn = (id, code, message) => { warnings += 1; checks.push({ id, status: "warning", message, error_code: code, fix_ref: doctorFixRef(code) }); if (!json)
|
|
94
94
|
output.error(`warning: ${message}`); };
|
|
95
95
|
const framework = await detectFramework(root);
|
|
96
|
+
// The generated vendo/vendo-root.tsx wrapper carries the <VendoRoot> AND
|
|
97
|
+
// <VendoOverlay /> markers itself, so an unexcluded scan would pass the
|
|
98
|
+
// client/surface gates even when NO layout mounts the wrapper — the exact
|
|
99
|
+
// doctor-green-but-invisible failure E-WIRE-006 exists to catch. Mirror
|
|
100
|
+
// init's layout decision: exclude the wrapper from the scan, then let a
|
|
101
|
+
// user-code <VendoRoot> mount next to an overlay-bearing wrapper satisfy
|
|
102
|
+
// the surface (the wrapper renders the overlay once a layout mounts it).
|
|
103
|
+
const wrapperCandidates = [
|
|
104
|
+
join(root, "vendo", "vendo-root.tsx"),
|
|
105
|
+
join(root, "src", "vendo", "vendo-root.tsx"),
|
|
106
|
+
];
|
|
107
|
+
let wrapperWithOverlay = false;
|
|
108
|
+
for (const candidate of wrapperCandidates) {
|
|
109
|
+
const source = await readFile(candidate, "utf8").catch(() => null);
|
|
110
|
+
if (source !== null && source.includes("<VendoOverlay"))
|
|
111
|
+
wrapperWithOverlay = true;
|
|
112
|
+
}
|
|
113
|
+
const scanned = await detectVendoWiring(root, { exclude: wrapperCandidates });
|
|
114
|
+
const wiring = { ...scanned, surface: scanned.surface || (scanned.client && wrapperWithOverlay) };
|
|
96
115
|
if (framework === "express") {
|
|
97
|
-
const wiring = await detectVendoWiring(root);
|
|
98
116
|
if (wiring.server)
|
|
99
117
|
pass("wiring/express-server", "Express server is wired");
|
|
100
118
|
else
|
|
@@ -113,21 +131,32 @@ export async function runDoctor(options) {
|
|
|
113
131
|
pass("wiring/next-route", "catch-all handler is wired");
|
|
114
132
|
else
|
|
115
133
|
fail("wiring/next-route", "E-WIRE-003", "missing app/api/vendo/[...vendo]/route.ts");
|
|
116
|
-
|
|
134
|
+
// The mount may live in ANY layout, not just the root one (i18n/route-group
|
|
135
|
+
// hosts mount in e.g. app/[locale]/layout.tsx — the literal root-layout
|
|
136
|
+
// grep fought exactly that correct wiring in the 0.4.1 E2E cert), and the
|
|
137
|
+
// correct scaffold mounts the generated vendo/vendo-root.tsx wrapper —
|
|
138
|
+
// whose export is also named VendoRoot, so the marker holds there too.
|
|
117
139
|
let rootWired = false;
|
|
118
|
-
for (const
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
for (const appDir of [join(root, "app"), join(root, "src", "app")]) {
|
|
141
|
+
for (const path of await walk(appDir, (rel) => /(^|[\\/])layout\.(?:tsx|jsx|js)$/.test(rel))) {
|
|
142
|
+
const source = await readFile(path, "utf8").catch(() => "");
|
|
143
|
+
if (source.includes("<VendoRoot") || source.includes("<VendoProvider"))
|
|
121
144
|
rootWired = true;
|
|
122
145
|
}
|
|
123
|
-
catch {
|
|
124
|
-
// Try the other layout convention.
|
|
125
|
-
}
|
|
126
146
|
}
|
|
127
147
|
if (rootWired)
|
|
128
148
|
pass("wiring/next-root", "<VendoRoot> wraps the app");
|
|
129
149
|
else
|
|
130
|
-
fail("wiring/next-root", "E-WIRE-004", "
|
|
150
|
+
fail("wiring/next-root", "E-WIRE-004", "no app layout mounts <VendoRoot> — wrap the app in the generated vendo/vendo-root.tsx wrapper (its export is also named VendoRoot), in the root layout or any layout that covers your pages");
|
|
151
|
+
}
|
|
152
|
+
// Visible surface (0.4.1 E2E cert B3): <VendoRoot> is a context provider
|
|
153
|
+
// that renders NOTHING — two certified stacks ended doctor-green with no
|
|
154
|
+
// way for a user to reach the agent. Green must mean visible.
|
|
155
|
+
if (wiring.surface) {
|
|
156
|
+
pass("wiring/surface", "a visible agent surface is mounted (<VendoOverlay /> or an equivalent)");
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
fail("wiring/surface", "E-WIRE-006", "no visible agent surface is mounted — <VendoRoot> renders nothing by itself; mount <VendoOverlay /> (init generates vendo/vendo-root.tsx for this), or render your own surface (<VendoThread />, <VendoToolResult>, the BYO embeds)");
|
|
131
160
|
}
|
|
132
161
|
if (await hasDependency(root))
|
|
133
162
|
pass("wiring/dependency", "@vendoai/vendo dependency is declared");
|
|
@@ -235,6 +264,18 @@ export async function runDoctor(options) {
|
|
|
235
264
|
else {
|
|
236
265
|
pass("live/status", `/status live round-trip (${body.version}, ${body.posture})`);
|
|
237
266
|
liveComposition = true;
|
|
267
|
+
// Split-brain guard (0.4.2 re-run, invoify defect 13): a direct
|
|
268
|
+
// @vendoai/vendo dependency pinned to an older range beats the vendoai
|
|
269
|
+
// umbrella's for the APP import, so `npm install vendoai@latest` runs a
|
|
270
|
+
// new CLI while /status silently serves the old runtime. Any CLI/wire
|
|
271
|
+
// version disagreement — split-brain or just a dev server started
|
|
272
|
+
// before the upgrade — means doctor is not certifying what users run.
|
|
273
|
+
if (body.version === CLI_VERSION) {
|
|
274
|
+
pass("deps/version-skew", `CLI and running wire agree on @vendoai/vendo ${CLI_VERSION}`);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
fail("deps/version-skew", "E-DEP-002", `the running wire serves @vendoai/vendo ${body.version} but this CLI is ${CLI_VERSION} — likely a split-brain install (a direct @vendoai/vendo dependency pinned to an older range wins over the vendoai umbrella's). Fix: npm install @vendoai/vendo@${CLI_VERSION} (or remove the direct @vendoai/vendo dependency and reinstall), then restart the dev server and re-run doctor.`);
|
|
278
|
+
}
|
|
238
279
|
// 10-mcp §1 — the door flag lives under blocks.mcp.
|
|
239
280
|
mcpEnabled = body.blocks.mcp === true;
|
|
240
281
|
sandboxVenue = body.blocks.sandbox;
|
|
@@ -254,7 +295,26 @@ export async function runDoctor(options) {
|
|
|
254
295
|
}
|
|
255
296
|
}
|
|
256
297
|
catch {
|
|
257
|
-
fail("live/status", "E-LIVE-002", `/status is unreachable at ${statusUrl}/status`);
|
|
298
|
+
fail("live/status", "E-LIVE-002", `/status is unreachable at ${statusUrl}/status — doctor expects the WIRE BASE (your app origin plus the mount path, e.g. http://localhost:3000/api/vendo); a bare site origin passed to --url is missing the /api/vendo part`);
|
|
299
|
+
}
|
|
300
|
+
// Render gate (0.4.1 E2E cert M3): a live wire proves nothing about the
|
|
301
|
+
// PAGES — the certified invoify install had every page 500ing (registry
|
|
302
|
+
// passed across the Server Component boundary) while doctor exited 0. One
|
|
303
|
+
// cheap GET of the app root catches a site that is down for users.
|
|
304
|
+
if (liveComposition) {
|
|
305
|
+
try {
|
|
306
|
+
const response = await fetchImpl(`${new URL(statusUrl).origin}/`, { headers: { accept: "text/html" } });
|
|
307
|
+
if (response.status >= 500) {
|
|
308
|
+
fail("live/render", "E-LIVE-006", `the app's root page returned ${response.status} — the site is crashing for users even though the wire answers (typical cause: the component registry imported in a Server Component layout; mount it via the generated vendo/vendo-root.tsx wrapper instead). Check the dev server log.`);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
pass("live/render", `the app's root page renders (HTTP ${response.status})`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// The wire answered but the origin root didn't resolve at all — hosts
|
|
316
|
+
// that serve no page at / are not doctor's business; skip silently.
|
|
317
|
+
}
|
|
258
318
|
}
|
|
259
319
|
if (!liveComposition) {
|
|
260
320
|
fail("auth/present", "E-AUTH-003", `present credential probe cannot run; start the dev server at ${statusUrl} and retry`);
|
package/dist/cli/framework.d.ts
CHANGED
|
@@ -2,7 +2,22 @@ export type HostFramework = "next" | "express" | "unknown";
|
|
|
2
2
|
export interface VendoWiring {
|
|
3
3
|
server: boolean;
|
|
4
4
|
client: boolean;
|
|
5
|
+
/** A VISIBLE agent surface is mounted — <VendoRoot> alone is a context
|
|
6
|
+
provider that renders nothing (0.4.1 E2E cert B3: by-the-book installs
|
|
7
|
+
ended doctor-green with nothing on screen). */
|
|
8
|
+
surface: boolean;
|
|
5
9
|
}
|
|
10
|
+
/** What counts as a visible surface: the shipped chrome (<VendoOverlay> and
|
|
11
|
+
the pieces it is built from), the BYO embeds a host chat renders, and the
|
|
12
|
+
hooks a host uses to drive a custom surface. Deliberately generous — this
|
|
13
|
+
list gates a doctor FAILURE, so a host with any plausible surface of its
|
|
14
|
+
own must pass. */
|
|
15
|
+
export declare const SURFACE_MARKERS: readonly string[];
|
|
6
16
|
export declare function detectFramework(root: string): Promise<HostFramework>;
|
|
7
|
-
/** Bounded source scan shared by init and doctor so their wiring verdicts
|
|
8
|
-
|
|
17
|
+
/** Bounded source scan shared by init and doctor so their wiring verdicts
|
|
18
|
+
agree. `exclude` skips generated files whose own markers would count as
|
|
19
|
+
host wiring (init's layout decision excludes the vendo-root wrapper: its
|
|
20
|
+
<VendoOverlay /> is only real once a layout mounts the wrapper itself). */
|
|
21
|
+
export declare function detectVendoWiring(root: string, options?: {
|
|
22
|
+
exclude?: string[];
|
|
23
|
+
}): Promise<VendoWiring>;
|
package/dist/cli/framework.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { walk } from "./theme/walk.js";
|
|
4
|
+
/** What counts as a visible surface: the shipped chrome (<VendoOverlay> and
|
|
5
|
+
the pieces it is built from), the BYO embeds a host chat renders, and the
|
|
6
|
+
hooks a host uses to drive a custom surface. Deliberately generous — this
|
|
7
|
+
list gates a doctor FAILURE, so a host with any plausible surface of its
|
|
8
|
+
own must pass. */
|
|
9
|
+
export const SURFACE_MARKERS = [
|
|
10
|
+
"<VendoOverlay",
|
|
11
|
+
"<VendoThread",
|
|
12
|
+
"<VendoTrigger",
|
|
13
|
+
"<VendoPalette",
|
|
14
|
+
"<VendoSlot",
|
|
15
|
+
"<VendoAppEmbed",
|
|
16
|
+
"<VendoApprovalEmbed",
|
|
17
|
+
"<VendoToolResult",
|
|
18
|
+
"useVendoOverlay(",
|
|
19
|
+
"useVendoThread(",
|
|
20
|
+
"useSlotApp(",
|
|
21
|
+
];
|
|
4
22
|
const SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/;
|
|
5
23
|
const SOURCE_SCAN_MAX_FILES = 2_000;
|
|
6
24
|
export async function detectFramework(root) {
|
|
@@ -17,20 +35,29 @@ export async function detectFramework(root) {
|
|
|
17
35
|
return "unknown";
|
|
18
36
|
}
|
|
19
37
|
}
|
|
20
|
-
/** Bounded source scan shared by init and doctor so their wiring verdicts
|
|
21
|
-
|
|
38
|
+
/** Bounded source scan shared by init and doctor so their wiring verdicts
|
|
39
|
+
agree. `exclude` skips generated files whose own markers would count as
|
|
40
|
+
host wiring (init's layout decision excludes the vendo-root wrapper: its
|
|
41
|
+
<VendoOverlay /> is only real once a layout mounts the wrapper itself). */
|
|
42
|
+
export async function detectVendoWiring(root, options = {}) {
|
|
22
43
|
let server = false;
|
|
23
44
|
let client = false;
|
|
45
|
+
let surface = false;
|
|
46
|
+
const excluded = new Set(options.exclude ?? []);
|
|
24
47
|
const files = await walk(root, (relativePath) => SOURCE_FILE.test(relativePath), SOURCE_SCAN_MAX_FILES);
|
|
25
48
|
for (const file of files) {
|
|
49
|
+
if (excluded.has(file))
|
|
50
|
+
continue;
|
|
26
51
|
const source = await readFile(file, "utf8").catch(() => "");
|
|
27
52
|
const code = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
28
53
|
if (code.includes("@vendoai/vendo/server") && /\bcreateVendo\s*\(/.test(code))
|
|
29
54
|
server = true;
|
|
30
|
-
if (code.includes("<VendoRoot"))
|
|
55
|
+
if (code.includes("<VendoRoot") || code.includes("<VendoProvider"))
|
|
31
56
|
client = true;
|
|
32
|
-
if (
|
|
57
|
+
if (SURFACE_MARKERS.some((marker) => code.includes(marker)))
|
|
58
|
+
surface = true;
|
|
59
|
+
if (server && client && surface)
|
|
33
60
|
break;
|
|
34
61
|
}
|
|
35
|
-
return { server, client };
|
|
62
|
+
return { server, client, surface };
|
|
36
63
|
}
|
|
@@ -8,6 +8,28 @@ export declare function authConfigLines(auth: AuthMatch): string;
|
|
|
8
8
|
as `catalog` (data fields only), `<VendoRoot components={registry}>` reads
|
|
9
9
|
the component references. Generated only while absent — never clobbered. */
|
|
10
10
|
export declare function registrySource(variant: "tsx" | "mjs"): string;
|
|
11
|
+
/** The anonymous-composition principal line (no auth preset wired). The
|
|
12
|
+
subject matches the demo principal both existing-agents quickstarts set in
|
|
13
|
+
their chat routes — the wire route MUST resolve the same subject as the
|
|
14
|
+
host's agent loop, or every app/approval created in chat is invisible to
|
|
15
|
+
the embeds, which call this route directly (0.4.1 E2E cert blocker B4:
|
|
16
|
+
a `() => null` wire against a demo-user chat route rendered an infinite
|
|
17
|
+
skeleton). Replaced wholesale when an auth preset is wired. */
|
|
18
|
+
export declare function anonymousPrincipalLines(): string;
|
|
19
|
+
/**
|
|
20
|
+
* The client mount wrapper (visible-surface fix, 0.4.1 E2E cert B3/M3): one
|
|
21
|
+
* generated "use client" boundary owns the registry + theme imports and
|
|
22
|
+
* mounts `<VendoOverlay />` — the launcher pill + panel end users actually
|
|
23
|
+
* see. The registry holds component references, so importing it in a Server
|
|
24
|
+
* Component layout and passing it into the client provider fails RSC
|
|
25
|
+
* serialization ("Only plain objects can be passed to Client Components");
|
|
26
|
+
* this wrapper is the layout-safe mount. Generated only while absent.
|
|
27
|
+
*/
|
|
28
|
+
export declare function vendoRootWrapperSource(options: {
|
|
29
|
+
/** Posix-style import specifier from the wrapper's dir to .vendo/theme.json,
|
|
30
|
+
or null when resolveJsonModule is explicitly disabled. */
|
|
31
|
+
themeSpecifier: string | null;
|
|
32
|
+
}): string;
|
|
11
33
|
export declare function routeSource(options: {
|
|
12
34
|
serverActions: boolean;
|
|
13
35
|
auth: AuthMatch | null;
|
|
@@ -48,6 +48,56 @@ export function registrySource(variant) {
|
|
|
48
48
|
? `${header}import type { ComponentRegistry } from "@vendoai/vendo";\n\nexport const registry = {} satisfies ComponentRegistry;\n`
|
|
49
49
|
: `${header}export const registry = {};\n`;
|
|
50
50
|
}
|
|
51
|
+
/** The anonymous-composition principal line (no auth preset wired). The
|
|
52
|
+
subject matches the demo principal both existing-agents quickstarts set in
|
|
53
|
+
their chat routes — the wire route MUST resolve the same subject as the
|
|
54
|
+
host's agent loop, or every app/approval created in chat is invisible to
|
|
55
|
+
the embeds, which call this route directly (0.4.1 E2E cert blocker B4:
|
|
56
|
+
a `() => null` wire against a demo-user chat route rendered an infinite
|
|
57
|
+
skeleton). Replaced wholesale when an auth preset is wired. */
|
|
58
|
+
export function anonymousPrincipalLines() {
|
|
59
|
+
return ` // Who the wire's callers act as. This must resolve the SAME subject your\n` +
|
|
60
|
+
` // agent loop uses (the docs' chat routes set this demo principal), or apps\n` +
|
|
61
|
+
` // and approvals created in chat are invisible to the embeds, which call\n` +
|
|
62
|
+
` // this route directly. Replace both sides with your real session lookup.\n` +
|
|
63
|
+
` principal: async () => ({ kind: "user" as const, subject: "demo-user" }),\n`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The client mount wrapper (visible-surface fix, 0.4.1 E2E cert B3/M3): one
|
|
67
|
+
* generated "use client" boundary owns the registry + theme imports and
|
|
68
|
+
* mounts `<VendoOverlay />` — the launcher pill + panel end users actually
|
|
69
|
+
* see. The registry holds component references, so importing it in a Server
|
|
70
|
+
* Component layout and passing it into the client provider fails RSC
|
|
71
|
+
* serialization ("Only plain objects can be passed to Client Components");
|
|
72
|
+
* this wrapper is the layout-safe mount. Generated only while absent.
|
|
73
|
+
*/
|
|
74
|
+
export function vendoRootWrapperSource(options) {
|
|
75
|
+
const withTheme = options.themeSpecifier !== null;
|
|
76
|
+
return `"use client";\n\n` +
|
|
77
|
+
`/**\n` +
|
|
78
|
+
` * The Vendo client mount — generated by \`vendo init\`, then yours.\n` +
|
|
79
|
+
` * This "use client" boundary owns the registry${withTheme ? " and theme" : ""} imports: the\n` +
|
|
80
|
+
` * registry carries component references, which cannot cross a Server\n` +
|
|
81
|
+
` * Component boundary as props (RSC serialization). <VendoOverlay /> is the\n` +
|
|
82
|
+
` * visible surface — the launcher pill + panel. Swap it for your own\n` +
|
|
83
|
+
` * surface (<VendoThread />, the BYO embeds) whenever you like.\n` +
|
|
84
|
+
` */\n` +
|
|
85
|
+
`import { VendoOverlay, VendoRoot as VendoClientRoot } from "@vendoai/vendo/react";\n` +
|
|
86
|
+
`import type { ReactNode } from "react";\n` +
|
|
87
|
+
`import { registry } from "./registry";\n` +
|
|
88
|
+
(withTheme
|
|
89
|
+
? `import theme from ${JSON.stringify(options.themeSpecifier)};\n` +
|
|
90
|
+
`import type { VendoTheme } from "@vendoai/vendo";\n`
|
|
91
|
+
: "") +
|
|
92
|
+
`\nexport function VendoRoot({ children }: { children: ReactNode }) {\n` +
|
|
93
|
+
` return (\n` +
|
|
94
|
+
` <VendoClientRoot components={registry}${withTheme ? " theme={theme as VendoTheme}" : ""}>\n` +
|
|
95
|
+
` {children}\n` +
|
|
96
|
+
` <VendoOverlay />\n` +
|
|
97
|
+
` </VendoClientRoot>\n` +
|
|
98
|
+
` );\n` +
|
|
99
|
+
`}\n`;
|
|
100
|
+
}
|
|
51
101
|
/** The preset's own import line (its own subpath, never "@vendoai/vendo/server"
|
|
52
102
|
— corpus-triage Task 9: a shared barrel meant any host importing the
|
|
53
103
|
server entry statically re-resolved every preset's optional peer dep,
|
|
@@ -61,7 +111,7 @@ export function routeSource(options) {
|
|
|
61
111
|
(options.serverActions ? `import { serverActions } from "./vendo-actions";\n` : "") +
|
|
62
112
|
`import { registry } from ${JSON.stringify(options.registrySpecifier)};\n` +
|
|
63
113
|
`\nconst vendo = createVendo({\n` +
|
|
64
|
-
(options.auth === null ?
|
|
114
|
+
(options.auth === null ? anonymousPrincipalLines() : authConfigLines(options.auth)) +
|
|
65
115
|
` catalog: registry,\n` +
|
|
66
116
|
(options.serverActions ? ` serverActions,\n` : "") +
|
|
67
117
|
` policy: {}, // .vendo/policy.json: destructive asks, reads run\n` +
|
|
@@ -143,17 +193,19 @@ export function expressServerSource(typescript, auth = null) {
|
|
|
143
193
|
const registrySpecifier = typescript ? "./registry" : "./registry.mjs";
|
|
144
194
|
const clientHint = typescript
|
|
145
195
|
? ` * // in the client entry — theme.json adopts the host brand (08 §4);\n` +
|
|
146
|
-
` * // the cast narrows TypeScript's widened JSON-module string literals
|
|
147
|
-
` *
|
|
196
|
+
` * // the cast narrows TypeScript's widened JSON-module string literals;\n` +
|
|
197
|
+
` * // <VendoOverlay /> is the visible surface (launcher pill + panel):\n` +
|
|
198
|
+
` * import { VendoOverlay, VendoRoot } from "@vendoai/vendo/react";\n` +
|
|
148
199
|
` * import { registry } from "<path-to>/vendo/registry";\n` +
|
|
149
200
|
` * import theme from "<path-to>/.vendo/theme.json";\n` +
|
|
150
201
|
` * import type { VendoTheme } from "@vendoai/vendo";\n` +
|
|
151
|
-
` * root.render(<VendoRoot components={registry} theme={theme as VendoTheme}><App /></VendoRoot>);\n`
|
|
152
|
-
: ` * // in the client entry — theme.json adopts the host brand (08 §4)
|
|
153
|
-
` *
|
|
202
|
+
` * root.render(<VendoRoot components={registry} theme={theme as VendoTheme}><App /><VendoOverlay /></VendoRoot>);\n`
|
|
203
|
+
: ` * // in the client entry — theme.json adopts the host brand (08 §4);\n` +
|
|
204
|
+
` * // <VendoOverlay /> is the visible surface (launcher pill + panel):\n` +
|
|
205
|
+
` * import { VendoOverlay, VendoRoot } from "@vendoai/vendo/react";\n` +
|
|
154
206
|
` * import { registry } from "<path-to>/vendo/registry.mjs";\n` +
|
|
155
207
|
` * import theme from "<path-to>/.vendo/theme.json";\n` +
|
|
156
|
-
` * root.render(<VendoRoot components={registry} theme={theme}><App /></VendoRoot>);\n`;
|
|
208
|
+
` * root.render(<VendoRoot components={registry} theme={theme}><App /><VendoOverlay /></VendoRoot>);\n`;
|
|
157
209
|
return `/**\n` +
|
|
158
210
|
` * Add these wiring lines in your host:\n` +
|
|
159
211
|
` * app.use("/api/vendo", mountVendo());\n` +
|
|
@@ -165,7 +217,7 @@ export function expressServerSource(typescript, auth = null) {
|
|
|
165
217
|
`import { registry } from ${JSON.stringify(registrySpecifier)};\n` +
|
|
166
218
|
types +
|
|
167
219
|
`\nconst vendo = createVendo({\n` +
|
|
168
|
-
(auth === null ?
|
|
220
|
+
(auth === null ? anonymousPrincipalLines() : authConfigLines(auth)) +
|
|
169
221
|
` catalog: registry,\n` +
|
|
170
222
|
` policy: {}, // .vendo/policy.json: destructive asks, reads run\n` +
|
|
171
223
|
`});\n\n` +
|
package/dist/cli/init.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ export interface InitPlan {
|
|
|
21
21
|
path: string;
|
|
22
22
|
diff: string;
|
|
23
23
|
}>;
|
|
24
|
-
/**
|
|
24
|
+
/** Whatever wiring the run could not do safely itself (empty when the
|
|
25
|
+
layout auto-wire landed): the paste lines for the user. */
|
|
25
26
|
manualSteps: string[];
|
|
26
27
|
/** --agent only: deterministic extraction results, so an agent can act on
|
|
27
28
|
real tool names instead of re-deriving them. */
|
|
@@ -114,5 +115,14 @@ export interface StarProcess {
|
|
|
114
115
|
repo URL instead, one line, no error noise. Exported for the timeout's
|
|
115
116
|
direct unit test only. */
|
|
116
117
|
export declare function starViaGh(spawnStar: NonNullable<InitOptions["spawnStar"]>, timeoutMs?: number): Promise<boolean>;
|
|
118
|
+
/**
|
|
119
|
+
* The one bounded edit init makes to user-authored code (visible-surface fix,
|
|
120
|
+
* 0.4.1 E2E cert B3): mount the generated client wrapper around the layout's
|
|
121
|
+
* `{children}` and import it. Null — leave the file untouched, fall back to
|
|
122
|
+
* the printed paste — whenever a Vendo mount already exists (idempotence) or
|
|
123
|
+
* the file doesn't have exactly one JSX `{children}` to wrap (ambiguity).
|
|
124
|
+
* Exported for direct unit tests.
|
|
125
|
+
*/
|
|
126
|
+
export declare function wireNextLayout(source: string, wrapperSpecifier: string): string | null;
|
|
117
127
|
/** 09-vendo §5 — idempotent, zero-question setup. */
|
|
118
128
|
export declare function runInit(options: InitOptions): Promise<number>;
|