@vendoai/vendo 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +28 -0
- package/bin/vendo.mjs +4 -0
- package/dist/ai-sdk.d.ts +35 -0
- package/dist/ai-sdk.js +62 -0
- package/dist/auth-presets/auth-js.d.ts +14 -0
- package/dist/auth-presets/auth-js.js +91 -0
- package/dist/auth-presets/auth0.d.ts +23 -0
- package/dist/auth-presets/auth0.js +98 -0
- package/dist/auth-presets/clerk.d.ts +23 -0
- package/dist/auth-presets/clerk.js +65 -0
- package/dist/auth-presets/conformance.d.ts +42 -0
- package/dist/auth-presets/conformance.js +139 -0
- package/dist/auth-presets/identity.d.ts +56 -0
- package/dist/auth-presets/identity.js +152 -0
- package/dist/auth-presets/index.d.ts +14 -0
- package/dist/auth-presets/index.js +12 -0
- package/dist/auth-presets/jwt.d.ts +16 -0
- package/dist/auth-presets/jwt.js +54 -0
- package/dist/auth-presets/shared.d.ts +35 -0
- package/dist/auth-presets/shared.js +1 -0
- package/dist/auth-presets/supabase.d.ts +42 -0
- package/dist/auth-presets/supabase.js +229 -0
- package/dist/byo-approvals.d.ts +37 -0
- package/dist/byo-approvals.js +189 -0
- package/dist/capability-misses.d.ts +38 -0
- package/dist/capability-misses.js +172 -0
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.js +152 -0
- package/dist/cli/cloud/args.d.ts +3 -0
- package/dist/cli/cloud/args.js +32 -0
- package/dist/cli/cloud/auth.d.ts +17 -0
- package/dist/cli/cloud/auth.js +123 -0
- package/dist/cli/cloud/client.d.ts +28 -0
- package/dist/cli/cloud/client.js +133 -0
- package/dist/cli/cloud/command.d.ts +22 -0
- package/dist/cli/cloud/command.js +73 -0
- package/dist/cli/cloud/deploy.d.ts +26 -0
- package/dist/cli/cloud/deploy.js +187 -0
- package/dist/cli/cloud/device-login.d.ts +32 -0
- package/dist/cli/cloud/device-login.js +259 -0
- package/dist/cli/cloud/index.d.ts +5 -0
- package/dist/cli/cloud/index.js +59 -0
- package/dist/cli/cloud/keys.d.ts +2 -0
- package/dist/cli/cloud/keys.js +31 -0
- package/dist/cli/cloud/members.d.ts +3 -0
- package/dist/cli/cloud/members.js +25 -0
- package/dist/cli/cloud/output.d.ts +4 -0
- package/dist/cli/cloud/output.js +12 -0
- package/dist/cli/cloud/pending-claim.d.ts +29 -0
- package/dist/cli/cloud/pending-claim.js +38 -0
- package/dist/cli/cloud/read.d.ts +3 -0
- package/dist/cli/cloud/read.js +12 -0
- package/dist/cli/cloud/services.d.ts +2 -0
- package/dist/cli/cloud/services.js +9 -0
- package/dist/cli/cloud/session.d.ts +13 -0
- package/dist/cli/cloud/session.js +41 -0
- package/dist/cli/cloud-init.d.ts +60 -0
- package/dist/cli/cloud-init.js +149 -0
- package/dist/cli/dep-versions.d.ts +26 -0
- package/dist/cli/dep-versions.js +77 -0
- package/dist/cli/doctor-codes.d.ts +58 -0
- package/dist/cli/doctor-codes.js +60 -0
- package/dist/cli/doctor-live.d.ts +67 -0
- package/dist/cli/doctor-live.js +169 -0
- package/dist/cli/doctor.d.ts +52 -0
- package/dist/cli/doctor.js +467 -0
- package/dist/cli/eject.d.ts +29 -0
- package/dist/cli/eject.js +141 -0
- package/dist/cli/extract/apply.d.ts +26 -0
- package/dist/cli/extract/apply.js +123 -0
- package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
- package/dist/cli/extract/claude-cli-harness.js +132 -0
- package/dist/cli/extract/claude-harness.d.ts +14 -0
- package/dist/cli/extract/claude-harness.js +127 -0
- package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
- package/dist/cli/extract/codex-cli-harness.js +154 -0
- package/dist/cli/extract/delegate.d.ts +21 -0
- package/dist/cli/extract/delegate.js +77 -0
- package/dist/cli/extract/extraction.d.ts +65 -0
- package/dist/cli/extract/extraction.js +300 -0
- package/dist/cli/extract/gateway-fuel.d.ts +69 -0
- package/dist/cli/extract/gateway-fuel.js +69 -0
- package/dist/cli/extract/harness.d.ts +114 -0
- package/dist/cli/extract/harness.js +84 -0
- package/dist/cli/extract/index.d.ts +9 -0
- package/dist/cli/extract/index.js +9 -0
- package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
- package/dist/cli/extract/npx-engine-harness.js +225 -0
- package/dist/cli/extract/stages.d.ts +149 -0
- package/dist/cli/extract/stages.js +398 -0
- package/dist/cli/framework.d.ts +8 -0
- package/dist/cli/framework.js +36 -0
- package/dist/cli/init-auth.d.ts +67 -0
- package/dist/cli/init-auth.js +142 -0
- package/dist/cli/init-scaffolds.d.ts +28 -0
- package/dist/cli/init-scaffolds.js +235 -0
- package/dist/cli/init.d.ts +115 -0
- package/dist/cli/init.js +1008 -0
- package/dist/cli/mcp/index.d.ts +7 -0
- package/dist/cli/mcp/index.js +59 -0
- package/dist/cli/mcp/registry.d.ts +9 -0
- package/dist/cli/mcp/registry.js +117 -0
- package/dist/cli/mcp/server-json.d.ts +12 -0
- package/dist/cli/mcp/server-json.js +65 -0
- package/dist/cli/mcp/server.schema.json +574 -0
- package/dist/cli/mcp/verify-domain.d.ts +11 -0
- package/dist/cli/mcp/verify-domain.js +48 -0
- package/dist/cli/playground/app/embed-entry.d.ts +20 -0
- package/dist/cli/playground/app/embed-entry.js +46 -0
- package/dist/cli/playground/app/fake-client.d.ts +3 -0
- package/dist/cli/playground/app/fake-client.js +215 -0
- package/dist/cli/playground/app/fixtures.d.ts +47 -0
- package/dist/cli/playground/app/fixtures.js +472 -0
- package/dist/cli/playground/app/main.d.ts +1 -0
- package/dist/cli/playground/app/main.js +108 -0
- package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
- package/dist/cli/playground/app/scenario-mount.js +48 -0
- package/dist/cli/playground/app/scenarios.d.ts +25 -0
- package/dist/cli/playground/app/scenarios.js +152 -0
- package/dist/cli/playground/app/theme-editor.d.ts +13 -0
- package/dist/cli/playground/app/theme-editor.js +149 -0
- package/dist/cli/playground/app/theme-state.d.ts +29 -0
- package/dist/cli/playground/app/theme-state.js +151 -0
- package/dist/cli/playground/bundle.gen.d.ts +2 -0
- package/dist/cli/playground/bundle.gen.js +2 -0
- package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
- package/dist/cli/playground/embed-bundle.gen.js +2 -0
- package/dist/cli/playground.d.ts +33 -0
- package/dist/cli/playground.js +120 -0
- package/dist/cli/pretty.d.ts +48 -0
- package/dist/cli/pretty.js +330 -0
- package/dist/cli/refine.d.ts +43 -0
- package/dist/cli/refine.js +181 -0
- package/dist/cli/semantics.d.ts +31 -0
- package/dist/cli/semantics.js +122 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +184 -0
- package/dist/cli/sync.d.ts +36 -0
- package/dist/cli/sync.js +202 -0
- package/dist/cli/theme/color.d.ts +15 -0
- package/dist/cli/theme/color.js +188 -0
- package/dist/cli/theme/css-vars.d.ts +13 -0
- package/dist/cli/theme/css-vars.js +40 -0
- package/dist/cli/theme/entry-candidates.d.ts +5 -0
- package/dist/cli/theme/entry-candidates.js +22 -0
- package/dist/cli/theme/extract-theme.d.ts +184 -0
- package/dist/cli/theme/extract-theme.js +358 -0
- package/dist/cli/theme/walk.d.ts +2 -0
- package/dist/cli/theme/walk.js +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +333 -0
- package/dist/cloud-apps.d.ts +17 -0
- package/dist/cloud-apps.js +46 -0
- package/dist/cloud-console.d.ts +32 -0
- package/dist/cloud-console.js +72 -0
- package/dist/cloud-tools.d.ts +23 -0
- package/dist/cloud-tools.js +196 -0
- package/dist/connections.d.ts +71 -0
- package/dist/connections.js +192 -0
- package/dist/deployment-identity.d.ts +12 -0
- package/dist/deployment-identity.js +69 -0
- package/dist/dev-creds/model.d.ts +65 -0
- package/dist/dev-creds/model.js +159 -0
- package/dist/dev-creds/resolve.d.ts +36 -0
- package/dist/dev-creds/resolve.js +57 -0
- package/dist/hosted-store.d.ts +52 -0
- package/dist/hosted-store.js +322 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -0
- package/dist/mastra.d.ts +46 -0
- package/dist/mastra.js +88 -0
- package/dist/react.d.ts +10 -0
- package/dist/react.js +32 -0
- package/dist/refine.d.ts +244 -0
- package/dist/refine.js +599 -0
- package/dist/remixable.d.ts +18 -0
- package/dist/remixable.js +42 -0
- package/dist/runtime-capture.d.ts +18 -0
- package/dist/runtime-capture.js +111 -0
- package/dist/sandbox-wire.d.ts +101 -0
- package/dist/sandbox-wire.js +101 -0
- package/dist/sandbox.d.ts +38 -0
- package/dist/sandbox.js +389 -0
- package/dist/server.d.ts +216 -0
- package/dist/server.js +1320 -0
- package/dist/sync-impact.d.ts +14 -0
- package/dist/sync-impact.js +75 -0
- package/dist/turn-liveness.d.ts +33 -0
- package/dist/turn-liveness.js +105 -0
- package/dist/wire/approvals.d.ts +10 -0
- package/dist/wire/approvals.js +59 -0
- package/dist/wire/apps.d.ts +5 -0
- package/dist/wire/apps.js +155 -0
- package/dist/wire/automations.d.ts +5 -0
- package/dist/wire/automations.js +57 -0
- package/dist/wire/box.d.ts +3 -0
- package/dist/wire/box.js +268 -0
- package/dist/wire/connections.d.ts +5 -0
- package/dist/wire/connections.js +47 -0
- package/dist/wire/context.d.ts +22 -0
- package/dist/wire/context.js +190 -0
- package/dist/wire/doctor.d.ts +19 -0
- package/dist/wire/doctor.js +116 -0
- package/dist/wire/misc.d.ts +18 -0
- package/dist/wire/misc.js +227 -0
- package/dist/wire/shared.d.ts +158 -0
- package/dist/wire/shared.js +134 -0
- package/dist/wire/threads.d.ts +3 -0
- package/dist/wire/threads.js +68 -0
- package/package.json +151 -0
- package/skills/vendo-setup/SKILL.md +128 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { draftToolSchema, parseArtifact, } from "./harness.js";
|
|
5
|
+
import { readOptional, writeText } from "../shared.js";
|
|
6
|
+
import { BRAND_SLOTS, modelThemeSchema } from "../theme/extract-theme.js";
|
|
7
|
+
/**
|
|
8
|
+
* The staged extraction pipeline (install-dx, PostHog lesson: narrow stages
|
|
9
|
+
* beat one-shot). Four required passes plus one optional pass, all over the
|
|
10
|
+
* SAME harness seam — each is one `harness.run(instructions)` with its own
|
|
11
|
+
* narrow instructions and its own zod-validated artifact, written to
|
|
12
|
+
* `.vendo/data/extract/<stage>.json` so a failed run is diagnosable stage by
|
|
13
|
+
* stage:
|
|
14
|
+
*
|
|
15
|
+
* 1. survey — map the repo: frameworks, where the API surfaces live, and a
|
|
16
|
+
* grouping of the static tool list into surfaces (cheap/fast; respects a
|
|
17
|
+
* VENDO_EXTRACTION_SURVEY_MODEL override).
|
|
18
|
+
* 2. draft — one focused pass per surface, drafting judgment for just that
|
|
19
|
+
* surface's tools. Surfaces run sequentially (rate-limit friendly); a
|
|
20
|
+
* failed surface is SKIPPED with an honest note, never aborting the run.
|
|
21
|
+
* 3. cross-check — one pass over the combined draft for naming consistency,
|
|
22
|
+
* duplicate coverage, and risk sanity. It may only AMEND entries within
|
|
23
|
+
* the same schema (unknown names ignored, omitted entries stand); a
|
|
24
|
+
* failure degrades to the uncross-checked drafts.
|
|
25
|
+
* 4. brief — drafted from what the survey and drafts learned; a failure
|
|
26
|
+
* keeps the current brief.
|
|
27
|
+
* 5. theme — OPTIONAL, only when the caller supplies `theme` input and the
|
|
28
|
+
* allowlist left brand slots unfilled: fills those slots the same way the
|
|
29
|
+
* old theme model pass did, but over this harness seam (Read/Glob/Grep the
|
|
30
|
+
* repo instead of a fixed evidence-file set). A failure degrades to a note
|
|
31
|
+
* — the exact reads and neutral defaults still stand.
|
|
32
|
+
*
|
|
33
|
+
* The combined output feeds the EXISTING applyDraft guards in extraction.ts
|
|
34
|
+
* unchanged — deterministic verification stays the single gate. Nothing here
|
|
35
|
+
* assumes a vendor; the model override rides VENDO_EXTRACTION_MODEL, which
|
|
36
|
+
* every harness already honors.
|
|
37
|
+
*/
|
|
38
|
+
export const BRIEF_TEMPLATE = "Describe this product, its users, and the jobs the agent should help them complete.";
|
|
39
|
+
/** Static facts don't clutter passes that only need names — kept small. */
|
|
40
|
+
export const staticToolSchema = z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
description: z.string().optional(),
|
|
43
|
+
risk: z.enum(["read", "write", "destructive"]).optional(),
|
|
44
|
+
disabled: z.boolean().optional(),
|
|
45
|
+
method: z.string().optional(),
|
|
46
|
+
path: z.string().optional(),
|
|
47
|
+
});
|
|
48
|
+
export const surveySchema = z.object({
|
|
49
|
+
/** Frameworks/routers the surveyor identified (context for later stages). */
|
|
50
|
+
frameworks: z.array(z.string().max(120)).optional(),
|
|
51
|
+
surfaces: z.array(z.object({
|
|
52
|
+
name: z.string().min(1).max(80),
|
|
53
|
+
/** One line on where the surface lives in the code. */
|
|
54
|
+
note: z.string().max(300).optional(),
|
|
55
|
+
tools: z.array(z.string().min(1)),
|
|
56
|
+
})).min(1),
|
|
57
|
+
});
|
|
58
|
+
/** Draft and cross-check share one schema: the cross-check may only AMEND
|
|
59
|
+
* the draft within it. */
|
|
60
|
+
const surfaceDraftSchema = z.object({
|
|
61
|
+
tools: z.array(draftToolSchema),
|
|
62
|
+
missedSurfaces: z.array(z.string().max(300)).optional(),
|
|
63
|
+
});
|
|
64
|
+
const briefSchema = z.object({
|
|
65
|
+
brief: z.string().min(1).max(4000),
|
|
66
|
+
});
|
|
67
|
+
/** More surfaces than this get their tail merged into one pass — a runaway
|
|
68
|
+
* survey must not turn into a runaway number of model calls. */
|
|
69
|
+
const MAX_SURFACES = 12;
|
|
70
|
+
export function staticFacts(tools) {
|
|
71
|
+
return JSON.stringify(tools.map((tool) => ({
|
|
72
|
+
name: tool.name,
|
|
73
|
+
...(tool.method === undefined ? {} : { method: tool.method }),
|
|
74
|
+
...(tool.path === undefined ? {} : { path: tool.path }),
|
|
75
|
+
risk: tool.risk,
|
|
76
|
+
...(tool.disabled === true ? { disabled: true } : {}),
|
|
77
|
+
description: tool.description,
|
|
78
|
+
})), null, 2);
|
|
79
|
+
}
|
|
80
|
+
function composeSurveyInstructions(tools, appName) {
|
|
81
|
+
return [
|
|
82
|
+
"You are Vendo's extraction surveyor. Map this repo (Read/Glob/Grep only) so later",
|
|
83
|
+
"focused passes can draft tool documentation surface by surface.",
|
|
84
|
+
"",
|
|
85
|
+
`Product/package name: ${appName}`,
|
|
86
|
+
"Statically extracted tools (name, method+path when known):",
|
|
87
|
+
JSON.stringify(tools.map((tool) => ({
|
|
88
|
+
name: tool.name,
|
|
89
|
+
...(tool.method === undefined ? {} : { method: tool.method }),
|
|
90
|
+
...(tool.path === undefined ? {} : { path: tool.path }),
|
|
91
|
+
})), null, 2),
|
|
92
|
+
"",
|
|
93
|
+
"Rules:",
|
|
94
|
+
"- Reply with ONLY one fenced json block matching:",
|
|
95
|
+
' { "frameworks"?: string[], "surfaces": [{ "name", "note"?, "tools": string[] }] }',
|
|
96
|
+
"- surfaces: group EVERY tool above into a small number of product surfaces (billing,",
|
|
97
|
+
" auth, admin, …) by reading where their routes live. Each tool name appears in exactly",
|
|
98
|
+
" one surface; use only names from the list.",
|
|
99
|
+
"- name: a short surface label. note: one line on where the surface lives in the code.",
|
|
100
|
+
"- frameworks: the frameworks/routers you identified.",
|
|
101
|
+
"- Survey only — do not draft descriptions or risk grades here.",
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
104
|
+
export function composeInstructions(tools, appName, surface) {
|
|
105
|
+
return [
|
|
106
|
+
"You are Vendo's extraction agent. Read this codebase (Read/Glob/Grep only) and return",
|
|
107
|
+
surface === undefined
|
|
108
|
+
? "judgment on the API tools a static extractor already found."
|
|
109
|
+
: `judgment on the API tools a static extractor already found, focusing ONLY on the "${surface.name}" surface.`,
|
|
110
|
+
"",
|
|
111
|
+
`Product/package name: ${appName}`,
|
|
112
|
+
...(surface?.note === undefined ? [] : [`Where this surface lives: ${surface.note}`]),
|
|
113
|
+
"Statically extracted tools (name, method+path when known, current risk, disabled state):",
|
|
114
|
+
staticFacts(tools),
|
|
115
|
+
"",
|
|
116
|
+
"Rules:",
|
|
117
|
+
"- Reply with ONLY one fenced json block matching:",
|
|
118
|
+
' { "tools": [{ "name", "description", "risk"?, "critical"?, "disabled"?, "reasoning"? }], "missedSurfaces"?: string[] }',
|
|
119
|
+
"- tools: include ONLY names from the list above. Rewrite each description so an agent choosing tools understands what it actually does (read the handler source). <= 200 chars each.",
|
|
120
|
+
"- risk: you may RAISE risk (read->write->destructive) when the handler is more dangerous than labeled; never lower it. Mark irreversible operations critical: true.",
|
|
121
|
+
"- A tool listed as disabled was statically unclassifiable. If you can read its handler and grade it, set disabled: false WITH a risk and one-line reasoning. Leave it out otherwise.",
|
|
122
|
+
"- missedSurfaces: API surfaces you found that the list is missing (path + one line). Do not invent tools for them.",
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
function composeCrossCheckInstructions(drafted, tools, appName) {
|
|
126
|
+
return [
|
|
127
|
+
"You are Vendo's extraction cross-checker. Focused passes drafted judgment surface by",
|
|
128
|
+
"surface; review the COMBINED draft for naming consistency, duplicate coverage, and",
|
|
129
|
+
"risk sanity.",
|
|
130
|
+
"",
|
|
131
|
+
`Product/package name: ${appName}`,
|
|
132
|
+
"Combined draft:",
|
|
133
|
+
JSON.stringify(drafted, null, 2),
|
|
134
|
+
"Static facts for the same tools:",
|
|
135
|
+
staticFacts(tools),
|
|
136
|
+
"",
|
|
137
|
+
"Rules:",
|
|
138
|
+
"- Reply with ONLY one fenced json block matching:",
|
|
139
|
+
' { "tools": [{ "name", "description", "risk"?, "critical"?, "disabled"?, "reasoning"? }] }',
|
|
140
|
+
"- Return ONLY the entries you want to AMEND. Entries you omit stand as drafted; you cannot remove a tool. Use only names from the combined draft.",
|
|
141
|
+
"- Same rules as drafting: risk may be RAISED, never lowered; mark irreversible operations critical: true.",
|
|
142
|
+
].join("\n");
|
|
143
|
+
}
|
|
144
|
+
function composeBriefInstructions(input) {
|
|
145
|
+
return [
|
|
146
|
+
"You are Vendo's extraction agent, drafting the product brief. A survey and per-surface",
|
|
147
|
+
"drafting passes already ran; build on what they learned plus the code itself.",
|
|
148
|
+
"",
|
|
149
|
+
`Product/package name: ${input.appName}`,
|
|
150
|
+
...(input.survey === null ? [] : [
|
|
151
|
+
"Surveyed surfaces:",
|
|
152
|
+
JSON.stringify(input.survey.surfaces.map(({ name, note }) => ({ name, ...(note === undefined ? {} : { note }) })), null, 2),
|
|
153
|
+
]),
|
|
154
|
+
"Drafted tools (name + description):",
|
|
155
|
+
JSON.stringify(input.drafted.map(({ name, description }) => ({ name, description })), null, 2),
|
|
156
|
+
"",
|
|
157
|
+
"Rules:",
|
|
158
|
+
'- Reply with ONLY one fenced json block matching: { "brief": string }',
|
|
159
|
+
"- brief: one paragraph — what the product does, who uses it, the jobs the agent should help with. Written from the actual code, no marketing fluff.",
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Instructions for the OPTIONAL theme stage — the SOLE copy of the theme
|
|
164
|
+
* judgment rules (extract-theme.ts's old model pass and its prompt were
|
|
165
|
+
* deleted when the deterministic split landed; that file now owns only the
|
|
166
|
+
* exact pass, validators, and assembly).
|
|
167
|
+
*/
|
|
168
|
+
export function composeThemeInstructions(input) {
|
|
169
|
+
return [
|
|
170
|
+
"You are Vendo's extraction agent, filling the theme's brand slots. Read this codebase",
|
|
171
|
+
"(Read/Glob/Grep — not just the hints below) to fill the brand theme slots a deterministic",
|
|
172
|
+
"allowlist pass could not read exactly.",
|
|
173
|
+
"",
|
|
174
|
+
"Slots: accent (the brand's primary interactive color), accentText (text on accent),",
|
|
175
|
+
"background (page), surface (cards/panels), text (body), mutedText (secondary text),",
|
|
176
|
+
"border (default hairline), danger (destructive/error), radius (default control corner",
|
|
177
|
+
"radius, canonical px), fontFamily (body stack), headingFamily (heading stack, only if",
|
|
178
|
+
"distinct), baseSize (body font size, px), density (compact|comfortable), motion (full|reduced).",
|
|
179
|
+
"",
|
|
180
|
+
`Product/package name: ${input.appName}`,
|
|
181
|
+
"Needed slots:",
|
|
182
|
+
JSON.stringify(input.needed, null, 2),
|
|
183
|
+
"Already exact (context — do not re-derive these):",
|
|
184
|
+
JSON.stringify(input.alreadyExact, null, 2),
|
|
185
|
+
"Evidence files found so far (starting hints — read more of the repo if useful):",
|
|
186
|
+
JSON.stringify(input.evidencePaths, null, 2),
|
|
187
|
+
"",
|
|
188
|
+
"Rules:",
|
|
189
|
+
"- Reply with ONLY one fenced json block matching:",
|
|
190
|
+
' { "slots": { <slot>: string, ... }, "uncertain"?: [{ "slot", "note" }] }',
|
|
191
|
+
"- Fill ONLY the needed slots above, ONLY from evidence in the codebase.",
|
|
192
|
+
"- Colors must be 6-digit hex. Resolve CSS variables and color functions yourself.",
|
|
193
|
+
"- next/font: the imported font's export name is the family (underscores become spaces).",
|
|
194
|
+
"- The geist npm package's GeistSans/GeistMono imports are font sources exactly like next/font.",
|
|
195
|
+
"- fontFamily is the BODY font: the Tailwind `sans`/default fontFamily key. A `display` face",
|
|
196
|
+
" goes to headingFamily, never fontFamily.",
|
|
197
|
+
"- A design-token sheet outranks scattered utility classes; dominant usage outranks one-offs.",
|
|
198
|
+
"- When several tokens could fill the same slot (e.g. multiple muted/soft/faint text inks),",
|
|
199
|
+
" COUNT their usages across the sheets and pick the dominant one; if you did not count or",
|
|
200
|
+
" the counts are close, still fill the slot with your best pick AND list it in uncertain.",
|
|
201
|
+
"- Status/state colors (success, positive, negative, warning, error, overdue, verified,",
|
|
202
|
+
" and colors a comment demotes to data/status-only) are NEVER the brand accent.",
|
|
203
|
+
"- Monochrome brands exist: when the sheet declares no saturated non-status brand color,",
|
|
204
|
+
" the ink/text color itself is the accent (primary buttons are painted with it).",
|
|
205
|
+
"- radius is the default CONTROL radius (buttons/inputs); a token named for cards or",
|
|
206
|
+
" popovers rounds cards, which are typically larger than controls.",
|
|
207
|
+
"- An accessibility-only prefers-reduced-motion override does NOT make the brand 'reduced'.",
|
|
208
|
+
"- Omit any slot the codebase does not evidence. Do not invent plausible values.",
|
|
209
|
+
"- List a slot in `uncertain` ONLY when the codebase genuinely supports multiple different",
|
|
210
|
+
" answers (a real fork, e.g. two plausible brand colors). A value settled by the rules",
|
|
211
|
+
" above — monochrome accent, contrast-derived accentText, single-font inheritance,",
|
|
212
|
+
" browser-default sizing — is NOT uncertain.",
|
|
213
|
+
].join("\n");
|
|
214
|
+
}
|
|
215
|
+
function slugify(name, taken) {
|
|
216
|
+
const base = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "surface";
|
|
217
|
+
let slug = base;
|
|
218
|
+
for (let suffix = 2; taken.has(slug); suffix += 1)
|
|
219
|
+
slug = `${base}-${suffix}`;
|
|
220
|
+
taken.add(slug);
|
|
221
|
+
return slug;
|
|
222
|
+
}
|
|
223
|
+
/** Deterministic normalization of the survey grouping: unknown names are
|
|
224
|
+
* dropped, a tool claimed twice keeps its first surface, unassigned tools
|
|
225
|
+
* land in a catch-all pass, and a runaway surface count is merged down. The
|
|
226
|
+
* drafting stage always covers EVERY static tool exactly once. */
|
|
227
|
+
export function normalizeSurfaces(survey, tools, notes) {
|
|
228
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
229
|
+
const assigned = new Set();
|
|
230
|
+
const surfaces = [];
|
|
231
|
+
for (const surface of survey?.surfaces ?? []) {
|
|
232
|
+
const members = [];
|
|
233
|
+
for (const name of surface.tools) {
|
|
234
|
+
const fact = byName.get(name);
|
|
235
|
+
if (fact === undefined) {
|
|
236
|
+
notes.push(`survey: unknown tool "${name}" in surface "${surface.name}" ignored`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (assigned.has(name))
|
|
240
|
+
continue;
|
|
241
|
+
assigned.add(name);
|
|
242
|
+
members.push(fact);
|
|
243
|
+
}
|
|
244
|
+
if (members.length > 0) {
|
|
245
|
+
surfaces.push({ name: surface.name, ...(surface.note === undefined ? {} : { note: surface.note }), tools: members });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const unassigned = tools.filter((tool) => !assigned.has(tool.name));
|
|
249
|
+
if (unassigned.length > 0) {
|
|
250
|
+
if (surfaces.length > 0) {
|
|
251
|
+
notes.push(`survey left ${unassigned.length} tools unassigned — drafting them as "everything else"`);
|
|
252
|
+
}
|
|
253
|
+
surfaces.push({ name: surfaces.length > 0 ? "everything else" : "all tools", tools: unassigned });
|
|
254
|
+
}
|
|
255
|
+
if (surfaces.length > MAX_SURFACES) {
|
|
256
|
+
const tail = surfaces.splice(MAX_SURFACES - 1);
|
|
257
|
+
surfaces.push({ name: "everything else", tools: tail.flatMap((surface) => surface.tools) });
|
|
258
|
+
notes.push(`survey produced ${MAX_SURFACES - 1 + tail.length} surfaces — merged the tail into one pass`);
|
|
259
|
+
}
|
|
260
|
+
const taken = new Set();
|
|
261
|
+
return surfaces.map((surface) => ({ ...surface, slug: slugify(surface.name, taken) }));
|
|
262
|
+
}
|
|
263
|
+
function message(error) {
|
|
264
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
265
|
+
}
|
|
266
|
+
/** Orchestrate the staged pipeline. Throws only when no stage produced
|
|
267
|
+
* anything usable (the error names the failed stage); partial failures
|
|
268
|
+
* degrade with notes instead. */
|
|
269
|
+
export async function runStagedExtraction(input) {
|
|
270
|
+
const { root, env, harness, tools, appName, onProgress } = input;
|
|
271
|
+
const artifactDir = join(root, ".vendo", "data", "extract");
|
|
272
|
+
await rm(artifactDir, { recursive: true, force: true });
|
|
273
|
+
const notes = [];
|
|
274
|
+
async function runStage(stage, instructions, schema, stageEnv) {
|
|
275
|
+
let text = null;
|
|
276
|
+
try {
|
|
277
|
+
text = await harness.run({ root, env: stageEnv, instructions, ...(onProgress === undefined ? {} : { onProgress }) });
|
|
278
|
+
const artifact = parseArtifact(text, schema);
|
|
279
|
+
await writeText(join(artifactDir, `${stage}.json`), `${JSON.stringify(artifact, null, 2)}\n`);
|
|
280
|
+
return artifact;
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
await writeText(join(artifactDir, `${stage}.json`), `${JSON.stringify({ stage, error: message(error), ...(text === null ? {} : { raw: text }) }, null, 2)}\n`);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Survey: cheap repo map; an override can point it at a faster model. A
|
|
288
|
+
// failed survey degrades to drafting everything as one surface (= v1).
|
|
289
|
+
const surveyModel = env["VENDO_EXTRACTION_SURVEY_MODEL"];
|
|
290
|
+
let survey = null;
|
|
291
|
+
onProgress?.("survey: mapping API surfaces");
|
|
292
|
+
try {
|
|
293
|
+
survey = await runStage("survey", composeSurveyInstructions(tools, appName), surveySchema, surveyModel === undefined ? env : { ...env, VENDO_EXTRACTION_MODEL: surveyModel });
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
notes.push(`survey stage failed (${message(error)}) — drafting all ${tools.length} tools as one surface`);
|
|
297
|
+
}
|
|
298
|
+
const surfaces = normalizeSurfaces(survey, tools, notes);
|
|
299
|
+
// Draft per surface, sequentially. A failed surface is skipped, not fatal —
|
|
300
|
+
// unless EVERY surface failed, which means the harness itself is broken.
|
|
301
|
+
const drafted = new Map();
|
|
302
|
+
const missedSurfaces = [];
|
|
303
|
+
let failures = 0;
|
|
304
|
+
let lastError = "";
|
|
305
|
+
for (const surface of surfaces) {
|
|
306
|
+
onProgress?.(`drafting "${surface.name}" (${surface.tools.length} tools)`);
|
|
307
|
+
try {
|
|
308
|
+
const artifact = await runStage(`draft.${surface.slug}`, composeInstructions(surface.tools, appName, surface), surfaceDraftSchema, env);
|
|
309
|
+
// A focused pass may only draft its own surface's tools — an
|
|
310
|
+
// out-of-surface entry would defeat the containment story (a skipped
|
|
311
|
+
// surface must actually keep extractor defaults).
|
|
312
|
+
const members = new Set(surface.tools.map((tool) => tool.name));
|
|
313
|
+
for (const entry of artifact.tools) {
|
|
314
|
+
if (!members.has(entry.name)) {
|
|
315
|
+
notes.push(`surface "${surface.name}": draft for out-of-surface tool "${entry.name}" ignored`);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
drafted.set(entry.name, entry);
|
|
319
|
+
}
|
|
320
|
+
missedSurfaces.push(...(artifact.missedSurfaces ?? []));
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
failures += 1;
|
|
324
|
+
lastError = message(error);
|
|
325
|
+
notes.push(`surface "${surface.name}" skipped (${lastError}) — its ${surface.tools.length} tools keep extractor defaults`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (surfaces.length > 0 && failures === surfaces.length) {
|
|
329
|
+
throw new Error(`draft stage failed for every surface (${lastError})`);
|
|
330
|
+
}
|
|
331
|
+
// Cross-check: amendments only, within the draft schema. applyDraft remains
|
|
332
|
+
// the gate — an amendment can no more downgrade risk than a draft can.
|
|
333
|
+
if (drafted.size > 0) {
|
|
334
|
+
onProgress?.("cross-check: consistency pass over the combined draft");
|
|
335
|
+
try {
|
|
336
|
+
const artifact = await runStage("cross-check", composeCrossCheckInstructions([...drafted.values()], tools, appName), surfaceDraftSchema, env);
|
|
337
|
+
for (const amendment of artifact.tools) {
|
|
338
|
+
const existing = drafted.get(amendment.name);
|
|
339
|
+
if (existing === undefined) {
|
|
340
|
+
notes.push(`cross-check: amendment for undrafted tool "${amendment.name}" ignored`);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
// Merge, never replace: a description-only amendment must not drop
|
|
344
|
+
// the risk/critical/wake judgment the focused pass produced.
|
|
345
|
+
drafted.set(amendment.name, { ...existing, ...amendment });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
notes.push(`cross-check stage failed (${message(error)}) — using the uncross-checked drafts`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Brief: drafted from what the earlier stages learned; on failure the
|
|
353
|
+
// current brief stands (applyDraft's hand-written-brief guard still wins).
|
|
354
|
+
let brief = null;
|
|
355
|
+
onProgress?.("brief: drafting the product brief");
|
|
356
|
+
try {
|
|
357
|
+
brief = (await runStage("brief", composeBriefInstructions({ appName, survey, drafted: [...drafted.values()] }), briefSchema, env)).brief;
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
notes.push(`brief stage failed (${message(error)}) — keeping the current brief`);
|
|
361
|
+
}
|
|
362
|
+
const briefFromStage = brief !== null;
|
|
363
|
+
if (brief === null) {
|
|
364
|
+
const current = ((await readOptional(join(root, ".vendo", "brief.md"))) ?? "").trim();
|
|
365
|
+
brief = current === "" ? BRIEF_TEMPLATE : current;
|
|
366
|
+
}
|
|
367
|
+
// Theme: OPTIONAL, and only when the allowlist left brand slots unfilled —
|
|
368
|
+
// the non-brand slots (accentText, headingFamily, baseSize, density,
|
|
369
|
+
// motion) derive or default safely and must never trigger this stage on
|
|
370
|
+
// their own.
|
|
371
|
+
let theme;
|
|
372
|
+
const themeInput = input.theme;
|
|
373
|
+
if (themeInput !== undefined) {
|
|
374
|
+
const brandNeeded = themeInput.needed.filter((slot) => BRAND_SLOTS.includes(slot));
|
|
375
|
+
if (brandNeeded.length > 0) {
|
|
376
|
+
onProgress?.("theme: filling brand slots");
|
|
377
|
+
try {
|
|
378
|
+
theme = await runStage("theme", composeThemeInstructions({
|
|
379
|
+
needed: themeInput.needed,
|
|
380
|
+
alreadyExact: themeInput.alreadyExact,
|
|
381
|
+
evidencePaths: themeInput.evidencePaths,
|
|
382
|
+
appName,
|
|
383
|
+
}), modelThemeSchema, env);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
notes.push(`theme stage failed (${message(error)}) — exact reads and defaults stand`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const missed = [...new Set(missedSurfaces)];
|
|
391
|
+
const draft = {
|
|
392
|
+
brief,
|
|
393
|
+
tools: [...drafted.values()],
|
|
394
|
+
...(missed.length === 0 ? {} : { missedSurfaces: missed }),
|
|
395
|
+
};
|
|
396
|
+
await writeText(join(artifactDir, "draft.json"), `${JSON.stringify(draft, null, 2)}\n`);
|
|
397
|
+
return { draft, notes, briefFromStage, ...(theme === undefined ? {} : { theme }) };
|
|
398
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type HostFramework = "next" | "express" | "unknown";
|
|
2
|
+
export interface VendoWiring {
|
|
3
|
+
server: boolean;
|
|
4
|
+
client: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function detectFramework(root: string): Promise<HostFramework>;
|
|
7
|
+
/** Bounded source scan shared by init and doctor so their wiring verdicts agree. */
|
|
8
|
+
export declare function detectVendoWiring(root: string): Promise<VendoWiring>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { walk } from "./theme/walk.js";
|
|
4
|
+
const SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/;
|
|
5
|
+
const SOURCE_SCAN_MAX_FILES = 2_000;
|
|
6
|
+
export async function detectFramework(root) {
|
|
7
|
+
try {
|
|
8
|
+
const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
9
|
+
const sections = [manifest.dependencies, manifest.devDependencies];
|
|
10
|
+
if (sections.some((dependencies) => dependencies?.next !== undefined))
|
|
11
|
+
return "next";
|
|
12
|
+
if (sections.some((dependencies) => dependencies?.express !== undefined))
|
|
13
|
+
return "express";
|
|
14
|
+
return "unknown";
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return "unknown";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Bounded source scan shared by init and doctor so their wiring verdicts agree. */
|
|
21
|
+
export async function detectVendoWiring(root) {
|
|
22
|
+
let server = false;
|
|
23
|
+
let client = false;
|
|
24
|
+
const files = await walk(root, (relativePath) => SOURCE_FILE.test(relativePath), SOURCE_SCAN_MAX_FILES);
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const source = await readFile(file, "utf8").catch(() => "");
|
|
27
|
+
const code = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
28
|
+
if (code.includes("@vendoai/vendo/server") && /\bcreateVendo\s*\(/.test(code))
|
|
29
|
+
server = true;
|
|
30
|
+
if (code.includes("<VendoRoot"))
|
|
31
|
+
client = true;
|
|
32
|
+
if (server && client)
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
return { server, client };
|
|
36
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { SelectOption } from "./pretty.js";
|
|
2
|
+
/** The auth families init detects in package.json (09-vendo §2.1). */
|
|
3
|
+
export type AuthPresetName = "authJs" | "clerk" | "supabase" | "auth0";
|
|
4
|
+
/** Each zero-arg preset function ships on its own subpath — not
|
|
5
|
+
`@vendoai/vendo/server` — so importing one preset never resolves the
|
|
6
|
+
others' optional peer deps (corpus-triage Task 9: a shared barrel meant
|
|
7
|
+
ANY host importing the server entry statically re-resolved every
|
|
8
|
+
preset's optional peer, e.g. @auth/core, even unused). Scaffolded code
|
|
9
|
+
imports the preset from here and createVendo/nextVendoHandler from
|
|
10
|
+
"@vendoai/vendo/server" separately. */
|
|
11
|
+
export declare const AUTH_PRESET_SPECIFIER: Record<AuthPresetName, string>;
|
|
12
|
+
export interface AuthMatch {
|
|
13
|
+
preset: AuthPresetName;
|
|
14
|
+
dependency: string;
|
|
15
|
+
/** How the family was chosen: detection (default) cites the dependency it
|
|
16
|
+
found; a picker pick says so honestly — nothing was detected. */
|
|
17
|
+
source?: "picked";
|
|
18
|
+
}
|
|
19
|
+
export interface AuthDetection {
|
|
20
|
+
/** Exactly one family matched — the preset init wires silently. */
|
|
21
|
+
wired: AuthMatch | null;
|
|
22
|
+
/** Every family that matched (for the ambiguity advisory). */
|
|
23
|
+
matches: AuthMatch[];
|
|
24
|
+
}
|
|
25
|
+
export declare const AUTH_FAMILIES: ReadonlyArray<{
|
|
26
|
+
preset: AuthPresetName;
|
|
27
|
+
test: (dependency: string) => boolean;
|
|
28
|
+
}>;
|
|
29
|
+
/** Silent auth-preset detection from the host's package.json (zero-question
|
|
30
|
+
contract): one unambiguous family gets wired; none or several stay
|
|
31
|
+
anonymous and become one advisory line (detection-as-advice). */
|
|
32
|
+
export declare function detectAuthPreset(root: string): Promise<AuthDetection>;
|
|
33
|
+
/** The one calm auth line for the none/ambiguous cases — names the exact
|
|
34
|
+
line to add, never asks a question. Emitted only when init scaffolds the
|
|
35
|
+
composition (a hand-wired host may already have auth). */
|
|
36
|
+
export declare function authAdvisory(detection: AuthDetection, compositionPath: string): string | null;
|
|
37
|
+
/** The declined-confirm advisory: anonymous composition, exact line in hand. */
|
|
38
|
+
export declare function declinedAuthAdvisory(match: AuthMatch, compositionPath: string): string;
|
|
39
|
+
export type ConfirmAuth = (question: string, defaultYes: boolean) => Promise<boolean>;
|
|
40
|
+
export type SelectAuth = (question: string, options: SelectOption[]) => Promise<string>;
|
|
41
|
+
/** Picker labels + the runtime package each zero-arg preset lazy-loads (the
|
|
42
|
+
install hint when the picked family's SDK is absent; the preset's own
|
|
43
|
+
lazy-load error already guards runtime). */
|
|
44
|
+
export declare const AUTH_FAMILY_INFO: Record<AuthPresetName, {
|
|
45
|
+
name: string;
|
|
46
|
+
label: string;
|
|
47
|
+
runtime: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** The auth picker (decline or ambiguity): none — stay anonymous — is first
|
|
50
|
+
and the default; detected families come next (named), then the remaining
|
|
51
|
+
zero-arg presets, then jwt (recipe only — it cannot be zero-arg). */
|
|
52
|
+
export declare function pickScaffoldAuth(detection: AuthDetection, compositionPath: string, selectAuth: SelectAuth): Promise<{
|
|
53
|
+
wired: AuthMatch | null;
|
|
54
|
+
advice: string | null;
|
|
55
|
+
}>;
|
|
56
|
+
/** Detect + confirm + choose: in interactive runs, exactly one detected
|
|
57
|
+
family gets ONE calm [Y/n] question before anything is written (Enter
|
|
58
|
+
accepts and wires it — no picker on the happy path). A decline — and the
|
|
59
|
+
ambiguous case (several families) — offers the picker instead of settling
|
|
60
|
+
for anonymous. Without the seams (non-interactive, --yes, --agent) silent
|
|
61
|
+
detection stands and none/ambiguous keep the advisory line — a default
|
|
62
|
+
has to exist. None-detected never asks: there is nothing to choose from
|
|
63
|
+
that the advisory doesn't already name. */
|
|
64
|
+
export declare function resolveScaffoldAuth(root: string, compositionPath: string, authAnswer: AuthPresetName | "jwt" | "none" | undefined, confirmAuth: ConfirmAuth | undefined, selectAuth: SelectAuth | undefined): Promise<{
|
|
65
|
+
wired: AuthMatch | null;
|
|
66
|
+
advice: string | null;
|
|
67
|
+
}>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readOptional } from "./shared.js";
|
|
3
|
+
/** Each zero-arg preset function ships on its own subpath — not
|
|
4
|
+
`@vendoai/vendo/server` — so importing one preset never resolves the
|
|
5
|
+
others' optional peer deps (corpus-triage Task 9: a shared barrel meant
|
|
6
|
+
ANY host importing the server entry statically re-resolved every
|
|
7
|
+
preset's optional peer, e.g. @auth/core, even unused). Scaffolded code
|
|
8
|
+
imports the preset from here and createVendo/nextVendoHandler from
|
|
9
|
+
"@vendoai/vendo/server" separately. */
|
|
10
|
+
export const AUTH_PRESET_SPECIFIER = {
|
|
11
|
+
authJs: "@vendoai/vendo/auth/auth-js",
|
|
12
|
+
clerk: "@vendoai/vendo/auth/clerk",
|
|
13
|
+
supabase: "@vendoai/vendo/auth/supabase",
|
|
14
|
+
auth0: "@vendoai/vendo/auth/auth0",
|
|
15
|
+
};
|
|
16
|
+
export const AUTH_FAMILIES = [
|
|
17
|
+
{ preset: "authJs", test: (dependency) => dependency === "next-auth" || dependency.startsWith("@auth/") },
|
|
18
|
+
{ preset: "clerk", test: (dependency) => dependency.startsWith("@clerk/") },
|
|
19
|
+
{ preset: "supabase", test: (dependency) => dependency.startsWith("@supabase/") },
|
|
20
|
+
{ preset: "auth0", test: (dependency) => dependency.startsWith("@auth0/") },
|
|
21
|
+
];
|
|
22
|
+
/** Silent auth-preset detection from the host's package.json (zero-question
|
|
23
|
+
contract): one unambiguous family gets wired; none or several stay
|
|
24
|
+
anonymous and become one advisory line (detection-as-advice). */
|
|
25
|
+
export async function detectAuthPreset(root) {
|
|
26
|
+
let dependencies = [];
|
|
27
|
+
try {
|
|
28
|
+
const manifest = JSON.parse((await readOptional(join(root, "package.json"))) ?? "{}");
|
|
29
|
+
dependencies = Object.keys({ ...manifest.dependencies, ...manifest.devDependencies });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// No readable manifest — nothing to detect; anonymous is the safe default.
|
|
33
|
+
}
|
|
34
|
+
const matches = AUTH_FAMILIES.flatMap(({ preset, test }) => {
|
|
35
|
+
const dependency = dependencies.find(test);
|
|
36
|
+
return dependency === undefined ? [] : [{ preset, dependency }];
|
|
37
|
+
});
|
|
38
|
+
return { wired: matches.length === 1 ? matches[0] : null, matches };
|
|
39
|
+
}
|
|
40
|
+
/** The one calm auth line for the none/ambiguous cases — names the exact
|
|
41
|
+
line to add, never asks a question. Emitted only when init scaffolds the
|
|
42
|
+
composition (a hand-wired host may already have auth). */
|
|
43
|
+
export function authAdvisory(detection, compositionPath) {
|
|
44
|
+
if (detection.wired !== null)
|
|
45
|
+
return null;
|
|
46
|
+
if (detection.matches.length === 0) {
|
|
47
|
+
return `Auth: no provider detected — sessions stay anonymous. When you add one, add one line in ${compositionPath}: ` +
|
|
48
|
+
`auth: authJs() (Auth.js), clerk(), supabase(), auth0(), or jwt({ secret }).`;
|
|
49
|
+
}
|
|
50
|
+
const names = detection.matches.map((match) => match.dependency).join(", ");
|
|
51
|
+
const calls = detection.matches.map((match) => `auth: ${match.preset}()`).join(" or ");
|
|
52
|
+
return `Auth: several providers detected (${names}) — staying anonymous rather than guessing. Add one line in ${compositionPath}: ${calls}.`;
|
|
53
|
+
}
|
|
54
|
+
/** The declined-confirm advisory: anonymous composition, exact line in hand. */
|
|
55
|
+
export function declinedAuthAdvisory(match, compositionPath) {
|
|
56
|
+
return `Auth: left anonymous. To wire ${match.dependency} later, add one line in ${compositionPath}: auth: ${match.preset}().`;
|
|
57
|
+
}
|
|
58
|
+
/** Picker labels + the runtime package each zero-arg preset lazy-loads (the
|
|
59
|
+
install hint when the picked family's SDK is absent; the preset's own
|
|
60
|
+
lazy-load error already guards runtime). */
|
|
61
|
+
export const AUTH_FAMILY_INFO = {
|
|
62
|
+
authJs: { name: "Auth.js", label: "authJs() — Auth.js / next-auth", runtime: "@auth/core" },
|
|
63
|
+
clerk: { name: "Clerk", label: "clerk() — Clerk", runtime: "@clerk/backend" },
|
|
64
|
+
supabase: { name: "Supabase Auth", label: "supabase() — Supabase Auth", runtime: "jose" },
|
|
65
|
+
auth0: { name: "Auth0", label: "auth0() — Auth0", runtime: "jose" },
|
|
66
|
+
};
|
|
67
|
+
/** The auth picker (decline or ambiguity): none — stay anonymous — is first
|
|
68
|
+
and the default; detected families come next (named), then the remaining
|
|
69
|
+
zero-arg presets, then jwt (recipe only — it cannot be zero-arg). */
|
|
70
|
+
export async function pickScaffoldAuth(detection, compositionPath, selectAuth) {
|
|
71
|
+
const detected = detection.matches;
|
|
72
|
+
const undetected = Object.keys(AUTH_FAMILY_INFO)
|
|
73
|
+
.filter((preset) => !detected.some((match) => match.preset === preset));
|
|
74
|
+
const picked = await selectAuth("Which auth should Vendo wire?", [
|
|
75
|
+
{ value: "none", label: "none — stay anonymous, add it later" },
|
|
76
|
+
...detected.map((match) => ({
|
|
77
|
+
value: match.preset,
|
|
78
|
+
label: AUTH_FAMILY_INFO[match.preset].label,
|
|
79
|
+
hint: `detected ${match.dependency}`,
|
|
80
|
+
})),
|
|
81
|
+
...undetected.map((preset) => ({ value: preset, label: AUTH_FAMILY_INFO[preset].label })),
|
|
82
|
+
{ value: "jwt", label: "jwt — my own JWT scheme (prints the recipe)" },
|
|
83
|
+
]);
|
|
84
|
+
if (picked === "jwt") {
|
|
85
|
+
// jwt() cannot be zero-arg — nothing is wired; the recipe is the answer.
|
|
86
|
+
return {
|
|
87
|
+
wired: null,
|
|
88
|
+
advice: `Auth: your own JWT — add one line in ${compositionPath}: auth: jwt({ secret: <your signing secret> }). ` +
|
|
89
|
+
"Options and the claim mapping: docs/act-as-presets.md.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const detectedMatch = detected.find((match) => match.preset === picked);
|
|
93
|
+
if (detectedMatch !== undefined)
|
|
94
|
+
return { wired: detectedMatch, advice: null };
|
|
95
|
+
if (picked in AUTH_FAMILY_INFO) {
|
|
96
|
+
// Picked without its SDK in package.json: wire it exactly like a
|
|
97
|
+
// detection-accept, plus one install hint.
|
|
98
|
+
const preset = picked;
|
|
99
|
+
const info = AUTH_FAMILY_INFO[preset];
|
|
100
|
+
return {
|
|
101
|
+
wired: { preset, dependency: info.runtime, source: "picked" },
|
|
102
|
+
advice: `Auth: ${preset}() wired — ${info.runtime} is not in package.json yet; install it ` +
|
|
103
|
+
`(npm install ${info.runtime}) before the first authenticated run (the preset fails loud until then).`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// none (or anything unrecognized): today's decline behavior.
|
|
107
|
+
return detection.wired !== null
|
|
108
|
+
? { wired: null, advice: declinedAuthAdvisory(detection.wired, compositionPath) }
|
|
109
|
+
: { wired: null, advice: authAdvisory(detection, compositionPath) };
|
|
110
|
+
}
|
|
111
|
+
/** Detect + confirm + choose: in interactive runs, exactly one detected
|
|
112
|
+
family gets ONE calm [Y/n] question before anything is written (Enter
|
|
113
|
+
accepts and wires it — no picker on the happy path). A decline — and the
|
|
114
|
+
ambiguous case (several families) — offers the picker instead of settling
|
|
115
|
+
for anonymous. Without the seams (non-interactive, --yes, --agent) silent
|
|
116
|
+
detection stands and none/ambiguous keep the advisory line — a default
|
|
117
|
+
has to exist. None-detected never asks: there is nothing to choose from
|
|
118
|
+
that the advisory doesn't already name. */
|
|
119
|
+
export async function resolveScaffoldAuth(root, compositionPath, authAnswer, confirmAuth, selectAuth) {
|
|
120
|
+
const detection = await detectAuthPreset(root);
|
|
121
|
+
// --auth answers the confirm AND the picker in one flag: route it through
|
|
122
|
+
// the picker path so a flag answer and an interactive pick wire identically
|
|
123
|
+
// (detection-accept, install hint, jwt recipe, none advisory).
|
|
124
|
+
if (authAnswer !== undefined) {
|
|
125
|
+
return pickScaffoldAuth(detection, compositionPath, async () => authAnswer);
|
|
126
|
+
}
|
|
127
|
+
if (confirmAuth === undefined) {
|
|
128
|
+
return { wired: detection.wired, advice: authAdvisory(detection, compositionPath) };
|
|
129
|
+
}
|
|
130
|
+
if (detection.wired !== null) {
|
|
131
|
+
const accepted = await confirmAuth(`Detected ${detection.wired.dependency} — wire auth: ${detection.wired.preset}()?`, true);
|
|
132
|
+
if (accepted)
|
|
133
|
+
return { wired: detection.wired, advice: null };
|
|
134
|
+
if (selectAuth !== undefined)
|
|
135
|
+
return pickScaffoldAuth(detection, compositionPath, selectAuth);
|
|
136
|
+
return { wired: null, advice: declinedAuthAdvisory(detection.wired, compositionPath) };
|
|
137
|
+
}
|
|
138
|
+
if (detection.matches.length > 1 && selectAuth !== undefined) {
|
|
139
|
+
return pickScaffoldAuth(detection, compositionPath, selectAuth);
|
|
140
|
+
}
|
|
141
|
+
return { wired: null, advice: authAdvisory(detection, compositionPath) };
|
|
142
|
+
}
|