@vendoai/vendo 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +28 -0
  3. package/bin/vendo.mjs +4 -0
  4. package/dist/ai-sdk.d.ts +35 -0
  5. package/dist/ai-sdk.js +62 -0
  6. package/dist/auth-presets/auth-js.d.ts +14 -0
  7. package/dist/auth-presets/auth-js.js +91 -0
  8. package/dist/auth-presets/auth0.d.ts +23 -0
  9. package/dist/auth-presets/auth0.js +98 -0
  10. package/dist/auth-presets/clerk.d.ts +23 -0
  11. package/dist/auth-presets/clerk.js +65 -0
  12. package/dist/auth-presets/conformance.d.ts +42 -0
  13. package/dist/auth-presets/conformance.js +139 -0
  14. package/dist/auth-presets/identity.d.ts +56 -0
  15. package/dist/auth-presets/identity.js +152 -0
  16. package/dist/auth-presets/index.d.ts +14 -0
  17. package/dist/auth-presets/index.js +12 -0
  18. package/dist/auth-presets/jwt.d.ts +16 -0
  19. package/dist/auth-presets/jwt.js +54 -0
  20. package/dist/auth-presets/shared.d.ts +35 -0
  21. package/dist/auth-presets/shared.js +1 -0
  22. package/dist/auth-presets/supabase.d.ts +42 -0
  23. package/dist/auth-presets/supabase.js +229 -0
  24. package/dist/byo-approvals.d.ts +37 -0
  25. package/dist/byo-approvals.js +189 -0
  26. package/dist/capability-misses.d.ts +38 -0
  27. package/dist/capability-misses.js +172 -0
  28. package/dist/catalog.d.ts +19 -0
  29. package/dist/catalog.js +152 -0
  30. package/dist/cli/cloud/args.d.ts +3 -0
  31. package/dist/cli/cloud/args.js +32 -0
  32. package/dist/cli/cloud/auth.d.ts +17 -0
  33. package/dist/cli/cloud/auth.js +123 -0
  34. package/dist/cli/cloud/client.d.ts +28 -0
  35. package/dist/cli/cloud/client.js +133 -0
  36. package/dist/cli/cloud/command.d.ts +22 -0
  37. package/dist/cli/cloud/command.js +73 -0
  38. package/dist/cli/cloud/deploy.d.ts +26 -0
  39. package/dist/cli/cloud/deploy.js +187 -0
  40. package/dist/cli/cloud/device-login.d.ts +32 -0
  41. package/dist/cli/cloud/device-login.js +259 -0
  42. package/dist/cli/cloud/index.d.ts +5 -0
  43. package/dist/cli/cloud/index.js +59 -0
  44. package/dist/cli/cloud/keys.d.ts +2 -0
  45. package/dist/cli/cloud/keys.js +31 -0
  46. package/dist/cli/cloud/members.d.ts +3 -0
  47. package/dist/cli/cloud/members.js +25 -0
  48. package/dist/cli/cloud/output.d.ts +4 -0
  49. package/dist/cli/cloud/output.js +12 -0
  50. package/dist/cli/cloud/pending-claim.d.ts +29 -0
  51. package/dist/cli/cloud/pending-claim.js +38 -0
  52. package/dist/cli/cloud/read.d.ts +3 -0
  53. package/dist/cli/cloud/read.js +12 -0
  54. package/dist/cli/cloud/services.d.ts +2 -0
  55. package/dist/cli/cloud/services.js +9 -0
  56. package/dist/cli/cloud/session.d.ts +13 -0
  57. package/dist/cli/cloud/session.js +41 -0
  58. package/dist/cli/cloud-init.d.ts +60 -0
  59. package/dist/cli/cloud-init.js +149 -0
  60. package/dist/cli/dep-versions.d.ts +26 -0
  61. package/dist/cli/dep-versions.js +77 -0
  62. package/dist/cli/doctor-codes.d.ts +58 -0
  63. package/dist/cli/doctor-codes.js +60 -0
  64. package/dist/cli/doctor-live.d.ts +67 -0
  65. package/dist/cli/doctor-live.js +169 -0
  66. package/dist/cli/doctor.d.ts +52 -0
  67. package/dist/cli/doctor.js +467 -0
  68. package/dist/cli/eject.d.ts +29 -0
  69. package/dist/cli/eject.js +141 -0
  70. package/dist/cli/extract/apply.d.ts +26 -0
  71. package/dist/cli/extract/apply.js +123 -0
  72. package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
  73. package/dist/cli/extract/claude-cli-harness.js +132 -0
  74. package/dist/cli/extract/claude-harness.d.ts +14 -0
  75. package/dist/cli/extract/claude-harness.js +127 -0
  76. package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
  77. package/dist/cli/extract/codex-cli-harness.js +154 -0
  78. package/dist/cli/extract/delegate.d.ts +21 -0
  79. package/dist/cli/extract/delegate.js +77 -0
  80. package/dist/cli/extract/extraction.d.ts +65 -0
  81. package/dist/cli/extract/extraction.js +300 -0
  82. package/dist/cli/extract/gateway-fuel.d.ts +69 -0
  83. package/dist/cli/extract/gateway-fuel.js +69 -0
  84. package/dist/cli/extract/harness.d.ts +114 -0
  85. package/dist/cli/extract/harness.js +84 -0
  86. package/dist/cli/extract/index.d.ts +9 -0
  87. package/dist/cli/extract/index.js +9 -0
  88. package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
  89. package/dist/cli/extract/npx-engine-harness.js +225 -0
  90. package/dist/cli/extract/stages.d.ts +149 -0
  91. package/dist/cli/extract/stages.js +398 -0
  92. package/dist/cli/framework.d.ts +8 -0
  93. package/dist/cli/framework.js +36 -0
  94. package/dist/cli/init-auth.d.ts +67 -0
  95. package/dist/cli/init-auth.js +142 -0
  96. package/dist/cli/init-scaffolds.d.ts +28 -0
  97. package/dist/cli/init-scaffolds.js +235 -0
  98. package/dist/cli/init.d.ts +115 -0
  99. package/dist/cli/init.js +1008 -0
  100. package/dist/cli/mcp/index.d.ts +7 -0
  101. package/dist/cli/mcp/index.js +59 -0
  102. package/dist/cli/mcp/registry.d.ts +9 -0
  103. package/dist/cli/mcp/registry.js +117 -0
  104. package/dist/cli/mcp/server-json.d.ts +12 -0
  105. package/dist/cli/mcp/server-json.js +65 -0
  106. package/dist/cli/mcp/server.schema.json +574 -0
  107. package/dist/cli/mcp/verify-domain.d.ts +11 -0
  108. package/dist/cli/mcp/verify-domain.js +48 -0
  109. package/dist/cli/playground/app/embed-entry.d.ts +20 -0
  110. package/dist/cli/playground/app/embed-entry.js +46 -0
  111. package/dist/cli/playground/app/fake-client.d.ts +3 -0
  112. package/dist/cli/playground/app/fake-client.js +215 -0
  113. package/dist/cli/playground/app/fixtures.d.ts +47 -0
  114. package/dist/cli/playground/app/fixtures.js +472 -0
  115. package/dist/cli/playground/app/main.d.ts +1 -0
  116. package/dist/cli/playground/app/main.js +108 -0
  117. package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
  118. package/dist/cli/playground/app/scenario-mount.js +48 -0
  119. package/dist/cli/playground/app/scenarios.d.ts +25 -0
  120. package/dist/cli/playground/app/scenarios.js +152 -0
  121. package/dist/cli/playground/app/theme-editor.d.ts +13 -0
  122. package/dist/cli/playground/app/theme-editor.js +149 -0
  123. package/dist/cli/playground/app/theme-state.d.ts +29 -0
  124. package/dist/cli/playground/app/theme-state.js +151 -0
  125. package/dist/cli/playground/bundle.gen.d.ts +2 -0
  126. package/dist/cli/playground/bundle.gen.js +2 -0
  127. package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
  128. package/dist/cli/playground/embed-bundle.gen.js +2 -0
  129. package/dist/cli/playground.d.ts +33 -0
  130. package/dist/cli/playground.js +120 -0
  131. package/dist/cli/pretty.d.ts +48 -0
  132. package/dist/cli/pretty.js +330 -0
  133. package/dist/cli/refine.d.ts +43 -0
  134. package/dist/cli/refine.js +181 -0
  135. package/dist/cli/semantics.d.ts +31 -0
  136. package/dist/cli/semantics.js +122 -0
  137. package/dist/cli/shared.d.ts +68 -0
  138. package/dist/cli/shared.js +184 -0
  139. package/dist/cli/sync.d.ts +36 -0
  140. package/dist/cli/sync.js +202 -0
  141. package/dist/cli/theme/color.d.ts +15 -0
  142. package/dist/cli/theme/color.js +188 -0
  143. package/dist/cli/theme/css-vars.d.ts +13 -0
  144. package/dist/cli/theme/css-vars.js +40 -0
  145. package/dist/cli/theme/entry-candidates.d.ts +5 -0
  146. package/dist/cli/theme/entry-candidates.js +22 -0
  147. package/dist/cli/theme/extract-theme.d.ts +184 -0
  148. package/dist/cli/theme/extract-theme.js +358 -0
  149. package/dist/cli/theme/walk.d.ts +2 -0
  150. package/dist/cli/theme/walk.js +24 -0
  151. package/dist/cli.d.ts +2 -0
  152. package/dist/cli.js +333 -0
  153. package/dist/cloud-apps.d.ts +17 -0
  154. package/dist/cloud-apps.js +46 -0
  155. package/dist/cloud-console.d.ts +32 -0
  156. package/dist/cloud-console.js +72 -0
  157. package/dist/cloud-tools.d.ts +23 -0
  158. package/dist/cloud-tools.js +196 -0
  159. package/dist/connections.d.ts +71 -0
  160. package/dist/connections.js +192 -0
  161. package/dist/deployment-identity.d.ts +12 -0
  162. package/dist/deployment-identity.js +69 -0
  163. package/dist/dev-creds/model.d.ts +65 -0
  164. package/dist/dev-creds/model.js +159 -0
  165. package/dist/dev-creds/resolve.d.ts +36 -0
  166. package/dist/dev-creds/resolve.js +57 -0
  167. package/dist/hosted-store.d.ts +52 -0
  168. package/dist/hosted-store.js +322 -0
  169. package/dist/index.d.ts +13 -0
  170. package/dist/index.js +1 -0
  171. package/dist/mastra.d.ts +46 -0
  172. package/dist/mastra.js +88 -0
  173. package/dist/react.d.ts +10 -0
  174. package/dist/react.js +32 -0
  175. package/dist/refine.d.ts +244 -0
  176. package/dist/refine.js +599 -0
  177. package/dist/remixable.d.ts +18 -0
  178. package/dist/remixable.js +42 -0
  179. package/dist/runtime-capture.d.ts +18 -0
  180. package/dist/runtime-capture.js +111 -0
  181. package/dist/sandbox-wire.d.ts +101 -0
  182. package/dist/sandbox-wire.js +101 -0
  183. package/dist/sandbox.d.ts +38 -0
  184. package/dist/sandbox.js +389 -0
  185. package/dist/server.d.ts +216 -0
  186. package/dist/server.js +1320 -0
  187. package/dist/sync-impact.d.ts +14 -0
  188. package/dist/sync-impact.js +75 -0
  189. package/dist/turn-liveness.d.ts +33 -0
  190. package/dist/turn-liveness.js +105 -0
  191. package/dist/wire/approvals.d.ts +10 -0
  192. package/dist/wire/approvals.js +59 -0
  193. package/dist/wire/apps.d.ts +5 -0
  194. package/dist/wire/apps.js +155 -0
  195. package/dist/wire/automations.d.ts +5 -0
  196. package/dist/wire/automations.js +57 -0
  197. package/dist/wire/box.d.ts +3 -0
  198. package/dist/wire/box.js +268 -0
  199. package/dist/wire/connections.d.ts +5 -0
  200. package/dist/wire/connections.js +47 -0
  201. package/dist/wire/context.d.ts +22 -0
  202. package/dist/wire/context.js +190 -0
  203. package/dist/wire/doctor.d.ts +19 -0
  204. package/dist/wire/doctor.js +116 -0
  205. package/dist/wire/misc.d.ts +18 -0
  206. package/dist/wire/misc.js +227 -0
  207. package/dist/wire/shared.d.ts +158 -0
  208. package/dist/wire/shared.js +134 -0
  209. package/dist/wire/threads.d.ts +3 -0
  210. package/dist/wire/threads.js +68 -0
  211. package/package.json +151 -0
  212. package/skills/vendo-setup/SKILL.md +128 -0
@@ -0,0 +1,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
+ }