@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,33 @@
1
+ import { type Output, type TelemetryOptions } from "./shared.js";
2
+ /**
3
+ * `vendo playground` — a local page rendering every shipped Vendo surface
4
+ * against scripted (director-mode) data: no model key, no database, no host
5
+ * app. install-dx design §8; the page itself ships in ./playground/.
6
+ */
7
+ export interface PlaygroundServer {
8
+ url: string;
9
+ close(): Promise<void>;
10
+ }
11
+ export declare function startPlaygroundServer(options: {
12
+ port?: number;
13
+ }): Promise<PlaygroundServer>;
14
+ export interface PlaygroundOptions {
15
+ port?: number;
16
+ /** When false (`--no-open`), print the URL without launching the browser. */
17
+ open?: boolean;
18
+ output?: Output;
19
+ /** Test seams. */
20
+ openBrowser?: (url: string) => void;
21
+ /** When false, return right after startup instead of blocking until Ctrl+C. */
22
+ wait?: boolean;
23
+ /** Injectable telemetry deps (matches init/doctor). */
24
+ telemetry?: TelemetryOptions;
25
+ }
26
+ /** Windows' `start` is a cmd built-in, not an executable — execFile can only
27
+ * reach it through `cmd /c start "" <url>` (the empty string is the window
28
+ * title, so a URL is never mistaken for one). */
29
+ export declare function browserOpenCommand(platform: NodeJS.Platform, url: string): {
30
+ command: string;
31
+ args: string[];
32
+ };
33
+ export declare function runPlayground(options?: PlaygroundOptions): Promise<number>;
@@ -0,0 +1,120 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createServer } from "node:http";
3
+ import { PLAYGROUND_BUNDLE_SOURCE } from "./playground/bundle.gen.js";
4
+ import { EMBED_BUNDLE_SOURCE } from "./playground/embed-bundle.gen.js";
5
+ import { consoleOutput, withCommandRun } from "./shared.js";
6
+ function pageHtml() {
7
+ // Cache-busting is belt and braces: no-store headers do the real work, and
8
+ // the length-keyed ?v= keeps even a heuristically-cached HTML copy from
9
+ // pairing with a mismatched bundle after most rebuilds.
10
+ return `<!doctype html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8" />
14
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
15
+ <title>Vendo Playground</title>
16
+ </head>
17
+ <body>
18
+ <div id="root"></div>
19
+ <script src="/playground.js?v=${PLAYGROUND_BUNDLE_SOURCE.length.toString(36)}"></script>
20
+ </body>
21
+ </html>
22
+ `;
23
+ }
24
+ export async function startPlaygroundServer(options) {
25
+ const server = createServer((request, response) => {
26
+ if (request.method !== "GET") {
27
+ response.writeHead(405).end();
28
+ return;
29
+ }
30
+ const path = new URL(request.url ?? "/", "http://localhost").pathname;
31
+ if (path === "/playground.js") {
32
+ response.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "cache-control": "no-store" });
33
+ response.end(PLAYGROUND_BUNDLE_SOURCE);
34
+ return;
35
+ }
36
+ // The docs inline-embed bundle (window.VendoDocsEmbed) — served here so
37
+ // self-hosted docs/pages can mount surfaces without vendo.run. CORS-open:
38
+ // it's a public static script by design.
39
+ if (path === "/embed.js") {
40
+ response.writeHead(200, {
41
+ "content-type": "application/javascript; charset=utf-8",
42
+ "cache-control": "no-store",
43
+ "access-control-allow-origin": "*",
44
+ });
45
+ response.end(EMBED_BUNDLE_SOURCE);
46
+ return;
47
+ }
48
+ if (path === "/favicon.ico") {
49
+ response.writeHead(204).end();
50
+ return;
51
+ }
52
+ if (path !== "/" && path !== "/index.html") {
53
+ response.writeHead(404, { "content-type": "text/plain" }).end("Not found");
54
+ return;
55
+ }
56
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
57
+ response.end(pageHtml());
58
+ });
59
+ await new Promise((resolve, reject) => {
60
+ server.once("error", reject);
61
+ server.listen(options.port ?? 0, "127.0.0.1", () => {
62
+ // Detach the bind-failure handler so a later runtime error is not
63
+ // swallowed by rejecting an already-settled promise.
64
+ server.removeListener("error", reject);
65
+ resolve();
66
+ });
67
+ });
68
+ const { port } = server.address();
69
+ return {
70
+ url: `http://127.0.0.1:${port}`,
71
+ close: () => new Promise((resolve, reject) => {
72
+ server.closeAllConnections();
73
+ server.close((error) => (error ? reject(error) : resolve()));
74
+ }),
75
+ };
76
+ }
77
+ /** Windows' `start` is a cmd built-in, not an executable — execFile can only
78
+ * reach it through `cmd /c start "" <url>` (the empty string is the window
79
+ * title, so a URL is never mistaken for one). */
80
+ export function browserOpenCommand(platform, url) {
81
+ if (platform === "darwin")
82
+ return { command: "open", args: [url] };
83
+ if (platform === "win32")
84
+ return { command: "cmd", args: ["/c", "start", "", url] };
85
+ return { command: "xdg-open", args: [url] };
86
+ }
87
+ function defaultOpenBrowser(url) {
88
+ const { command, args } = browserOpenCommand(process.platform, url);
89
+ execFile(command, args, () => undefined);
90
+ }
91
+ export async function runPlayground(options = {}) {
92
+ const output = options.output ?? consoleOutput;
93
+ return withCommandRun({
94
+ command: "playground",
95
+ ...(options.telemetry === undefined ? {} : { telemetry: options.telemetry }),
96
+ }, (failure) => playground(options, output, failure));
97
+ }
98
+ async function playground(options, output, failure) {
99
+ let server;
100
+ try {
101
+ server = await startPlaygroundServer({ port: options.port });
102
+ }
103
+ catch (error) {
104
+ const detail = error instanceof Error ? error.message : "unknown error";
105
+ failure.failedStep = "listen";
106
+ output.error(`vendo playground: could not listen on port ${options.port ?? 0} (${detail}) — pass a different --port`);
107
+ return 1;
108
+ }
109
+ output.log(`Vendo playground running at ${server.url}`);
110
+ output.log("Every surface, scripted data, no model key. Press Ctrl+C to stop.");
111
+ if (options.open !== false)
112
+ (options.openBrowser ?? defaultOpenBrowser)(server.url);
113
+ if (options.wait === false) {
114
+ await server.close();
115
+ return 0;
116
+ }
117
+ await new Promise((resolve) => process.once("SIGINT", resolve));
118
+ await server.close();
119
+ return 0;
120
+ }
@@ -0,0 +1,48 @@
1
+ import type { Output } from "./shared.js";
2
+ /** TTY + no opt-outs → the pretty renderer; anything else keeps plain output.
3
+ NO_COLOR and CI follow the "present and non-empty" convention. */
4
+ export declare function usePrettyOutput(stream?: {
5
+ isTTY?: boolean;
6
+ }, env?: Record<string, string | undefined>): boolean;
7
+ export interface SelectOption {
8
+ value: string;
9
+ label: string;
10
+ /** Dim parenthetical after the label (e.g. what detection found). */
11
+ hint?: string;
12
+ }
13
+ /** The slice of a readable TTY stream the select loop needs (injectable for
14
+ tests — a plain emitter drives the keypress parser without a PTY). */
15
+ export interface SelectInput {
16
+ isTTY?: boolean;
17
+ setRawMode?(mode: boolean): unknown;
18
+ resume?(): unknown;
19
+ pause?(): unknown;
20
+ on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
21
+ off(event: "data", listener: (chunk: Buffer | string) => void): unknown;
22
+ }
23
+ export interface PrettyOutput extends Output {
24
+ /** Dots spinner for a slow phase; any log/error line clears the frame. */
25
+ spin(label: string): void;
26
+ stopSpin(): void;
27
+ /** The styled [Y/n] confirm — Enter accepts the default, answer echoed. */
28
+ confirm(question: string, defaultYes?: boolean): Promise<boolean>;
29
+ /** The styled select — arrows move, Enter accepts, number keys pick
30
+ directly; collapses to the chosen answer. Number keys cover options
31
+ 1-9 only: keep lists at nine options or fewer (a longer list stays
32
+ arrow-navigable, but two-digit entry is deliberately not built). */
33
+ select(question: string, options: SelectOption[], defaultIndex?: number): Promise<string>;
34
+ /** The `└ Done in Xs` footer (red `Failed` when init exits non-zero). */
35
+ done(durationMs: number, ok: boolean): void;
36
+ }
37
+ /** The plain-terminal select for non-pretty interactive runs: numbered list +
38
+ readline. Non-TTY runs never prompt — the default option stands; an
39
+ empty, garbage, or out-of-range answer also settles on the default.
40
+ Streams are injectable for tests only; call sites use the defaults. */
41
+ export declare function plainSelect(question: string, options: SelectOption[], defaultIndex?: number, input?: NodeJS.ReadableStream & {
42
+ isTTY?: boolean;
43
+ }, output?: NodeJS.WritableStream & {
44
+ isTTY?: boolean;
45
+ }): Promise<string>;
46
+ export declare function createPrettyOutput(write?: (chunk: string) => void, input?: SelectInput, promptOutput?: NodeJS.WritableStream & {
47
+ isTTY?: boolean;
48
+ }): PrettyOutput;
@@ -0,0 +1,330 @@
1
+ import { stdin, stdout } from "node:process";
2
+ import { createInterface } from "node:readline/promises";
3
+ /**
4
+ * The vendo CLI's TTY visual system (init first; doctor/sync can adopt the
5
+ * same primitives later). Clack-style vertical-bar layout: one `┌ vendo init`
6
+ * header, `◇`/`◆` section markers on a dim `│` rail, colored diff markers,
7
+ * a dots spinner for the slow phases, and ONE deliberately emphasized block —
8
+ * Vendo Cloud — in the brand accent (blue/cyan family, calm not neon).
9
+ *
10
+ * Degradation contract: this module is only selected when stdout is a real
11
+ * TTY and none of NO_COLOR / CI / TERM=dumb opt out (see usePrettyOutput).
12
+ * Every other run — tests, pipes, CI — keeps today's exact plain strings,
13
+ * because runInit's emissions are unchanged: this is a renderer over the
14
+ * existing Output seam, not a second copy of the copy.
15
+ */
16
+ const ESC = "\u001b";
17
+ const style = (open, close) => (text) => `${ESC}[${open}m${text}${ESC}[${close}m`;
18
+ const bold = style("1", "22");
19
+ const dim = style("2", "22");
20
+ const red = style("31", "39");
21
+ const green = style("32", "39");
22
+ const yellow = style("33", "39");
23
+ const blue = style("34", "39");
24
+ const cyan = style("36", "39");
25
+ const brightCyan = style("96", "39");
26
+ /** TTY + no opt-outs → the pretty renderer; anything else keeps plain output.
27
+ NO_COLOR and CI follow the "present and non-empty" convention. */
28
+ export function usePrettyOutput(stream = stdout, env = process.env) {
29
+ if (stream.isTTY !== true)
30
+ return false;
31
+ if ((env.NO_COLOR ?? "") !== "")
32
+ return false;
33
+ if ((env.CI ?? "") !== "")
34
+ return false;
35
+ if (env.TERM === "dumb")
36
+ return false;
37
+ return true;
38
+ }
39
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
40
+ const BAR = dim("│");
41
+ const CLEAR_LINE = `\r${ESC}[2K`;
42
+ /** Inline `code spans` in the calm command color. */
43
+ function styleInline(text) {
44
+ return text.replace(/`([^`]+)`/g, (_match, code) => bold(cyan(code)));
45
+ }
46
+ /** The plain-terminal select for non-pretty interactive runs: numbered list +
47
+ readline. Non-TTY runs never prompt — the default option stands; an
48
+ empty, garbage, or out-of-range answer also settles on the default.
49
+ Streams are injectable for tests only; call sites use the defaults. */
50
+ export async function plainSelect(question, options, defaultIndex = 0, input = stdin, output = stdout) {
51
+ const fallback = (options[defaultIndex] ?? options[0]).value;
52
+ if (input.isTTY !== true || output.isTTY !== true)
53
+ return fallback;
54
+ output.write(`${question}\n`);
55
+ options.forEach((option, index) => {
56
+ output.write(` ${index + 1}. ${option.label}${option.hint === undefined ? "" : ` (${option.hint})`}\n`);
57
+ });
58
+ const prompt = createInterface({ input, output });
59
+ try {
60
+ const answer = (await prompt.question(`Choose [${defaultIndex + 1}]: `)).trim();
61
+ const number = /^\d+$/.test(answer) ? Number(answer) : NaN;
62
+ if (Number.isInteger(number) && number >= 1 && number <= options.length) {
63
+ return options[number - 1].value;
64
+ }
65
+ return fallback;
66
+ }
67
+ finally {
68
+ prompt.close();
69
+ }
70
+ }
71
+ export function createPrettyOutput(write = (chunk) => { stdout.write(chunk); }, input = stdin, promptOutput = stdout) {
72
+ let headerPrinted = false;
73
+ let lastWasBar = false;
74
+ let timer = null;
75
+ let frame = 0;
76
+ const line = (text) => {
77
+ write(`${text}\n`);
78
+ lastWasBar = text === BAR;
79
+ };
80
+ const bar = () => {
81
+ if (!lastWasBar)
82
+ line(BAR);
83
+ };
84
+ const ensureHeader = () => {
85
+ if (headerPrinted)
86
+ return;
87
+ headerPrinted = true;
88
+ line(`${dim("┌")} ${bold("vendo init")}`);
89
+ line(BAR);
90
+ };
91
+ const body = (text) => line(`${BAR} ${styleInline(text)}`);
92
+ const section = (marker, title) => {
93
+ bar();
94
+ line(`${marker} ${title}`);
95
+ };
96
+ const clearFrame = () => {
97
+ if (timer !== null)
98
+ write(CLEAR_LINE);
99
+ };
100
+ const stopSpin = () => {
101
+ if (timer === null)
102
+ return;
103
+ clearInterval(timer);
104
+ timer = null;
105
+ write(CLEAR_LINE);
106
+ };
107
+ /** The emphasized block: brand-blue header + ✦ bullets. */
108
+ const cloudHeader = () => section(blue("◆"), bold(blue("Vendo Cloud")));
109
+ const cloudBullet = (text) => body(`${blue("✦")} ${blue(text)}`);
110
+ const render = (raw) => {
111
+ if (raw === "") {
112
+ bar();
113
+ return;
114
+ }
115
+ const wired = raw.match(/^(Wired \(\d+ files?\)):$/);
116
+ if (wired !== null) {
117
+ section(cyan("◆"), bold(wired[1]));
118
+ return;
119
+ }
120
+ if (raw === "Already wired — nothing to change.") {
121
+ section(cyan("◇"), `${bold("Already wired")} — nothing to change`);
122
+ return;
123
+ }
124
+ const marker = raw.match(/^ {2}([+~]) (.+)$/);
125
+ if (marker !== null) {
126
+ body(`${marker[1] === "+" ? green("+") : yellow("~")} ${dim(cyan(marker[2]))}`);
127
+ return;
128
+ }
129
+ const theme = raw.match(/^Theme: (.*)$/);
130
+ if (theme !== null) {
131
+ section(cyan("◇"), bold("Theme captured"));
132
+ body(theme[1]);
133
+ return;
134
+ }
135
+ if (raw.startsWith("Theme lives in ")) {
136
+ body(dim(raw));
137
+ return;
138
+ }
139
+ const cloudAbsent = raw.match(/^Vendo Cloud \(optional\): not configured\. A key unlocks (.+)\.$/);
140
+ if (cloudAbsent !== null) {
141
+ cloudHeader();
142
+ for (const bullet of cloudAbsent[1].split("; "))
143
+ cloudBullet(bullet);
144
+ return;
145
+ }
146
+ const cloud = raw.match(/^Vendo Cloud: (.+)$/);
147
+ if (cloud !== null) {
148
+ cloudHeader();
149
+ cloudBullet(cloud[1]);
150
+ return;
151
+ }
152
+ if (/`?vendo (cloud )?login`?/.test(raw)) {
153
+ // The CTA: bright, factual, pointing at the free dev-mode key.
154
+ body(`${bold(brightCyan("→"))} ${raw.replace(/`?vendo (cloud )?login`?/g, (m) => bold(brightCyan(m.replaceAll("`", ""))))}`);
155
+ return;
156
+ }
157
+ if (raw === "Last steps are yours:") {
158
+ section(cyan("◇"), bold("Last steps are yours"));
159
+ return;
160
+ }
161
+ // Generic indented detail (paste steps, progress lines): the plain
162
+ // two-space indent becomes the rail; deeper nesting is preserved.
163
+ const indented = raw.match(/^ {2}(.*)$/);
164
+ if (indented !== null) {
165
+ body(indented[1]);
166
+ return;
167
+ }
168
+ body(raw);
169
+ };
170
+ return {
171
+ log(message) {
172
+ clearFrame();
173
+ ensureHeader();
174
+ if (message.startsWith("\n"))
175
+ bar();
176
+ for (const raw of message.replace(/^\n+/, "").split("\n"))
177
+ render(raw);
178
+ },
179
+ error(message) {
180
+ clearFrame();
181
+ ensureHeader();
182
+ if (message.startsWith("\n"))
183
+ bar();
184
+ for (const raw of message.replace(/^\n+/, "").split("\n")) {
185
+ const warning = raw.match(/^\s*warning: (.*)$/);
186
+ if (warning !== null)
187
+ body(yellow(`⚠ ${warning[1]}`));
188
+ else if (raw.startsWith("Vendo Cloud: ")) {
189
+ cloudHeader();
190
+ body(yellow(`⚠ ${raw.slice("Vendo Cloud: ".length)}`));
191
+ }
192
+ else
193
+ body(red(`✖ ${raw}`));
194
+ }
195
+ },
196
+ spin(label) {
197
+ stopSpin();
198
+ ensureHeader();
199
+ const draw = () => {
200
+ frame = (frame + 1) % FRAMES.length;
201
+ write(`${CLEAR_LINE}${cyan(FRAMES[frame])} ${dim(label)}`);
202
+ };
203
+ timer = setInterval(draw, 80);
204
+ timer.unref?.();
205
+ draw();
206
+ },
207
+ stopSpin,
208
+ async confirm(question, defaultYes = false) {
209
+ // usePrettyOutput gates on stdout only; a piped/closed stdin can still
210
+ // reach here (vendo init < file). Never block readline on a non-TTY —
211
+ // the default stands, mirroring the plain askYesNo guard.
212
+ if (input.isTTY !== true)
213
+ return defaultYes;
214
+ stopSpin();
215
+ ensureHeader();
216
+ section(cyan("◇"), bold(question));
217
+ // SelectInput is the raw-key slice of the same real stream readline
218
+ // needs; the default (stdin) satisfies both.
219
+ const prompt = createInterface({
220
+ input: input,
221
+ output: promptOutput,
222
+ });
223
+ try {
224
+ const answer = (await prompt.question(`${BAR} ${dim(defaultYes ? "Y/n" : "y/N")} ${dim("›")} `)).trim().toLowerCase();
225
+ const accepted = answer === "" ? defaultYes : ["y", "yes"].includes(answer);
226
+ line(`${BAR} ${cyan("●")} ${accepted ? "Yes" : "No"}`);
227
+ return accepted;
228
+ }
229
+ finally {
230
+ prompt.close();
231
+ }
232
+ },
233
+ async select(question, options, defaultIndex = 0) {
234
+ // Same stdin guard as confirm: no keypress source → the default option.
235
+ if (input.isTTY !== true)
236
+ return (options[defaultIndex] ?? options[0]).value;
237
+ stopSpin();
238
+ ensureHeader();
239
+ section(cyan("◇"), bold(question));
240
+ let index = defaultIndex;
241
+ const optionLine = (option, at) => {
242
+ const marker = at === index ? cyan("●") : dim("○");
243
+ const label = at === index ? option.label : dim(option.label);
244
+ const hint = option.hint === undefined ? "" : ` ${dim(`(${option.hint})`)}`;
245
+ return `${BAR} ${marker} ${label}${hint}`;
246
+ };
247
+ for (const [at, option] of options.entries())
248
+ line(optionLine(option, at));
249
+ const redraw = () => {
250
+ write(`${ESC}[${options.length}A`);
251
+ for (const [at, option] of options.entries())
252
+ write(`${ESC}[2K${optionLine(option, at)}\n`);
253
+ };
254
+ const chosen = await new Promise((resolveChoice) => {
255
+ const cleanup = () => {
256
+ input.off("data", onData);
257
+ input.setRawMode?.(false);
258
+ input.pause?.();
259
+ };
260
+ // Raw input arrives as arbitrary chunks - a paste ("2\r"), fast
261
+ // typing, or an escape sequence split across reads. Buffer and
262
+ // consume COMPLETE key sequences, handling several keys per chunk;
263
+ // an incomplete escape sequence waits for the next chunk.
264
+ let pending = "";
265
+ const move = (delta) => {
266
+ index = (index + options.length + delta) % options.length;
267
+ redraw();
268
+ };
269
+ const onData = (chunk) => {
270
+ pending += String(chunk);
271
+ while (pending.length > 0) {
272
+ // A full CSI (ESC [ ... final byte) or SS3 (ESC O A-D) sequence.
273
+ const sequence = /^\u001b(?:\[[0-9;]*[@-~]|O[A-D])/.exec(pending)?.[0];
274
+ if (sequence !== undefined) {
275
+ pending = pending.slice(sequence.length);
276
+ const final = sequence[sequence.length - 1];
277
+ if (final === "A" || final === "D")
278
+ move(-1);
279
+ else if (final === "B" || final === "C")
280
+ move(1);
281
+ continue;
282
+ }
283
+ if (pending.startsWith(ESC)) {
284
+ // A prefix of a sequence still in flight waits for more bytes;
285
+ // any other escape is dropped.
286
+ if (/^\u001b(?:\[[0-9;]*|O)?$/.test(pending))
287
+ return;
288
+ pending = pending.slice(1);
289
+ continue;
290
+ }
291
+ const key = pending[0];
292
+ pending = pending.slice(1);
293
+ if (key === "\u0003") { // Ctrl+C
294
+ cleanup();
295
+ write("\n");
296
+ process.exit(130);
297
+ }
298
+ if (key === "\r" || key === "\n") {
299
+ cleanup();
300
+ resolveChoice(index);
301
+ return;
302
+ }
303
+ // Number keys pick directly (the arrows-free fallback).
304
+ if (/^[1-9]$/.test(key) && Number(key) <= options.length) {
305
+ index = Number(key) - 1;
306
+ cleanup();
307
+ resolveChoice(index);
308
+ return;
309
+ }
310
+ // Other printable bytes are ignored.
311
+ }
312
+ };
313
+ input.setRawMode?.(true);
314
+ input.resume?.();
315
+ input.on("data", onData);
316
+ });
317
+ // Collapse the option list to the chosen answer.
318
+ write(`${ESC}[${options.length}A${ESC}[0J`);
319
+ line(`${BAR} ${cyan("●")} ${options[chosen].label}`);
320
+ return options[chosen].value;
321
+ },
322
+ done(durationMs, ok) {
323
+ stopSpin();
324
+ ensureHeader();
325
+ bar();
326
+ const seconds = `${(durationMs / 1000).toFixed(1)}s`;
327
+ line(`${dim("└")} ${ok ? green(`Done in ${seconds}`) : red(`Failed after ${seconds}`)}`);
328
+ },
329
+ };
330
+ }
@@ -0,0 +1,43 @@
1
+ import type { LanguageModel } from "ai";
2
+ import { type DevModelOptions } from "../dev-creds/model.js";
3
+ import { type Output, type TelemetryOptions } from "./shared.js";
4
+ /**
5
+ * `vendo refine` — the CLI surface of the refine engine (ENG-250). Extraction
6
+ * stays a build step (sync); refine is explicitly a COMMAND: it proposes
7
+ * agent-layer artifacts as reviewable diffs and applies them only on approval.
8
+ */
9
+ export interface RefineCommandOptions {
10
+ targetDir: string;
11
+ url?: string;
12
+ modelImport?: string;
13
+ /** Non-interactive interview answers (`--ask`, repeatable). */
14
+ asks?: string[];
15
+ /** Approve every displayed diff without prompting. */
16
+ yes?: boolean;
17
+ output?: Output;
18
+ fetchImpl?: typeof fetch;
19
+ /** Test seam: skip model resolution. */
20
+ model?: LanguageModel;
21
+ confirm?: (change: {
22
+ path: string;
23
+ diff: string;
24
+ }) => Promise<boolean>;
25
+ interview?: (questions: string[]) => Promise<string[]>;
26
+ env?: Record<string, string | undefined>;
27
+ /** Injectable telemetry deps (matches init/doctor). */
28
+ telemetry?: TelemetryOptions;
29
+ }
30
+ /** BYO key through the existing provider-agnostic seam: `--model-import` loads
31
+ * the host's own ai-SDK model module; otherwise the shared dev-credential
32
+ * ladder composes (the same resolver init, doctor, and createVendo ride):
33
+ * provider env keys first, then VENDO_API_KEY via the Cloud model gateway.
34
+ * Resolved EAGERLY so a credential problem fails here, with instructions,
35
+ * instead of mid-run. */
36
+ export declare function resolveRefineModel(options: {
37
+ root: string;
38
+ modelImport?: string;
39
+ env: Record<string, string | undefined>;
40
+ /** Test seam for host-module resolution (mirrors DevModelOptions). */
41
+ importModule?: DevModelOptions["importModule"];
42
+ }): Promise<LanguageModel>;
43
+ export declare function runRefineCommand(options: RefineCommandOptions): Promise<number>;