@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
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { mergeOverrides, vendoSync, } from "@vendoai/actions";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { stdin, stdout } from "node:process";
|
|
8
|
+
import { scrubErrorDetail } from "@vendoai/telemetry";
|
|
9
|
+
import { detectDepVersions, installedAiVersion } from "./dep-versions.js";
|
|
10
|
+
import { AUTH_MD_URL, runCloudStep, upsertEnvLocal } from "./cloud-init.js";
|
|
11
|
+
import { APPLY_COMMAND, composeDelegatedInstructions, EXTRACTION_DRAFT_JSON_SCHEMA } from "./extract/delegate.js";
|
|
12
|
+
import { askYesNo, runAiExtraction } from "./extract/extraction.js";
|
|
13
|
+
import { BRIEF_TEMPLATE } from "./extract/stages.js";
|
|
14
|
+
import { ENV_KEY_VARS, resolveDevCredential, describeDevCredential } from "../dev-creds/resolve.js";
|
|
15
|
+
import { detectFramework, detectVendoWiring } from "./framework.js";
|
|
16
|
+
import { resolveScaffoldAuth } from "./init-auth.js";
|
|
17
|
+
import { expressServerSource, registrySource, routeSource, serverActionsModuleSource, VENDO_ENV_EXAMPLE, wiringServerActions, } from "./init-scaffolds.js";
|
|
18
|
+
import { createPrettyOutput, plainSelect, usePrettyOutput } from "./pretty.js";
|
|
19
|
+
import { contrastingText } from "./theme/color.js";
|
|
20
|
+
import { applyThemeDraft, extractTheme as extractThemeSlots, validateSlotValue, } from "./theme/extract-theme.js";
|
|
21
|
+
import { cloudProjectProps, consoleOutput, envLocalValueSync, errorClass, exists, readOptional, toolingTelemetry, writeText, } from "./shared.js";
|
|
22
|
+
/**
|
|
23
|
+
* `vendo init` (install-dx v1, re-derived 2026-07-18): one command, zero
|
|
24
|
+
* questions on the happy path, no ceremony.
|
|
25
|
+
*
|
|
26
|
+
* scan → wire (the two-file surface — empty vendo/registry.tsx + the
|
|
27
|
+
* catch-all handler wired to it; a detected auth preset gets one
|
|
28
|
+
* consent-style confirm in interactive runs, --yes/non-interactive accept
|
|
29
|
+
* it silently — plus package.json hooks; never edits user-authored code)
|
|
30
|
+
* → key (env stated, else the cloud starter offer) → done summary (files
|
|
31
|
+
* changed, the VendoRoot line to paste, next steps).
|
|
32
|
+
*
|
|
33
|
+
* Removed by design: the interview, per-diff y/N approvals, the layout
|
|
34
|
+
* codemod, the lib/ai.ts scaffold (createVendo's `model` is optional now),
|
|
35
|
+
* remix offers, the encryption-key step, the refine offer, and the finale
|
|
36
|
+
* ceremony (doctor owns verification and the live turn).
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULT_RADIUS = { small: "4px", large: "12px" };
|
|
39
|
+
const BRIEF_PLACEHOLDER = `${BRIEF_TEMPLATE}\n`;
|
|
40
|
+
function toVendoTheme(slots) {
|
|
41
|
+
const deriveRadius = (factor, fallback) => {
|
|
42
|
+
const value = slots.radius.match(/^(\d+(?:\.\d+)?)px$/)?.[1];
|
|
43
|
+
return value === undefined ? fallback : `${Number(value) * factor}px`;
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
colors: {
|
|
47
|
+
background: slots.background,
|
|
48
|
+
surface: slots.surface,
|
|
49
|
+
text: slots.text,
|
|
50
|
+
muted: slots.mutedText,
|
|
51
|
+
accent: slots.accent,
|
|
52
|
+
accentText: slots.accentText,
|
|
53
|
+
danger: slots.danger,
|
|
54
|
+
border: slots.border,
|
|
55
|
+
},
|
|
56
|
+
typography: {
|
|
57
|
+
fontFamily: slots.fontFamily,
|
|
58
|
+
headingFamily: slots.headingFamily,
|
|
59
|
+
baseSize: slots.baseSize,
|
|
60
|
+
},
|
|
61
|
+
radius: {
|
|
62
|
+
small: deriveRadius(0.5, DEFAULT_RADIUS.small),
|
|
63
|
+
medium: slots.radius,
|
|
64
|
+
large: deriveRadius(1.5, DEFAULT_RADIUS.large),
|
|
65
|
+
},
|
|
66
|
+
density: slots.density,
|
|
67
|
+
motion: slots.motion,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const THEME_PALETTE_SLOTS = ["accent", "background", "surface", "text", "mutedText", "border", "danger"];
|
|
71
|
+
/** ANSI truecolor swatch when interactive; plain hex otherwise. */
|
|
72
|
+
function swatch(hex) {
|
|
73
|
+
const match = /^#([0-9a-f]{6})$/i.exec(hex);
|
|
74
|
+
if (!match || !stdout.isTTY)
|
|
75
|
+
return "";
|
|
76
|
+
const [r, g, b] = [0, 2, 4].map((index) => parseInt(match[1].slice(index, index + 2), 16));
|
|
77
|
+
return `\u001b[48;2;${r};${g};${b}m \u001b[0m `;
|
|
78
|
+
}
|
|
79
|
+
/** One-glance confirm (§B2): the extracted palette, where each slot came
|
|
80
|
+
from is visible in defaulted/errors, and theme.json stays the editable
|
|
81
|
+
source of truth. */
|
|
82
|
+
function printThemeSummary(summary, output) {
|
|
83
|
+
const palette = THEME_PALETTE_SLOTS
|
|
84
|
+
.map((slot) => `${swatch(summary.slots[slot])}${slot} ${summary.slots[slot]}`)
|
|
85
|
+
.join(" · ");
|
|
86
|
+
output.log(`Theme: ${palette}`);
|
|
87
|
+
const headings = summary.slots.headingFamily === summary.slots.fontFamily
|
|
88
|
+
? ""
|
|
89
|
+
: ` · headings ${summary.slots.headingFamily}`;
|
|
90
|
+
output.log(`Type: ${summary.slots.fontFamily}${headings} · radius ${summary.slots.radius}`);
|
|
91
|
+
const missing = summary.defaulted.filter((slot) => THEME_PALETTE_SLOTS.includes(slot) || slot === "fontFamily");
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
output.log(`No host evidence for ${missing.join(", ")} — neutral defaults used.`);
|
|
94
|
+
}
|
|
95
|
+
for (const error of summary.errors)
|
|
96
|
+
output.error(`warning: ${error}`);
|
|
97
|
+
output.log("Theme lives in .vendo/theme.json — edit it anytime; it is the source of truth.");
|
|
98
|
+
}
|
|
99
|
+
/** Interactive review of model-flagged uncertain slots (the ONLY theme question). */
|
|
100
|
+
async function defaultThemeReview(summary) {
|
|
101
|
+
if (!stdin.isTTY || !stdout.isTTY)
|
|
102
|
+
return {};
|
|
103
|
+
const prompt = createInterface({ input: stdin, output: stdout });
|
|
104
|
+
const overrides = {};
|
|
105
|
+
try {
|
|
106
|
+
for (const { slot, note } of summary.uncertain) {
|
|
107
|
+
const answer = (await prompt.question(`Theme ${slot} is uncertain (${note}); extracted ${summary.slots[slot]}. Replacement value, or Enter to keep: `)).trim();
|
|
108
|
+
if (answer !== "")
|
|
109
|
+
overrides[slot] = answer;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
prompt.close();
|
|
114
|
+
}
|
|
115
|
+
return overrides;
|
|
116
|
+
}
|
|
117
|
+
async function appDirectory(root) {
|
|
118
|
+
if (await exists(join(root, "src", "app")))
|
|
119
|
+
return join(root, "src", "app");
|
|
120
|
+
return join(root, "app");
|
|
121
|
+
}
|
|
122
|
+
/** Telemetry `router` enum (init_completed): app | pages | none, from the
|
|
123
|
+
same directory evidence appDirectory rides. Express hosts are "none". */
|
|
124
|
+
async function detectRouter(root, framework) {
|
|
125
|
+
if (framework === "next") {
|
|
126
|
+
if (await exists(join(root, "src", "app")) || await exists(join(root, "app")))
|
|
127
|
+
return "app";
|
|
128
|
+
if (await exists(join(root, "src", "pages")) || await exists(join(root, "pages")))
|
|
129
|
+
return "pages";
|
|
130
|
+
}
|
|
131
|
+
return "none";
|
|
132
|
+
}
|
|
133
|
+
/** Relative, posix-style import specifier from the layout's directory to the
|
|
134
|
+
project-root `.vendo/theme.json` — printed for the user's paste, never
|
|
135
|
+
written by init. Returns null when the project EXPLICITLY disables
|
|
136
|
+
resolveJsonModule, so the printed snippet compiles. */
|
|
137
|
+
async function themeImportSpecifier(root, layoutDir) {
|
|
138
|
+
if (await resolveJsonModuleDisabled(root))
|
|
139
|
+
return null;
|
|
140
|
+
const themeJson = join(root, ".vendo", "theme.json");
|
|
141
|
+
return relative(layoutDir, themeJson).split(sep).join("/");
|
|
142
|
+
}
|
|
143
|
+
/** True only when tsconfig/jsconfig EXPLICITLY sets
|
|
144
|
+
`compilerOptions.resolveJsonModule === false` — the one case where importing
|
|
145
|
+
theme.json breaks the build. */
|
|
146
|
+
async function resolveJsonModuleDisabled(root) {
|
|
147
|
+
for (const file of ["tsconfig.json", "jsconfig.json"]) {
|
|
148
|
+
const raw = await readOptional(join(root, file));
|
|
149
|
+
if (raw === null)
|
|
150
|
+
continue;
|
|
151
|
+
try {
|
|
152
|
+
const config = JSON.parse(raw);
|
|
153
|
+
if (config.compilerOptions?.resolveJsonModule === false)
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Malformed config — assume the default (enabled).
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
function diff(path, before, after) {
|
|
163
|
+
const oldLines = before === null ? [] : before.trimEnd().split("\n");
|
|
164
|
+
const newLines = after.trimEnd().split("\n");
|
|
165
|
+
return [
|
|
166
|
+
`--- a/${path}`,
|
|
167
|
+
`+++ b/${path}`,
|
|
168
|
+
...oldLines.map((line) => `-${line}`),
|
|
169
|
+
...newLines.map((line) => `+${line}`),
|
|
170
|
+
].join("\n");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Wire the server-action registration map into an EXISTING generated route
|
|
174
|
+
* (ENG-248 idempotency fix): a host that adds `"use server"` actions AFTER the
|
|
175
|
+
* initial init gets vendo-actions.ts generated, but its route.ts still calls
|
|
176
|
+
* `createVendo` without `serverActions` — so every server-action call fails
|
|
177
|
+
* closed at runtime. Best-effort: rewrites only the recognized
|
|
178
|
+
* `createVendo({ ... })` shape; returns null when already wired or the shape is
|
|
179
|
+
* unrecognized (never corrupts a hand-customized route). Idempotent: a route
|
|
180
|
+
* that already imports and passes serverActions yields null.
|
|
181
|
+
*/
|
|
182
|
+
function wireRouteServerActions(source) {
|
|
183
|
+
const importsMap = /from\s+["']\.\/vendo-actions["']/.test(source);
|
|
184
|
+
const call = source.match(/createVendo\(\s*\{/);
|
|
185
|
+
if (!call)
|
|
186
|
+
return null; // unrecognized composition — leave it untouched
|
|
187
|
+
const callIndex = source.indexOf(call[0]);
|
|
188
|
+
const passesActions = /(^|[\s{,])serverActions\b/.test(source.slice(callIndex));
|
|
189
|
+
// Already wired, or hand-customized: a route that passes serverActions
|
|
190
|
+
// sourced from anywhere other than ./vendo-actions (a local map, an aliased
|
|
191
|
+
// import) would get a conflicting duplicate binding from our import — leave
|
|
192
|
+
// it untouched either way.
|
|
193
|
+
if (passesActions)
|
|
194
|
+
return null;
|
|
195
|
+
let next = source;
|
|
196
|
+
if (!importsMap) {
|
|
197
|
+
const importLine = `import { serverActions } from "./vendo-actions";`;
|
|
198
|
+
const serverImport = next.match(/^.*from\s+["']@vendoai\/vendo\/server["'];?[^\n]*$/m);
|
|
199
|
+
if (serverImport) {
|
|
200
|
+
const at = next.indexOf(serverImport[0]) + serverImport[0].length;
|
|
201
|
+
next = `${next.slice(0, at)}\n${importLine}${next.slice(at)}`;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
next = `${importLine}\n${next}`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
next = next.replace(/createVendo\(\s*\{/, (match) => `${match}\n serverActions,`);
|
|
208
|
+
return next === source ? null : next;
|
|
209
|
+
}
|
|
210
|
+
function packageWithSyncHooks(raw) {
|
|
211
|
+
const manifest = JSON.parse(raw);
|
|
212
|
+
const priorScripts = manifest["scripts"];
|
|
213
|
+
const scripts = typeof priorScripts === "object" && priorScripts !== null && !Array.isArray(priorScripts)
|
|
214
|
+
? priorScripts
|
|
215
|
+
: {};
|
|
216
|
+
let changed = false;
|
|
217
|
+
const hook = (name, command) => {
|
|
218
|
+
const prior = scripts[name];
|
|
219
|
+
if (typeof prior !== "string") {
|
|
220
|
+
scripts[name] = command;
|
|
221
|
+
changed = true;
|
|
222
|
+
}
|
|
223
|
+
else if (!prior.includes(command)) {
|
|
224
|
+
scripts[name] = `${command} && ${prior}`;
|
|
225
|
+
changed = true;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
hook("predev", "vendo sync");
|
|
229
|
+
hook("prebuild", "vendo sync --strict");
|
|
230
|
+
if (!changed)
|
|
231
|
+
return null;
|
|
232
|
+
manifest["scripts"] = scripts;
|
|
233
|
+
const detectedIndent = raw.match(/^[\t ]+(?=")/m)?.[0] ?? " ";
|
|
234
|
+
const trailingNewline = raw.endsWith("\r\n") ? "\r\n" : raw.endsWith("\n") ? "\n" : "";
|
|
235
|
+
return `${JSON.stringify(manifest, null, detectedIndent)}${trailingNewline}`;
|
|
236
|
+
}
|
|
237
|
+
/** Read-only extraction for the agent plan. vendoSync writes its artifacts, so
|
|
238
|
+
it runs against a throwaway out dir — the host tree stays untouched (the
|
|
239
|
+
--agent contract). Existing overrides ride along so the plan reflects prior
|
|
240
|
+
human risk decisions, mirroring vendoSync's own merge semantics. */
|
|
241
|
+
async function extractForPlan(root) {
|
|
242
|
+
const out = await mkdtemp(join(tmpdir(), "vendo-agent-plan-"));
|
|
243
|
+
try {
|
|
244
|
+
const overridesRaw = await readOptional(join(root, ".vendo", "overrides.json"));
|
|
245
|
+
if (overridesRaw !== null)
|
|
246
|
+
await writeText(join(out, "overrides.json"), overridesRaw);
|
|
247
|
+
const report = await vendoSync({ root, out });
|
|
248
|
+
const file = JSON.parse(await readFile(join(out, "tools.json"), "utf8"));
|
|
249
|
+
let overrides = null;
|
|
250
|
+
try {
|
|
251
|
+
overrides = overridesRaw === null ? null : JSON.parse(overridesRaw);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// vendoSync already validated the copy; an unreadable original merges as absent.
|
|
255
|
+
}
|
|
256
|
+
return { tools: mergeOverrides(file.tools ?? [], overrides), warnings: report.warnings };
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
// The plan must always emit — extraction failures degrade to a warning.
|
|
260
|
+
return { tools: [], warnings: [`extraction failed: ${error instanceof Error ? error.message : "unknown error"}`] };
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
await rm(out, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Project extraction results onto the small static-facts shape the
|
|
267
|
+
delegation instructions carry (route bindings surface method+path). */
|
|
268
|
+
function planStaticTools(tools) {
|
|
269
|
+
return tools.map((tool) => {
|
|
270
|
+
const binding = tool.binding;
|
|
271
|
+
return {
|
|
272
|
+
name: tool.name,
|
|
273
|
+
description: tool.description,
|
|
274
|
+
risk: tool.risk,
|
|
275
|
+
...(tool.disabled === true ? { disabled: true } : {}),
|
|
276
|
+
...(typeof binding.method === "string" ? { method: binding.method } : {}),
|
|
277
|
+
...(typeof binding.path === "string" ? { path: binding.path } : {}),
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/** 04-actions §1 risk ladder projected as advice: destructive asks first,
|
|
282
|
+
writes get reviewed, reads auto-run (no entry). */
|
|
283
|
+
function riskRecommendations(tools) {
|
|
284
|
+
return tools.flatMap((tool) => {
|
|
285
|
+
if (tool.disabled === true) {
|
|
286
|
+
return [{ tool: tool.name, risk: tool.risk, recommendation: "extracted disabled (unclassifiable); enable it deliberately in .vendo/overrides.json after review" }];
|
|
287
|
+
}
|
|
288
|
+
if (tool.critical === true) {
|
|
289
|
+
return [{ tool: tool.name, risk: tool.risk, recommendation: "already marked critical in .vendo/overrides.json; policy asks before running it" }];
|
|
290
|
+
}
|
|
291
|
+
if (tool.risk === "destructive") {
|
|
292
|
+
return [{ tool: tool.name, risk: tool.risk, recommendation: "irreversible; mark it critical in .vendo/overrides.json so policy asks first" }];
|
|
293
|
+
}
|
|
294
|
+
if (tool.risk === "write") {
|
|
295
|
+
return [{ tool: tool.name, risk: tool.risk, recommendation: "writes host data; review it and mark critical in .vendo/overrides.json when irreversible" }];
|
|
296
|
+
}
|
|
297
|
+
return [];
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/** The packaged vendo-setup skill (shipped in the npm tarball next to dist/).
|
|
301
|
+
Resolved relative to this module so src (tests) and dist (published bin)
|
|
302
|
+
agree; a missing file degrades to not offering the skill. */
|
|
303
|
+
async function setupSkillSource() {
|
|
304
|
+
try {
|
|
305
|
+
return await readFile(new URL("../../skills/vendo-setup/SKILL.md", import.meta.url), "utf8");
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** The one line init never writes: the user pastes the VendoRoot wrap. When
|
|
312
|
+
the shared registry exists (scaffolded or host-authored) the wrap carries
|
|
313
|
+
`components={registry}` — the client half of the one-file/two-consumers
|
|
314
|
+
pattern; it stays ONE pasted line plus its imports. */
|
|
315
|
+
async function vendoRootPasteLines(root, framework, withRegistry) {
|
|
316
|
+
if (framework === "express") {
|
|
317
|
+
const wrap = withRegistry
|
|
318
|
+
? `<VendoRoot components={registry} theme={theme}>…</VendoRoot>`
|
|
319
|
+
: `<VendoRoot theme={theme}>…</VendoRoot>`;
|
|
320
|
+
return [
|
|
321
|
+
`app.use("/api/vendo", mountVendo()); // in your server`,
|
|
322
|
+
`${wrap} // around your client root (see vendo/server for the imports)`,
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
const app = await appDirectory(root);
|
|
326
|
+
const specifier = await themeImportSpecifier(root, app);
|
|
327
|
+
const layout = relative(root, join(app, "layout.tsx"));
|
|
328
|
+
const registrySpecifier = relative(app, join(dirname(app), "vendo", "registry")).split(sep).join("/");
|
|
329
|
+
const importLines = [
|
|
330
|
+
`import { VendoRoot } from "@vendoai/vendo/react";`,
|
|
331
|
+
...(withRegistry ? [`import { registry } from ${JSON.stringify(registrySpecifier)};`] : []),
|
|
332
|
+
...(specifier === null
|
|
333
|
+
? []
|
|
334
|
+
: [
|
|
335
|
+
`import theme from ${JSON.stringify(specifier)};`,
|
|
336
|
+
`import type { VendoTheme } from "@vendoai/vendo";`,
|
|
337
|
+
]),
|
|
338
|
+
];
|
|
339
|
+
const props = [
|
|
340
|
+
...(withRegistry ? ["components={registry}"] : []),
|
|
341
|
+
...(specifier === null ? [] : ["theme={theme as VendoTheme}"]),
|
|
342
|
+
];
|
|
343
|
+
const wrap = `<VendoRoot${props.length === 0 ? "" : ` ${props.join(" ")}`}>{children}</VendoRoot>`;
|
|
344
|
+
return [`In ${layout}:`, ...importLines.map((line) => ` ${line}`), ` … then wrap: ${wrap}`];
|
|
345
|
+
}
|
|
346
|
+
/** The repo-specific agent tail (agent-install-dx): a non-interactive
|
|
347
|
+
scaffold run is agent-driven, so the run ends with plain deterministic
|
|
348
|
+
pointers — the wired auth preset and what is still stubbed about it, the
|
|
349
|
+
exact files left to hand-edit (derived from what THIS run wrote, never
|
|
350
|
+
canned prose), and the one doctor command that gates "done". A pointer to
|
|
351
|
+
work, not documentation: the playbook carries the teaching. */
|
|
352
|
+
async function agentTailLines(args) {
|
|
353
|
+
const lines = [];
|
|
354
|
+
// Auth is a tail fact only when a composition was created this run — a
|
|
355
|
+
// re-run against an existing composition changed nothing about auth.
|
|
356
|
+
if (args.compositionPath !== null) {
|
|
357
|
+
if (args.authWired === null) {
|
|
358
|
+
lines.push("auth: none wired — sessions stay anonymous until a preset is added");
|
|
359
|
+
}
|
|
360
|
+
else if (args.authWired.source === "picked") {
|
|
361
|
+
lines.push(`auth: ${args.authWired.preset}() wired — stubbed: ${args.authWired.dependency} is not in package.json; install it before the first authenticated run`);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
lines.push(`auth: ${args.authWired.preset}() wired (detected ${args.authWired.dependency})`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (args.registryPath !== null) {
|
|
368
|
+
lines.push(`edit ${args.registryPath} — register the components the agent may render (generated empty)`);
|
|
369
|
+
}
|
|
370
|
+
if (args.compositionPath !== null && args.authWired === null) {
|
|
371
|
+
lines.push(`edit ${args.compositionPath} — add the auth preset named in the advisory above when the host has auth`);
|
|
372
|
+
}
|
|
373
|
+
if (args.framework === "express") {
|
|
374
|
+
// No exact entry file exists to name on Express — point at the printed
|
|
375
|
+
// wiring lines instead of guessing a path.
|
|
376
|
+
lines.push("edit your server and client entries — paste the mountVendo() and <VendoRoot> lines above");
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
const layout = relative(args.root, join(await appDirectory(args.root), "layout.tsx"));
|
|
380
|
+
lines.push(`edit ${layout} — wrap the app in the <VendoRoot> lines above`);
|
|
381
|
+
}
|
|
382
|
+
if (await readOptional(join(args.root, ".vendo", "brief.md")) === BRIEF_PLACEHOLDER) {
|
|
383
|
+
lines.push(`edit ${join(".vendo", "brief.md")} — replace the placeholder with what this product does and for whom`);
|
|
384
|
+
}
|
|
385
|
+
if (args.cloudKeyMissing) {
|
|
386
|
+
lines.push(`cloud key: none — for Vendo Cloud, fetch ${AUTH_MD_URL} and run \`vendo login\` (your human approves a code in the browser; the key lands in .env.local), then re-run init or pass --cloud-key <key>; --byo with a provider key also works`);
|
|
387
|
+
}
|
|
388
|
+
lines.push("gate: run `vendo doctor --json` — done when every check reports green");
|
|
389
|
+
return lines;
|
|
390
|
+
}
|
|
391
|
+
const STAR_REPO = "runvendo/vendo";
|
|
392
|
+
// Tracked star link (vendo-web star-worker): captures star_link_clicked
|
|
393
|
+
// {src: cli} server-side, then redirects to the repo.
|
|
394
|
+
const STAR_LINK = "https://vendo.run/star?src=cli";
|
|
395
|
+
/** Star the repo via gh (agent-install-dx §CLI-5). Every failure mode — gh
|
|
396
|
+
not installed (spawn error), a non-zero exit, a throwing seam, or a gh
|
|
397
|
+
that hangs past `timeoutMs` — is plain `false`: the caller prints the
|
|
398
|
+
repo URL instead, one line, no error noise. Exported for the timeout's
|
|
399
|
+
direct unit test only. */
|
|
400
|
+
export function starViaGh(spawnStar, timeoutMs = 5_000) {
|
|
401
|
+
return new Promise((resolveStar) => {
|
|
402
|
+
let timer = null;
|
|
403
|
+
const settle = (starred) => {
|
|
404
|
+
if (timer !== null)
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
resolveStar(starred);
|
|
407
|
+
};
|
|
408
|
+
let child;
|
|
409
|
+
try {
|
|
410
|
+
child = spawnStar("gh", ["api", "-X", "PUT", `user/starred/${STAR_REPO}`]);
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
settle(false);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
timer = setTimeout(() => settle(false), timeoutMs);
|
|
417
|
+
timer.unref?.();
|
|
418
|
+
child.on("error", () => settle(false));
|
|
419
|
+
child.on("exit", (code) => settle(code === 0));
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
async function buildPlan(options, confirmAuth, selectAuth) {
|
|
423
|
+
const root = resolve(options.targetDir);
|
|
424
|
+
const framework = options.framework ?? await detectFramework(root);
|
|
425
|
+
const changes = [];
|
|
426
|
+
let authAdvice = null;
|
|
427
|
+
let authWired = null;
|
|
428
|
+
let compositionPath = null;
|
|
429
|
+
let registryPath = null;
|
|
430
|
+
let withRegistry = false;
|
|
431
|
+
if (framework === "express") {
|
|
432
|
+
const wiring = await detectVendoWiring(root);
|
|
433
|
+
if (!wiring.server || !wiring.client) {
|
|
434
|
+
const typescript = await exists(join(root, "tsconfig.json"));
|
|
435
|
+
const server = join(root, "vendo", typescript ? "server.ts" : "server.mjs");
|
|
436
|
+
const registryFile = join(root, "vendo", typescript ? "registry.tsx" : "registry.mjs");
|
|
437
|
+
const registryBefore = await readOptional(registryFile);
|
|
438
|
+
const serverBefore = await readOptional(server);
|
|
439
|
+
// Init owns the composition only when it CREATES it: no generated
|
|
440
|
+
// server module yet AND no hand-wired createVendo anywhere else. A host
|
|
441
|
+
// that composed at its own path but hasn't pasted <VendoRoot> yet gets
|
|
442
|
+
// neither a duplicate server module nor an orphaned registry — the
|
|
443
|
+
// Express analog of the Next branch's routeBefore === null guard.
|
|
444
|
+
const scaffolding = serverBefore === null && !wiring.server;
|
|
445
|
+
// The registry regenerates only for a composition that uses it: the one
|
|
446
|
+
// being created now, or a previously generated server module whose
|
|
447
|
+
// ./registry import would otherwise dangle. Never clobbered.
|
|
448
|
+
const registryPlanned = registryBefore === null
|
|
449
|
+
&& (scaffolding || serverBefore?.includes("./registry") === true);
|
|
450
|
+
if (registryPlanned) {
|
|
451
|
+
const path = relative(root, registryFile);
|
|
452
|
+
const registryAfter = registrySource(typescript ? "tsx" : "mjs");
|
|
453
|
+
changes.push({ absolute: registryFile, path, before: null, after: registryAfter, diff: diff(path, null, registryAfter) });
|
|
454
|
+
registryPath = path;
|
|
455
|
+
}
|
|
456
|
+
if (scaffolding) {
|
|
457
|
+
const path = relative(root, server);
|
|
458
|
+
// Detect + confirm happens only here — fresh composition creation —
|
|
459
|
+
// so a re-run before the manual <VendoRoot> paste neither asks nor
|
|
460
|
+
// re-fires the advisory after "Already wired".
|
|
461
|
+
const auth = await resolveScaffoldAuth(root, path, options.auth, confirmAuth, selectAuth);
|
|
462
|
+
const serverAfter = expressServerSource(typescript, auth.wired);
|
|
463
|
+
changes.push({ absolute: server, path, before: null, after: serverAfter, diff: diff(path, null, serverAfter) });
|
|
464
|
+
authAdvice = auth.advice;
|
|
465
|
+
authWired = auth.wired;
|
|
466
|
+
compositionPath = path;
|
|
467
|
+
}
|
|
468
|
+
withRegistry = registryBefore !== null || registryPlanned;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
const app = await appDirectory(root);
|
|
473
|
+
const route = join(app, "api", "vendo", "[...vendo]", "route.ts");
|
|
474
|
+
const actionsModule = join(app, "api", "vendo", "[...vendo]", "vendo-actions.ts");
|
|
475
|
+
const routeBefore = await readOptional(route);
|
|
476
|
+
const actionsBefore = await readOptional(actionsModule);
|
|
477
|
+
const registrations = await wiringServerActions(root);
|
|
478
|
+
// The shared registry mirrors the app dir (src/app → src/vendo): generated
|
|
479
|
+
// only while absent and only when the route uses it — a fresh scaffold, or
|
|
480
|
+
// a route that already imports vendo/registry. A hand-wired route that
|
|
481
|
+
// ignores the registry never grows an orphan file.
|
|
482
|
+
const registryFile = join(dirname(app), "vendo", "registry.tsx");
|
|
483
|
+
const registryBefore = await readOptional(registryFile);
|
|
484
|
+
const registryPlanned = registryBefore === null
|
|
485
|
+
&& (routeBefore === null || routeBefore.includes("vendo/registry"));
|
|
486
|
+
if (registryPlanned) {
|
|
487
|
+
const path = relative(root, registryFile);
|
|
488
|
+
const registryAfter = registrySource("tsx");
|
|
489
|
+
changes.push({ absolute: registryFile, path, before: null, after: registryAfter, diff: diff(path, null, registryAfter) });
|
|
490
|
+
registryPath = path;
|
|
491
|
+
}
|
|
492
|
+
withRegistry = registryBefore !== null || registryPlanned;
|
|
493
|
+
// The registration map regenerates whenever the detected "use server"
|
|
494
|
+
// surface changes; an existing map is kept compiling (emptied, never
|
|
495
|
+
// deleted) when the last action disappears.
|
|
496
|
+
if (registrations.length > 0 || actionsBefore !== null) {
|
|
497
|
+
const actionsAfter = serverActionsModuleSource(root, dirname(actionsModule), registrations);
|
|
498
|
+
if (actionsAfter !== actionsBefore) {
|
|
499
|
+
const path = relative(root, actionsModule);
|
|
500
|
+
changes.push({ absolute: actionsModule, path, before: actionsBefore, after: actionsAfter, diff: diff(path, actionsBefore, actionsAfter) });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (routeBefore === null) {
|
|
504
|
+
const path = relative(root, route);
|
|
505
|
+
// Detect + confirm happens only on fresh composition creation.
|
|
506
|
+
const auth = await resolveScaffoldAuth(root, path, options.auth, confirmAuth, selectAuth);
|
|
507
|
+
const registrySpecifier = relative(dirname(route), join(dirname(app), "vendo", "registry")).split(sep).join("/");
|
|
508
|
+
const routeAfter = routeSource({ serverActions: registrations.length > 0, auth: auth.wired, registrySpecifier });
|
|
509
|
+
changes.push({ absolute: route, path, before: routeBefore, after: routeAfter, diff: diff(path, routeBefore, routeAfter) });
|
|
510
|
+
authAdvice = auth.advice;
|
|
511
|
+
authWired = auth.wired;
|
|
512
|
+
compositionPath = path;
|
|
513
|
+
}
|
|
514
|
+
else if (registrations.length > 0) {
|
|
515
|
+
// The route already exists but server actions appeared since it was
|
|
516
|
+
// generated: wire the registration map into the existing createVendo so
|
|
517
|
+
// server-action execution doesn't fail closed (ENG-248 idempotency fix).
|
|
518
|
+
const wiredRoute = wireRouteServerActions(routeBefore);
|
|
519
|
+
if (wiredRoute !== null) {
|
|
520
|
+
const path = relative(root, route);
|
|
521
|
+
changes.push({ absolute: route, path, before: routeBefore, after: wiredRoute, diff: diff(path, routeBefore, wiredRoute) });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const packageJson = join(root, "package.json");
|
|
526
|
+
const packageBefore = await readOptional(packageJson);
|
|
527
|
+
if (packageBefore !== null) {
|
|
528
|
+
const packageAfter = packageWithSyncHooks(packageBefore);
|
|
529
|
+
if (packageAfter !== null) {
|
|
530
|
+
const path = relative(root, packageJson);
|
|
531
|
+
changes.push({
|
|
532
|
+
absolute: packageJson,
|
|
533
|
+
path,
|
|
534
|
+
before: packageBefore,
|
|
535
|
+
after: packageAfter,
|
|
536
|
+
diff: diff(path, packageBefore, packageAfter),
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// Agent surface: a host that already uses skills (.claude/ exists) gets the
|
|
541
|
+
// packaged vendo-setup skill. Written only while missing — an edited copy is
|
|
542
|
+
// respected (never overwritten); a deleted copy returns on the next init,
|
|
543
|
+
// like any missing scaffold.
|
|
544
|
+
if (await exists(join(root, ".claude"))) {
|
|
545
|
+
const skillAbsolute = join(root, ".claude", "skills", "vendo-setup", "SKILL.md");
|
|
546
|
+
if (!(await exists(skillAbsolute))) {
|
|
547
|
+
const skillSource = await setupSkillSource();
|
|
548
|
+
if (skillSource !== null) {
|
|
549
|
+
const path = relative(root, skillAbsolute);
|
|
550
|
+
changes.push({ absolute: skillAbsolute, path, before: null, after: skillSource, diff: diff(path, null, skillSource) });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const writes = [
|
|
555
|
+
".env.example",
|
|
556
|
+
".vendo/tools.json",
|
|
557
|
+
".vendo/overrides.json",
|
|
558
|
+
".vendo/policy.json",
|
|
559
|
+
".vendo/brief.md",
|
|
560
|
+
".vendo/theme.json",
|
|
561
|
+
".vendo/data/.gitignore",
|
|
562
|
+
];
|
|
563
|
+
const manualSteps = await vendoRootPasteLines(root, framework, withRegistry);
|
|
564
|
+
return {
|
|
565
|
+
changes,
|
|
566
|
+
manualSteps,
|
|
567
|
+
authAdvice,
|
|
568
|
+
authWired,
|
|
569
|
+
compositionPath,
|
|
570
|
+
registryPath,
|
|
571
|
+
plan: {
|
|
572
|
+
framework,
|
|
573
|
+
root,
|
|
574
|
+
writes,
|
|
575
|
+
codeChanges: changes.map(({ path, diff: rendered }) => ({ path, diff: rendered })),
|
|
576
|
+
manualSteps,
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
async function writeIfMissing(path, content, force) {
|
|
581
|
+
if (!force && await exists(path))
|
|
582
|
+
return;
|
|
583
|
+
await writeText(path, content);
|
|
584
|
+
}
|
|
585
|
+
/** The value of one NAME=value line in .env.local (the cloud step's upsert
|
|
586
|
+
target) — the same-run pickup reads the freshly minted key back from disk.
|
|
587
|
+
One parser for the whole CLI: shared.ts's envLocalValueSync (telemetry's
|
|
588
|
+
cloud-key read uses the same one, so the two can never disagree). */
|
|
589
|
+
async function envLocalValue(root, name) {
|
|
590
|
+
return envLocalValueSync(root, name);
|
|
591
|
+
}
|
|
592
|
+
async function ensureVendoEnvExample(root) {
|
|
593
|
+
const path = join(root, ".env.example");
|
|
594
|
+
const current = await readOptional(path);
|
|
595
|
+
if (current === null) {
|
|
596
|
+
await writeText(path, VENDO_ENV_EXAMPLE);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (/^\s*VENDO_BASE_URL\s*=/m.test(current))
|
|
600
|
+
return;
|
|
601
|
+
const separator = current.length === 0 ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
602
|
+
await writeText(path, `${current}${separator}${VENDO_ENV_EXAMPLE}`);
|
|
603
|
+
}
|
|
604
|
+
/** root rides in as the client's cwd: projectIdHash/packageManager and the
|
|
605
|
+
.env.local cloud-key read attribute to the TARGET project, not the shell
|
|
606
|
+
cwd (`vendo init ../app` from elsewhere). Seams in options.telemetry win. */
|
|
607
|
+
function telemetryFor(options, output, root) {
|
|
608
|
+
return toolingTelemetry({ cwd: root, ...options.telemetry, log: (message) => output.log(message) });
|
|
609
|
+
}
|
|
610
|
+
/** 09-vendo §5 — idempotent, zero-question setup. */
|
|
611
|
+
export async function runInit(options) {
|
|
612
|
+
// The clack-style renderer rides the SAME Output seam: it restyles the
|
|
613
|
+
// exact plain messages below, and is selected only for a human terminal
|
|
614
|
+
// (TTY, no NO_COLOR/CI, never --agent, never an injected output). Every
|
|
615
|
+
// other run — tests, pipes, CI — keeps the plain strings byte-for-byte.
|
|
616
|
+
const pretty = options.output === undefined && options.agent !== true && usePrettyOutput()
|
|
617
|
+
? createPrettyOutput()
|
|
618
|
+
: null;
|
|
619
|
+
const output = options.output ?? pretty ?? consoleOutput;
|
|
620
|
+
const started = Date.now();
|
|
621
|
+
const root = resolve(options.targetDir);
|
|
622
|
+
const env = options.env ?? process.env;
|
|
623
|
+
if (options.agent === true) {
|
|
624
|
+
// Extraction runs before the plan is emitted so the plan carries real tool
|
|
625
|
+
// names and risk advice; the throwaway out dir keeps --agent read-only.
|
|
626
|
+
const { plan } = await buildPlan(options);
|
|
627
|
+
const extraction = await extractForPlan(root);
|
|
628
|
+
let appName = "app";
|
|
629
|
+
try {
|
|
630
|
+
appName = JSON.parse((await readOptional(join(root, "package.json"))) ?? "{}").name ?? "app";
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
// package.json is optional context
|
|
634
|
+
}
|
|
635
|
+
output.log(JSON.stringify({
|
|
636
|
+
...plan,
|
|
637
|
+
extraction,
|
|
638
|
+
riskRecommendations: riskRecommendations(extraction.tools),
|
|
639
|
+
// The delegation contract: the agent reading this plan can do the AI
|
|
640
|
+
// polish itself and land it through `vendo extract --apply` — the same
|
|
641
|
+
// deterministic guards as init's built-in pass.
|
|
642
|
+
aiPolish: {
|
|
643
|
+
instructions: composeDelegatedInstructions(planStaticTools(extraction.tools), appName),
|
|
644
|
+
draftSchema: EXTRACTION_DRAFT_JSON_SCHEMA,
|
|
645
|
+
apply: APPLY_COMMAND,
|
|
646
|
+
},
|
|
647
|
+
}, null, 2));
|
|
648
|
+
return 0;
|
|
649
|
+
}
|
|
650
|
+
// Detect + confirm (interactive runs only): --yes and non-interactive runs
|
|
651
|
+
// accept the detected default silently — the same interactivity posture as
|
|
652
|
+
// the AI-polish consent.
|
|
653
|
+
const interactive = options.interactive ?? (Boolean(stdin.isTTY) && Boolean(stdout.isTTY));
|
|
654
|
+
// An undetectable framework has NO safe default: a non-interactive run
|
|
655
|
+
// (agents) errors with the exact flag instead of guessing the Next layout
|
|
656
|
+
// into an unknown host. Interactive runs keep today's fall-through.
|
|
657
|
+
if (options.framework === undefined && (options.yes === true || !interactive)
|
|
658
|
+
&& await detectFramework(root) === "unknown") {
|
|
659
|
+
output.error("Framework not detected (no next or express dependency in package.json) and this run cannot ask. " +
|
|
660
|
+
"Pass --framework. Example: vendo init --yes --framework next");
|
|
661
|
+
return 1;
|
|
662
|
+
}
|
|
663
|
+
// (No stdin-TTY guard on these defaults, unlike the star ask's: an unshown
|
|
664
|
+
// auth confirm resolving its default just wires the detected preset — the
|
|
665
|
+
// very accept the non-interactive path performs silently anyway.)
|
|
666
|
+
const confirmAuth = options.yes === true || !interactive
|
|
667
|
+
? undefined
|
|
668
|
+
: (options.confirmAuth ?? (pretty === null ? askYesNo : pretty.confirm));
|
|
669
|
+
const selectAuth = options.yes === true || !interactive
|
|
670
|
+
? undefined
|
|
671
|
+
: (options.selectAuth ?? (pretty === null ? plainSelect : pretty.select));
|
|
672
|
+
const detectStarted = Date.now();
|
|
673
|
+
const { plan, changes, manualSteps, authAdvice, authWired, compositionPath, registryPath } = await buildPlan(options, confirmAuth, selectAuth);
|
|
674
|
+
const detectMs = Date.now() - detectStarted;
|
|
675
|
+
let telemetry = telemetryFor(options, output, root);
|
|
676
|
+
await telemetry.track("init_started", { framework: plan.framework });
|
|
677
|
+
try {
|
|
678
|
+
// --cloud-key: the flag answer to the cloud-login offer — the supplied
|
|
679
|
+
// key lands exactly where the mint would (.env.local), so the merge
|
|
680
|
+
// below picks it up and the offer never fires.
|
|
681
|
+
if (options.cloudKey !== undefined) {
|
|
682
|
+
await upsertEnvLocal(root, "VENDO_API_KEY", options.cloudKey);
|
|
683
|
+
output.log("Wrote VENDO_API_KEY to .env.local (--cloud-key).");
|
|
684
|
+
}
|
|
685
|
+
// Key first (product order fix): the model-credential story — env keys,
|
|
686
|
+
// else the Vendo Cloud offer — runs BEFORE the AI-assisted passes, so a
|
|
687
|
+
// starter key minted here powers the SAME run's theme model pass and AI
|
|
688
|
+
// polish instead of those passes reporting "no model" while the offer
|
|
689
|
+
// waits below them. --yes / non-interactive semantics are unchanged.
|
|
690
|
+
// Dev keys may live in .env.local rather than this process's env — a
|
|
691
|
+
// PRIOR run's minted starter key, or hand-added provider keys. Merge
|
|
692
|
+
// them into the env every credential consumer reads (credential ladder,
|
|
693
|
+
// cloud step, theme model pass, AI polish); an explicit env value
|
|
694
|
+
// always wins over .env.local.
|
|
695
|
+
let effectiveEnv = env;
|
|
696
|
+
for (const name of [...ENV_KEY_VARS.map((entry) => entry.envVar), "VENDO_API_KEY"]) {
|
|
697
|
+
if ((env[name] ?? "").trim() !== "")
|
|
698
|
+
continue;
|
|
699
|
+
const stored = await envLocalValue(root, name);
|
|
700
|
+
if (stored !== null)
|
|
701
|
+
effectiveEnv = { ...effectiveEnv, [name]: stored };
|
|
702
|
+
}
|
|
703
|
+
let credential = await (options.resolveCredential ?? resolveDevCredential)({ env: effectiveEnv });
|
|
704
|
+
if (credential.rung === "env-key") {
|
|
705
|
+
output.log(`Model: ${describeDevCredential(credential)} — production uses this same key server-side.`);
|
|
706
|
+
}
|
|
707
|
+
const cloud = await runCloudStep({
|
|
708
|
+
root,
|
|
709
|
+
output,
|
|
710
|
+
// --byo answers the offer with "no" AND suppresses the agent-path
|
|
711
|
+
// auth.md pointer (an explicit BYO choice is final); --yes skips the
|
|
712
|
+
// prompt but still gets the pointer so an agent can mint in-band.
|
|
713
|
+
yes: options.yes === true,
|
|
714
|
+
byo: options.byo === true,
|
|
715
|
+
credential,
|
|
716
|
+
// The RUN's env, not process.env: a programmatic caller's key must be
|
|
717
|
+
// what the probe and the mint see (seams in options.cloud still win).
|
|
718
|
+
env: effectiveEnv,
|
|
719
|
+
// The step's own command_run row rides init's telemetry seams.
|
|
720
|
+
...(options.telemetry === undefined ? {} : { telemetry: options.telemetry }),
|
|
721
|
+
...(pretty === null ? {} : { confirm: pretty.confirm }),
|
|
722
|
+
...(options.cloud ?? {}),
|
|
723
|
+
});
|
|
724
|
+
// Same-run pickup: a starter key minted just now lands in .env.local —
|
|
725
|
+
// merge it the same way so THIS run's passes already benefit.
|
|
726
|
+
if (cloud.wroteEnvLocal) {
|
|
727
|
+
const minted = await envLocalValue(root, "VENDO_API_KEY");
|
|
728
|
+
if (minted !== null) {
|
|
729
|
+
effectiveEnv = { ...effectiveEnv, VENDO_API_KEY: minted };
|
|
730
|
+
credential = await (options.resolveCredential ?? resolveDevCredential)({ env: effectiveEnv });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// A key that landed in .env.local THIS run (--cloud-key upsert or the
|
|
734
|
+
// login ceremony) must activate the telemetry cloud lane for the rest of
|
|
735
|
+
// this run's events too — rebuild the client so it re-reads .env.local.
|
|
736
|
+
// A pre-existing key was already picked up at the first construction.
|
|
737
|
+
if (options.cloudKey !== undefined || cloud.wroteEnvLocal) {
|
|
738
|
+
telemetry = telemetryFor(options, output, root);
|
|
739
|
+
}
|
|
740
|
+
// Wire — apply the bounded change set and list it. No gates, no prompts.
|
|
741
|
+
// (Timed for the cloud lane's wiringMs; the static scan below adds on.)
|
|
742
|
+
const wiringStarted = Date.now();
|
|
743
|
+
for (const change of changes) {
|
|
744
|
+
await writeText(change.absolute, change.after);
|
|
745
|
+
}
|
|
746
|
+
// Scan — .vendo artifacts + static extraction (the hints layer for the AI
|
|
747
|
+
// extraction; interim tools.json source until it lands).
|
|
748
|
+
await ensureVendoEnvExample(root);
|
|
749
|
+
await mkdir(join(root, ".vendo"), { recursive: true });
|
|
750
|
+
await writeIfMissing(join(root, ".vendo", "overrides.json"), `${JSON.stringify({
|
|
751
|
+
format: "vendo/overrides@1",
|
|
752
|
+
tools: {},
|
|
753
|
+
remix: { ignoreSlots: [] },
|
|
754
|
+
}, null, 2)}\n`, options.force === true);
|
|
755
|
+
await writeIfMissing(join(root, ".vendo", "policy.json"), `${JSON.stringify({
|
|
756
|
+
format: "vendo/policy@1",
|
|
757
|
+
directions: [],
|
|
758
|
+
rules: [
|
|
759
|
+
{ match: { risk: "destructive" }, action: "ask", note: "Review irreversible actions" },
|
|
760
|
+
{ match: { risk: "read" }, action: "run" },
|
|
761
|
+
],
|
|
762
|
+
}, null, 2)}\n`, options.force === true);
|
|
763
|
+
await writeIfMissing(join(root, ".vendo", "brief.md"), BRIEF_PLACEHOLDER, options.force === true);
|
|
764
|
+
// Theme (Task 2/4 re-derive): the exact-only allowlist pass runs and
|
|
765
|
+
// writes theme.json right away — never overwriting an existing one (it
|
|
766
|
+
// is the editable source of truth) unless --force. Whatever brand slots
|
|
767
|
+
// the allowlist left unfilled ride the consent-gated AI-polish pass
|
|
768
|
+
// below; the merge, --theme answers, the one-glance palette print, and
|
|
769
|
+
// the uncertain-slot review all happen AFTER that pass returns, further
|
|
770
|
+
// down this function — a pre-existing theme.json is never touched.
|
|
771
|
+
const themePath = join(root, ".vendo", "theme.json");
|
|
772
|
+
const themeCreatedThisRun = options.force === true || !(await exists(themePath));
|
|
773
|
+
let wiringMs = Date.now() - wiringStarted;
|
|
774
|
+
let themeMs;
|
|
775
|
+
let themeSummary = null;
|
|
776
|
+
if (themeCreatedThisRun) {
|
|
777
|
+
pretty?.spin("Capturing your theme");
|
|
778
|
+
const themeStarted = Date.now();
|
|
779
|
+
themeSummary = await extractThemeSlots(root);
|
|
780
|
+
themeMs = Date.now() - themeStarted;
|
|
781
|
+
pretty?.stopSpin();
|
|
782
|
+
await writeText(themePath, `${JSON.stringify(toVendoTheme(themeSummary.slots), null, 2)}\n`);
|
|
783
|
+
}
|
|
784
|
+
await writeIfMissing(join(root, ".vendo", "data", ".gitignore"), "*\n!.gitignore\n", options.force === true);
|
|
785
|
+
pretty?.spin("Learning your API surface");
|
|
786
|
+
const scanStarted = Date.now();
|
|
787
|
+
const report = await vendoSync({ root, out: join(root, ".vendo") });
|
|
788
|
+
wiringMs += Date.now() - scanStarted;
|
|
789
|
+
pretty?.stopSpin();
|
|
790
|
+
for (const warning of report.warnings)
|
|
791
|
+
output.error(`warning: ${warning}`);
|
|
792
|
+
let toolCount = 0;
|
|
793
|
+
let routeCount = 0;
|
|
794
|
+
try {
|
|
795
|
+
const tools = JSON.parse(await readFile(join(root, ".vendo", "tools.json"), "utf8"));
|
|
796
|
+
toolCount = tools.tools?.length ?? 0;
|
|
797
|
+
routeCount = tools.tools?.filter((tool) => tool.binding?.kind === "route").length ?? 0;
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
// Sync already reported any extraction warning; telemetry gets a count only.
|
|
801
|
+
}
|
|
802
|
+
// Summary — what changed, what was learned.
|
|
803
|
+
if (changes.length > 0) {
|
|
804
|
+
output.log(`\nWired (${changes.length} file${changes.length === 1 ? "" : "s"}):`);
|
|
805
|
+
for (const change of changes) {
|
|
806
|
+
output.log(` ${change.before === null ? "+" : "~"} ${change.path}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
output.log("\nAlready wired — nothing to change.");
|
|
811
|
+
}
|
|
812
|
+
// Detection-as-advice (zero-question contract): a wired preset stays
|
|
813
|
+
// silent — the comment in the scaffold cites the escape hatch; none or
|
|
814
|
+
// ambiguous gets exactly one calm line naming the line to add.
|
|
815
|
+
if (authAdvice !== null)
|
|
816
|
+
output.log(authAdvice);
|
|
817
|
+
output.log(`Learned: ${toolCount} tools · theme captured → .vendo/ (tools.json, theme.json, brief.md)`);
|
|
818
|
+
// AI extraction (install-dx, staged): a coding agent surveys the repo,
|
|
819
|
+
// drafts each surface in a focused pass, cross-checks the combined draft,
|
|
820
|
+
// and drafts the brief — all into the override channel; deterministic
|
|
821
|
+
// guards decide what applies. Consent-gated; skipped silently when
|
|
822
|
+
// non-interactive or credential-less. A successful pass re-syncs so
|
|
823
|
+
// tools.json reflects the polish immediately.
|
|
824
|
+
const engineStarted = Date.now();
|
|
825
|
+
const polish = await runAiExtraction({
|
|
826
|
+
root,
|
|
827
|
+
output,
|
|
828
|
+
env: effectiveEnv,
|
|
829
|
+
yes: options.yes === true,
|
|
830
|
+
// --ai-polish IS the consent: no prompt, and non-interactive runs
|
|
831
|
+
// stop skipping.
|
|
832
|
+
...(options.aiPolish === true ? { consent: true } : {}),
|
|
833
|
+
...(options.force === true ? { force: true } : {}),
|
|
834
|
+
...(options.engine === undefined ? {} : { engine: options.engine }),
|
|
835
|
+
...(pretty === null ? {} : { confirm: pretty.confirm, choose: pretty.select }),
|
|
836
|
+
...(themeCreatedThisRun && themeSummary !== null ? {
|
|
837
|
+
theme: {
|
|
838
|
+
needed: themeSummary.needed,
|
|
839
|
+
alreadyExact: Object.fromEntries(Object.entries(themeSummary.matched)
|
|
840
|
+
.filter(([, provenance]) => provenance.startsWith("--"))
|
|
841
|
+
.map(([slot]) => [slot, String(themeSummary.slots[slot])])),
|
|
842
|
+
evidencePaths: themeSummary.evidencePaths,
|
|
843
|
+
},
|
|
844
|
+
} : {}),
|
|
845
|
+
...(options.extract ?? {}),
|
|
846
|
+
});
|
|
847
|
+
const engineMs = Date.now() - engineStarted;
|
|
848
|
+
// Theme finalization (Task 4): merge whatever the AI pass filled — if
|
|
849
|
+
// consent was declined or unavailable, `polish.theme` is simply absent
|
|
850
|
+
// and the exact-only summary stands — then --theme answers (a human
|
|
851
|
+
// "(you)" wins over a model value), the one-glance palette print, and
|
|
852
|
+
// finally the uncertain-slot review. Skipped entirely when theme.json
|
|
853
|
+
// pre-existed this run (nothing above ran either).
|
|
854
|
+
if (themeCreatedThisRun && themeSummary !== null) {
|
|
855
|
+
const summary = polish.theme === undefined ? themeSummary : applyThemeDraft(themeSummary, polish.theme);
|
|
856
|
+
// --theme answers land first; the review prompt then covers only the
|
|
857
|
+
// uncertain slots the flags left unanswered (non-interactive runs keep
|
|
858
|
+
// the extracted/merged values for those, exactly as before).
|
|
859
|
+
const answers = { ...(options.themeAnswers ?? {}) };
|
|
860
|
+
const unanswered = summary.uncertain.filter((entry) => !Object.hasOwn(answers, entry.slot));
|
|
861
|
+
if (unanswered.length > 0 && options.yes !== true) {
|
|
862
|
+
const reviewed = await (options.themeReview ?? defaultThemeReview)(unanswered.length === summary.uncertain.length ? summary : { ...summary, uncertain: unanswered });
|
|
863
|
+
for (const [slot, raw] of Object.entries(reviewed)) {
|
|
864
|
+
if (!Object.hasOwn(answers, slot))
|
|
865
|
+
answers[slot] = raw;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (Object.keys(answers).length > 0) {
|
|
869
|
+
for (const [slot, raw] of Object.entries(answers)) {
|
|
870
|
+
if (!Object.hasOwn(summary.slots, slot)) {
|
|
871
|
+
output.error(`ignored unknown theme slot ${JSON.stringify(slot)}`);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const value = validateSlotValue(slot, raw);
|
|
875
|
+
if (value === null) {
|
|
876
|
+
output.error(`ignored invalid theme ${slot} value ${JSON.stringify(raw)}`);
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
summary.slots[slot] = value;
|
|
880
|
+
summary.matched[slot] = "(you)";
|
|
881
|
+
// The slot no longer defaulted — the human just set it.
|
|
882
|
+
summary.defaulted = summary.defaulted.filter((name) => name !== slot);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// A replaced accent invalidates an accentText nobody chose — one that
|
|
886
|
+
// was contrast-derived, or still the neutral default because the
|
|
887
|
+
// model omitted the accent too. Re-derive against the new accent; an
|
|
888
|
+
// explicit token or a direct human/model answer stays authoritative.
|
|
889
|
+
const accentTextUnchosen = summary.matched["accentText"] === "(contrast) accent"
|
|
890
|
+
|| summary.defaulted.includes("accentText");
|
|
891
|
+
if (summary.matched["accent"] === "(you)" && accentTextUnchosen) {
|
|
892
|
+
summary.slots.accentText = contrastingText(summary.slots.accent);
|
|
893
|
+
summary.matched["accentText"] = "(contrast) accent";
|
|
894
|
+
summary.defaulted = summary.defaulted.filter((name) => name !== "accentText");
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
await writeText(themePath, `${JSON.stringify(toVendoTheme(summary.slots), null, 2)}\n`);
|
|
898
|
+
printThemeSummary(summary, output);
|
|
899
|
+
}
|
|
900
|
+
if (polish.ran) {
|
|
901
|
+
const resynced = await vendoSync({ root, out: join(root, ".vendo") });
|
|
902
|
+
for (const warning of resynced.warnings)
|
|
903
|
+
output.error(`warning: ${warning}`);
|
|
904
|
+
}
|
|
905
|
+
// Project-shape enrichment (posthog-analytics §3): bools, closed enums,
|
|
906
|
+
// counts, and bare dependency versions only — never names or content.
|
|
907
|
+
await telemetry.track("init_completed", {
|
|
908
|
+
framework: plan.framework,
|
|
909
|
+
command: "init",
|
|
910
|
+
toolCount,
|
|
911
|
+
durationMs: Date.now() - started,
|
|
912
|
+
typescript: await exists(join(root, "tsconfig.json")),
|
|
913
|
+
router: await detectRouter(root, plan.framework),
|
|
914
|
+
// The engine that actually ran the AI polish; "none" when it didn't run.
|
|
915
|
+
engine: polish.engine ?? "none",
|
|
916
|
+
// route-scan today; "zod" is reserved for a future oracle-backed detect
|
|
917
|
+
// (the zod collector currently enriches route-scan output invisibly).
|
|
918
|
+
apiDetectMethod: routeCount > 0 ? "route-scan" : "none",
|
|
919
|
+
routeCount,
|
|
920
|
+
themeExtracted: themeSummary !== null,
|
|
921
|
+
...(await detectDepVersions(root, plan.framework)),
|
|
922
|
+
// Cloud-lane-only props, passed unconditionally — the client strips
|
|
923
|
+
// every one of them in the anonymous lane.
|
|
924
|
+
detectMs,
|
|
925
|
+
engineMs,
|
|
926
|
+
...(themeMs === undefined ? {} : { themeMs }),
|
|
927
|
+
wiringMs,
|
|
928
|
+
...(await cloudProjectProps(root)),
|
|
929
|
+
});
|
|
930
|
+
// The one short Cloud reminder in the end-of-run summary — ONLY while no
|
|
931
|
+
// key exists (the full emphasized block already ran up top; no repeat).
|
|
932
|
+
if (credential.rung === "none") {
|
|
933
|
+
output.log("No model key yet: set ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY in .env.local, or run `vendo login` for a free dev key.");
|
|
934
|
+
}
|
|
935
|
+
// #478 short-term — npm installs the ai@7 peer conflict without failing
|
|
936
|
+
// and every internal turn then throws AI_InvalidPromptError; warn in the
|
|
937
|
+
// end-of-run summary instead of waiting for doctor to fail (E-DEP-001).
|
|
938
|
+
const aiVersion = await installedAiVersion(root);
|
|
939
|
+
if (aiVersion !== null && Number.parseInt(aiVersion, 10) >= 7) {
|
|
940
|
+
output.error(`warning: 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`);
|
|
941
|
+
}
|
|
942
|
+
// Done — the one paste that is the user's, then their own dev server.
|
|
943
|
+
output.log("\nLast steps are yours:");
|
|
944
|
+
for (const line of manualSteps)
|
|
945
|
+
output.log(` ${line}`);
|
|
946
|
+
output.log("\nThen start your dev server — the agent is live in your app.");
|
|
947
|
+
output.log("Verify everything: `npx vendo doctor` (it can start the server and run a live turn).");
|
|
948
|
+
// Agent tail (agent-install-dx): the --yes-or-non-TTY path is agent-driven
|
|
949
|
+
// — the run's FINAL block is the repo-specific pointers an agent parses.
|
|
950
|
+
// Interactive human runs keep the clack-style output untouched; --agent
|
|
951
|
+
// never reaches here (its read-only JSON plan returned above).
|
|
952
|
+
if (options.yes === true || !interactive) {
|
|
953
|
+
output.log("\nAgent tail:");
|
|
954
|
+
const tail = await agentTailLines({ root, framework: plan.framework, registryPath, compositionPath, authWired, cloudKeyMissing: credential.rung === "none" });
|
|
955
|
+
for (const line of tail)
|
|
956
|
+
output.log(` ${line}`);
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
// Star ask (agent-install-dx §CLI-5): the interactive success screen
|
|
960
|
+
// ends with ONE consent question — never shown non-interactively (the
|
|
961
|
+
// playbook owns the agent-path ask; deterministic runs stay that way),
|
|
962
|
+
// and never fatal: nothing in this step can change init's exit code.
|
|
963
|
+
// Yes stars via gh; any failure degrades to the repo URL, one line.
|
|
964
|
+
// No does nothing — no guilt text.
|
|
965
|
+
try {
|
|
966
|
+
// Consent guard: an unshown prompt is NEVER a yes. On a non-TTY
|
|
967
|
+
// stdin (programmatic `interactive: true`, `init < file`) both real
|
|
968
|
+
// confirms would resolve the default — pretty.confirm returns it,
|
|
969
|
+
// askYesNo would block — and starring is an account action, so the
|
|
970
|
+
// answer without a real keyboard is false, regardless of path.
|
|
971
|
+
const confirmStar = options.confirmStar
|
|
972
|
+
?? (async (question, defaultYes) => stdin.isTTY === true
|
|
973
|
+
? (pretty === null ? askYesNo : pretty.confirm)(question, defaultYes)
|
|
974
|
+
: false);
|
|
975
|
+
if (await confirmStar(`Star ${STAR_REPO} to support the project?`, true)) {
|
|
976
|
+
const starred = await starViaGh(options.spawnStar ?? ((command, args) => spawn(command, args, { stdio: "ignore" })));
|
|
977
|
+
if (!starred)
|
|
978
|
+
output.log(`Star it anytime: ${STAR_LINK}`);
|
|
979
|
+
// Star attribution: `starred` is an exact star-from-the-CLI signal
|
|
980
|
+
// (closed outcome enum; counts-and-enums promise holds).
|
|
981
|
+
await telemetry.track("star_prompt", { outcome: starred ? "starred" : "star-failed" });
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
await telemetry.track("star_prompt", { outcome: "declined" });
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
catch {
|
|
988
|
+
// The ask is best-effort by design; init already succeeded.
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
pretty?.done(Date.now() - started, true);
|
|
992
|
+
return 0;
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
await telemetry.track("init_failed", {
|
|
996
|
+
framework: plan.framework,
|
|
997
|
+
failedStep: "wiring",
|
|
998
|
+
errorClass: errorClass(error),
|
|
999
|
+
// Cloud lane only (stripped anonymously); scrubbed at the call site and
|
|
1000
|
+
// re-scrubbed by the client as defense-in-depth.
|
|
1001
|
+
errorDetail: scrubErrorDetail(error instanceof Error ? error.message : String(error)),
|
|
1002
|
+
});
|
|
1003
|
+
await telemetry.track("error_class", { errorClass: errorClass(error) });
|
|
1004
|
+
output.error(error instanceof Error ? error.message : "vendo init failed");
|
|
1005
|
+
pretty?.done(Date.now() - started, false);
|
|
1006
|
+
return 1;
|
|
1007
|
+
}
|
|
1008
|
+
}
|