@vendoai/vendo 0.4.0 → 0.4.2

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.
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { join } from "node:path";
3
- import { option, positionals } from "./args.js";
3
+ import { option } from "./args.js";
4
4
  import { isVendoKey, resolveCloudBaseUrl } from "./client.js";
5
5
  import { errorMessage, printJson } from "./output.js";
6
6
  import { deletePendingClaim, readPendingClaim, writePendingClaim } from "./pending-claim.js";
@@ -91,13 +91,16 @@ export async function runDeviceLogin(args, options = {}) {
91
91
  const waitSeconds = waitRaw !== undefined && /^\d+$/.test(waitRaw) ? Number(waitRaw) : undefined;
92
92
  const waitMs = waitSeconds === undefined ? undefined : waitSeconds * 1000;
93
93
  const pendingHome = options.home === undefined ? {} : { home: options.home };
94
+ const claimCwd = options.root ?? process.cwd();
94
95
  try {
95
96
  // A still-open claim from a dead process (#479): resume polling it so the
96
97
  // human's late approval — against the code they were already shown —
97
- // still lands the key. An expired or unreadable file is discarded (a
98
- // fresh ceremony overwrites it below).
99
- const pending = await readPendingClaim(pendingHome);
100
- const resume = pending !== null && pending.api_url === base && pending.expires_at > now()
98
+ // still lands the key. Claims are scoped per project directory (0.4.2):
99
+ // only THIS cwd's ceremony is ever resumed, so concurrent logins in other
100
+ // repos neither clobber this one nor get resumed by it. An expired or
101
+ // unreadable file is discarded (a fresh ceremony overwrites it below).
102
+ const pending = await readPendingClaim(claimCwd, pendingHome);
103
+ const resume = pending !== null && pending.api_url === base && pending.cwd === claimCwd && pending.expires_at > now()
101
104
  ? pending
102
105
  : null;
103
106
  let claimToken;
@@ -118,9 +121,11 @@ export async function runDeviceLogin(args, options = {}) {
118
121
  verificationUriComplete = resume.verification_uri_complete;
119
122
  }
120
123
  else {
121
- // Optional email hint shown to the human on the approval page.
122
- const email = option(args, "--email") ?? positionals(args, ["--api-url", "--email", "--wait"])[0];
123
- const claim = await postJson(fetchImpl, `${base}${CLAIM_PATH}`, "application/json", JSON.stringify(email === undefined ? {} : { login_hint: email }));
124
+ // No identity hint by design: the human signs in as whoever they choose
125
+ // on the approval page. A guessed login_hint (git email, positional arg)
126
+ // is an assumption that mis-attributes the claim, so the ceremony never
127
+ // sends one.
128
+ const claim = await postJson(fetchImpl, `${base}${CLAIM_PATH}`, "application/json", "{}");
124
129
  if (claim.status !== 200) {
125
130
  const envelope = claim.body;
126
131
  throw new Error(typeof envelope?.error?.message === "string"
@@ -172,7 +177,7 @@ export async function runDeviceLogin(args, options = {}) {
172
177
  throw new Error("Vendo Cloud returned an invalid credential");
173
178
  }
174
179
  await upsertEnvLocal(root, "VENDO_API_KEY", key);
175
- await deletePendingClaim(pendingHome);
180
+ await deletePendingClaim(claimCwd, pendingHome);
176
181
  // Never print the key itself — .env.local is the hand-off, last4 the
177
182
  // receipt. A resumed run names the full path: it may differ from cwd.
178
183
  output.log(`Approved — wrote VENDO_API_KEY (…${key.slice(-4)}) to ${resume !== null ? join(root, ".env.local") : ".env.local"}.`);
@@ -188,11 +193,11 @@ export async function runDeviceLogin(args, options = {}) {
188
193
  if (error === "slow_down")
189
194
  return "slow_down"; // RFC 8628 §3.5
190
195
  if (error === "expired_token") {
191
- await deletePendingClaim(pendingHome);
196
+ await deletePendingClaim(claimCwd, pendingHome);
192
197
  throw new Error("The code expired before it was approved; run `vendo login` again.");
193
198
  }
194
199
  if (error === "access_denied") {
195
- await deletePendingClaim(pendingHome);
200
+ await deletePendingClaim(claimCwd, pendingHome);
196
201
  throw new Error("Your human denied the request — no key was minted.");
197
202
  }
198
203
  // Any other token error is terminal for THIS claim (invalid_grant =
@@ -200,7 +205,7 @@ export async function runDeviceLogin(args, options = {}) {
200
205
  // again). Delete the pending file so the next `vendo login` opens a
201
206
  // fresh claim instead of resuming into the same error forever — the
202
207
  // exact trap a live install hit.
203
- await deletePendingClaim(pendingHome);
208
+ await deletePendingClaim(claimCwd, pendingHome);
204
209
  const description = poll.body?.error_description;
205
210
  throw new Error(typeof description === "string"
206
211
  ? description
@@ -228,7 +233,7 @@ export async function runDeviceLogin(args, options = {}) {
228
233
  if (result === "slow_down")
229
234
  intervalMs += 5000;
230
235
  }
231
- await deletePendingClaim(pendingHome);
236
+ await deletePendingClaim(claimCwd, pendingHome);
232
237
  throw new Error("The code expired before it was approved; run `vendo login` again.");
233
238
  }
234
239
  // Bounded budget: poll immediately, then pace by interval, stopping at
@@ -241,7 +246,7 @@ export async function runDeviceLogin(args, options = {}) {
241
246
  if (result === "slow_down")
242
247
  intervalMs += 5000;
243
248
  if (now() >= deadline) {
244
- await deletePendingClaim(pendingHome);
249
+ await deletePendingClaim(claimCwd, pendingHome);
245
250
  throw new Error("The code expired before it was approved; run `vendo login` again.");
246
251
  }
247
252
  if (now() >= pollDeadline)
@@ -23,7 +23,10 @@ export interface PendingClaimOptions {
23
23
  home?: string;
24
24
  }
25
25
  /** An unreadable or malformed file reads as "no pending claim" — the caller
26
- discards it by opening (and persisting) a fresh ceremony. */
27
- export declare function readPendingClaim(options?: PendingClaimOptions): Promise<PendingClaim | null>;
26
+ discards it by opening (and persisting) a fresh ceremony. Only a claim
27
+ opened for THIS cwd is ever returned. A pre-0.4.2 machine-global file is
28
+ honored (and migrated) only when its recorded cwd matches; someone else's
29
+ ceremony is never resumed. */
30
+ export declare function readPendingClaim(cwd: string, options?: PendingClaimOptions): Promise<PendingClaim | null>;
28
31
  export declare function writePendingClaim(claim: PendingClaim, options?: PendingClaimOptions): Promise<void>;
29
- export declare function deletePendingClaim(options?: PendingClaimOptions): Promise<void>;
32
+ export declare function deletePendingClaim(cwd: string, options?: PendingClaimOptions): Promise<void>;
@@ -1,7 +1,17 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
3
  import { homedir } from "node:os";
3
4
  import { join } from "node:path";
4
- function pendingClaimPath(options = {}) {
5
+ /** Claims are keyed by the project directory they were opened for. A single
6
+ machine-global file let two concurrent `vendo login` runs clobber each
7
+ other, and let project A resume (and receive the key for) project B's
8
+ ceremony — found by the 0.4.1 E2E certification campaign. */
9
+ function pendingClaimPath(cwd, options = {}) {
10
+ const name = `${createHash("sha256").update(cwd).digest("hex").slice(0, 16)}.json`;
11
+ return join(options.home ?? homedir(), ".vendo", "pending-claims", name);
12
+ }
13
+ /** The pre-0.4.2 machine-global location — read once for migration. */
14
+ function legacyPendingClaimPath(options = {}) {
5
15
  return join(options.home ?? homedir(), ".vendo", "pending-claim.json");
6
16
  }
7
17
  function isPendingClaim(value) {
@@ -16,23 +26,37 @@ function isPendingClaim(value) {
16
26
  && typeof claim.api_url === "string"
17
27
  && typeof claim.cwd === "string";
18
28
  }
19
- /** An unreadable or malformed file reads as "no pending claim" — the caller
20
- discards it by opening (and persisting) a fresh ceremony. */
21
- export async function readPendingClaim(options = {}) {
29
+ async function readClaimFile(path) {
22
30
  try {
23
- const value = JSON.parse(await readFile(pendingClaimPath(options), "utf8"));
31
+ const value = JSON.parse(await readFile(path, "utf8"));
24
32
  return isPendingClaim(value) ? value : null;
25
33
  }
26
34
  catch {
27
35
  return null;
28
36
  }
29
37
  }
38
+ /** An unreadable or malformed file reads as "no pending claim" — the caller
39
+ discards it by opening (and persisting) a fresh ceremony. Only a claim
40
+ opened for THIS cwd is ever returned. A pre-0.4.2 machine-global file is
41
+ honored (and migrated) only when its recorded cwd matches; someone else's
42
+ ceremony is never resumed. */
43
+ export async function readPendingClaim(cwd, options = {}) {
44
+ const scoped = await readClaimFile(pendingClaimPath(cwd, options));
45
+ if (scoped !== null)
46
+ return scoped.cwd === cwd ? scoped : null;
47
+ const legacy = await readClaimFile(legacyPendingClaimPath(options));
48
+ if (legacy === null || legacy.cwd !== cwd)
49
+ return null;
50
+ await writePendingClaim(legacy, options);
51
+ await rm(legacyPendingClaimPath(options), { force: true });
52
+ return legacy;
53
+ }
30
54
  export async function writePendingClaim(claim, options = {}) {
31
- const path = pendingClaimPath(options);
55
+ const path = pendingClaimPath(claim.cwd, options);
32
56
  await mkdir(join(path, ".."), { recursive: true, mode: 0o700 });
33
57
  await writeFile(path, `${JSON.stringify(claim, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
34
58
  await chmod(path, 0o600);
35
59
  }
36
- export async function deletePendingClaim(options = {}) {
37
- await rm(pendingClaimPath(options), { force: true });
60
+ export async function deletePendingClaim(cwd, options = {}) {
61
+ await rm(pendingClaimPath(cwd, options), { force: true });
38
62
  }
@@ -4,6 +4,7 @@ import { type AiExtractionOptions } from "./extract/extraction.js";
4
4
  import { type DevCredential } from "../dev-creds/resolve.js";
5
5
  import { type HostFramework } from "./framework.js";
6
6
  import { type AuthPresetName } from "./init-auth.js";
7
+ import { type InstallRunner } from "./provider-deps.js";
7
8
  import { type SelectOption } from "./pretty.js";
8
9
  import { type ThemeSummary } from "./theme/extract-theme.js";
9
10
  import { type Output } from "./shared.js";
@@ -76,6 +77,8 @@ export interface InitOptions {
76
77
  resolveCredential?: (options: {
77
78
  env: Record<string, string | undefined>;
78
79
  }) => Promise<DevCredential>;
80
+ /** Test seam: the provider-dependency install subprocess (provider-deps.ts). */
81
+ installProvider?: InstallRunner;
79
82
  /** Test seam (ENG-339): cloud-in-init step overrides. */
80
83
  cloud?: Partial<Omit<CloudStepOptions, "root" | "output" | "yes" | "credential">>;
81
84
  /** Test seam: AI extraction step overrides (harnesses, consent). */
package/dist/cli/init.js CHANGED
@@ -14,6 +14,7 @@ import { BRIEF_TEMPLATE } from "./extract/stages.js";
14
14
  import { ENV_KEY_VARS, resolveDevCredential, describeDevCredential } from "../dev-creds/resolve.js";
15
15
  import { detectFramework, detectVendoWiring } from "./framework.js";
16
16
  import { resolveScaffoldAuth } from "./init-auth.js";
17
+ import { ensureProviderDeps } from "./provider-deps.js";
17
18
  import { expressServerSource, registrySource, routeSource, serverActionsModuleSource, VENDO_ENV_EXAMPLE, wiringServerActions, } from "./init-scaffolds.js";
18
19
  import { createPrettyOutput, plainSelect, usePrettyOutput } from "./pretty.js";
19
20
  import { contrastingText } from "./theme/color.js";
@@ -927,6 +928,16 @@ export async function runInit(options) {
927
928
  wiringMs,
928
929
  ...(await cloudProjectProps(root)),
929
930
  });
931
+ // The credential's runtime provider must be resolvable from the host or
932
+ // the FIRST turn 500s (dev-creds/model.ts loads it host-side; nothing
933
+ // declares @ai-sdk/* — 0.4.1 E2E cert finding). Install exactly what the
934
+ // resolved credential needs; a failure degrades to the manual command.
935
+ await ensureProviderDeps({
936
+ root,
937
+ credential,
938
+ output,
939
+ ...(options.installProvider === undefined ? {} : { run: options.installProvider }),
940
+ });
930
941
  // The one short Cloud reminder in the end-of-run summary — ONLY while no
931
942
  // key exists (the full emphasized block already ran up top; no repeat).
932
943
  if (credential.rung === "none") {
@@ -0,0 +1,26 @@
1
+ import type { DevCredential } from "../dev-creds/resolve.js";
2
+ import type { Output } from "./shared.js";
3
+ /** Which provider module the resolved credential will load at runtime.
4
+ The Vendo Cloud gateway speaks the Anthropic-compatible /messages API
5
+ through the host-installed @ai-sdk/anthropic (dev-creds/model.ts). */
6
+ export declare function providerModuleFor(credential: DevCredential): {
7
+ module: string;
8
+ spec: string;
9
+ } | null;
10
+ /** Lockfile-sniffed installer; npm is the fallback. */
11
+ export declare function installCommandFor(root: string): Promise<{
12
+ command: string;
13
+ args: string[];
14
+ }>;
15
+ /** Test seam: resolves to the child's exit code (null on spawn error). */
16
+ export type InstallRunner = (command: string, args: string[], cwd: string) => Promise<number | null>;
17
+ export interface EnsureProviderDepsOptions {
18
+ root: string;
19
+ credential: DevCredential;
20
+ output: Output;
21
+ run?: InstallRunner;
22
+ }
23
+ /** Installs `ai@^6` + the credential's provider when the host can't resolve
24
+ them. Never fatal: a failed install degrades to the exact manual command
25
+ (the same one doctor's E-DEP-001 story names). */
26
+ export declare function ensureProviderDeps(options: EnsureProviderDepsOptions): Promise<void>;
@@ -0,0 +1,100 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ /**
5
+ * The starter model ladder resolves its provider from the HOST's
6
+ * node_modules at runtime (dev-creds/model.ts) — nothing declares
7
+ * `@ai-sdk/*` as a dependency, so a fresh install 500s on the first turn
8
+ * until the user reads the dev-server error and installs it by hand (0.4.1
9
+ * E2E certification finding). Init knows the resolved credential, so it can
10
+ * install exactly the provider that credential needs, up front.
11
+ */
12
+ const PROVIDER_SPECS = {
13
+ anthropic: { module: "@ai-sdk/anthropic", spec: "@ai-sdk/anthropic@^3" },
14
+ openai: { module: "@ai-sdk/openai", spec: "@ai-sdk/openai@^3" },
15
+ google: { module: "@ai-sdk/google", spec: "@ai-sdk/google@^3" },
16
+ };
17
+ const AI_SPEC = "ai@^6";
18
+ /** Which provider module the resolved credential will load at runtime.
19
+ The Vendo Cloud gateway speaks the Anthropic-compatible /messages API
20
+ through the host-installed @ai-sdk/anthropic (dev-creds/model.ts). */
21
+ export function providerModuleFor(credential) {
22
+ if (credential.rung === "env-key")
23
+ return PROVIDER_SPECS[credential.provider];
24
+ if (credential.rung === "vendo-cloud")
25
+ return PROVIDER_SPECS.anthropic;
26
+ return null;
27
+ }
28
+ /** Resolvability is what the runtime ladder checks, so node_modules is the
29
+ evidence — not package.json (a hoisting monorepo satisfies the import
30
+ without a local entry). */
31
+ async function isInstalled(root, moduleName) {
32
+ try {
33
+ await readFile(join(root, "node_modules", ...moduleName.split("/"), "package.json"), "utf8");
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ async function fileExists(root, name) {
41
+ try {
42
+ await readFile(join(root, name));
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ /** Lockfile-sniffed installer; npm is the fallback. */
50
+ export async function installCommandFor(root) {
51
+ if (await fileExists(root, "pnpm-lock.yaml"))
52
+ return { command: "pnpm", args: ["add"] };
53
+ if (await fileExists(root, "yarn.lock"))
54
+ return { command: "yarn", args: ["add"] };
55
+ if ((await fileExists(root, "bun.lockb")) || (await fileExists(root, "bun.lock"))) {
56
+ return { command: "bun", args: ["add"] };
57
+ }
58
+ return { command: "npm", args: ["install"] };
59
+ }
60
+ const INSTALL_TIMEOUT_MS = 240_000;
61
+ const defaultRunner = (command, args, cwd) => new Promise((resolve) => {
62
+ const child = spawn(command, args, { cwd, stdio: "ignore" });
63
+ const timer = setTimeout(() => {
64
+ child.kill();
65
+ resolve(null);
66
+ }, INSTALL_TIMEOUT_MS);
67
+ child.on("error", () => {
68
+ clearTimeout(timer);
69
+ resolve(null);
70
+ });
71
+ child.on("exit", (code) => {
72
+ clearTimeout(timer);
73
+ resolve(code);
74
+ });
75
+ });
76
+ /** Installs `ai@^6` + the credential's provider when the host can't resolve
77
+ them. Never fatal: a failed install degrades to the exact manual command
78
+ (the same one doctor's E-DEP-001 story names). */
79
+ export async function ensureProviderDeps(options) {
80
+ const provider = providerModuleFor(options.credential);
81
+ if (provider === null)
82
+ return;
83
+ const specs = [];
84
+ if (!(await isInstalled(options.root, "ai")))
85
+ specs.push(AI_SPEC);
86
+ if (!(await isInstalled(options.root, provider.module)))
87
+ specs.push(provider.spec);
88
+ if (specs.length === 0)
89
+ return;
90
+ const { command, args } = await installCommandFor(options.root);
91
+ const invocation = `${command} ${[...args, ...specs].join(" ")}`;
92
+ options.output.log(`Installing the model provider this credential uses: ${specs.join(" ")} (${command})…`);
93
+ const code = await (options.run ?? defaultRunner)(command, [...args, ...specs], options.root);
94
+ if (code === 0) {
95
+ options.output.log(`Installed ${specs.join(" ")}.`);
96
+ }
97
+ else {
98
+ options.output.error(`warning: could not install ${specs.join(" ")} — run \`${invocation}\` yourself before the first turn, or it fails at runtime (E-DEP-001).`);
99
+ }
100
+ }
@@ -1,5 +1,5 @@
1
1
  import { type Telemetry } from "@vendoai/telemetry";
2
- export declare const CLI_VERSION = "0.4.0";
2
+ export declare const CLI_VERSION = "0.4.2";
3
3
  export interface Output {
4
4
  log(message: string): void;
5
5
  error(message: string): void;
@@ -4,7 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { stdin, stdout } from "node:process";
6
6
  import { initTelemetry, repoHost } from "@vendoai/telemetry";
7
- export const CLI_VERSION = "0.4.0";
7
+ export const CLI_VERSION = "0.4.2";
8
8
  export const consoleOutput = {
9
9
  log: (message) => console.log(message),
10
10
  error: (message) => console.error(message),
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ Usage: vendo <command> [dir] [options]
18
18
 
19
19
  Commands:
20
20
  init [dir] Set up Vendo: wire the handler, extract tools + theme, resolve a model key
21
- login [email] Claim a Vendo Cloud key: approve in the browser; the key lands in .env.local
21
+ login Claim a Vendo Cloud key: approve in the browser; the key lands in .env.local
22
22
  doctor [dir] Verify the install: wiring, live probes, and one real model turn (--json for agents)
23
23
 
24
24
  Advanced:
@@ -38,7 +38,6 @@ Options:
38
38
  --auth <preset> Init only: wire this auth preset without asking (authJs, clerk, supabase, auth0, jwt, none)
39
39
  --framework <name> Init only: override framework detection (next, express) — required non-interactively when detection fails
40
40
  --cloud-key <key> Init only: write this Vendo Cloud key to .env.local instead of the login offer
41
- --email <address> Login only: pre-fill the approval page (login hint)
42
41
  --wait <seconds> Login only: bound this call's polling to N seconds (agents loop re-runs; each resumes the same request), then exit resumably
43
42
  --byo Init only: decline the Vendo Cloud offer (bring your own model key)
44
43
  --ai-polish Init only: consent to the AI extraction pass without a prompt (works non-interactively)
@@ -88,7 +87,7 @@ const REFINE_FLAGS = new Set(["--yes"]);
88
87
  const REFINE_VALUE_OPTIONS = ["--url", "--model-import", "--ask"];
89
88
  const SYNC_FLAGS = new Set(["--strict", "--json", "--report"]);
90
89
  const SYNC_VALUE_OPTIONS = ["--url", "--key", "--api-url"];
91
- const LOGIN_VALUE_OPTIONS = ["--email", "--api-url", "--wait"];
90
+ const LOGIN_VALUE_OPTIONS = ["--api-url", "--wait"];
92
91
  /** ENG-335: options the CLI does not recognize — or value options missing
93
92
  their value — must fail loudly before anything runs. Silently dropping a
94
93
  flag is how the "--agent writes nothing" promise broke in the field: an
@@ -14,7 +14,7 @@ import type { RuntimeCaptureHandler } from "../runtime-capture.js";
14
14
  shares. The anonymous-session + RunContext resolution lives in
15
15
  wire/context.ts; server.ts assembles the table from the per-area modules
16
16
  under src/wire/. */
17
- export declare const VERSION = "0.4.0";
17
+ export declare const VERSION = "0.4.2";
18
18
  export declare const BASE_PATH = "/api/vendo";
19
19
  export type SandboxVenue = "e2b" | "cloud" | "custom" | false;
20
20
  /** How inference is served: "custom" (a host-passed model) or "ladder" (the
@@ -4,7 +4,7 @@ import { VendoError, } from "@vendoai/core";
4
4
  shares. The anonymous-session + RunContext resolution lives in
5
5
  wire/context.ts; server.ts assembles the table from the per-area modules
6
6
  under src/wire/. */
7
- export const VERSION = "0.4.0";
7
+ export const VERSION = "0.4.2";
8
8
  export const BASE_PATH = "/api/vendo";
9
9
  const STATUS_BY_CODE = {
10
10
  validation: 400,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vendoai/vendo",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "The Vendo umbrella: default composition, public wire routes, React provider, and the vendo CLI.",
5
5
  "keywords": [
6
6
  "ai",
@@ -86,16 +86,16 @@
86
86
  "ajv": "^8.20.0",
87
87
  "ajv-formats": "^3.0.1",
88
88
  "zod": "^3.25.0",
89
- "@vendoai/actions": "0.4.0",
90
- "@vendoai/core": "0.4.0",
91
- "@vendoai/apps": "0.4.0",
92
- "@vendoai/guard": "0.4.0",
93
- "@vendoai/mcp": "0.4.0",
94
- "@vendoai/agent": "0.4.0",
95
- "@vendoai/automations": "0.4.0",
96
- "@vendoai/ui": "0.4.0",
97
- "@vendoai/telemetry": "0.3.0",
98
- "@vendoai/store": "0.4.0"
89
+ "@vendoai/actions": "0.4.2",
90
+ "@vendoai/apps": "0.4.2",
91
+ "@vendoai/automations": "0.4.2",
92
+ "@vendoai/agent": "0.4.2",
93
+ "@vendoai/core": "0.4.2",
94
+ "@vendoai/guard": "0.4.2",
95
+ "@vendoai/mcp": "0.4.2",
96
+ "@vendoai/store": "0.4.2",
97
+ "@vendoai/telemetry": "0.3.1",
98
+ "@vendoai/ui": "0.4.2"
99
99
  },
100
100
  "peerDependencies": {
101
101
  "@auth/core": "^0.34.3",
@@ -1,26 +0,0 @@
1
- import type { AppDocument, PermissionGrant } from "@vendoai/core";
2
- import { type VendoStore } from "@vendoai/store";
3
- import { type CloudCommandOptions } from "./command.js";
4
- export interface LocalDeployApp {
5
- doc: AppDocument;
6
- enabled: boolean;
7
- }
8
- export interface LocalDeployProject {
9
- subject: string;
10
- apps: LocalDeployApp[];
11
- grants: PermissionGrant[];
12
- }
13
- export interface LocalProjectReadOptions {
14
- subject?: string;
15
- cwd?: string;
16
- }
17
- export interface ReadLocalProjectOptions extends LocalProjectReadOptions {
18
- storeFactory?: () => VendoStore;
19
- }
20
- export type LocalProjectReader = (options: LocalProjectReadOptions) => Promise<LocalDeployProject>;
21
- export interface CloudDeployOptions extends CloudCommandOptions {
22
- cwd?: string;
23
- localProjectReader?: LocalProjectReader;
24
- }
25
- export declare function readLocalProject(options?: ReadLocalProjectOptions): Promise<LocalDeployProject>;
26
- export declare function runDeploy(args: string[], options?: CloudDeployOptions): Promise<number>;
@@ -1,187 +0,0 @@
1
- import { appStore, createStore, grantStore, } from "@vendoai/store";
2
- import { join } from "node:path";
3
- import { option, options as repeatedOptions } from "./args.js";
4
- import { CloudError, resolveCloudBaseUrl } from "./client.js";
5
- import { commandContext, } from "./command.js";
6
- import { errorMessage, formatTable, printJson } from "./output.js";
7
- export async function readLocalProject(options = {}) {
8
- const store = options.storeFactory?.() ?? createStore({
9
- dataDir: join(options.cwd ?? process.cwd(), ".vendo/data"),
10
- });
11
- try {
12
- await store.ensureSchema();
13
- const driver = store.raw();
14
- const subjectRows = await driver.query("SELECT DISTINCT subject FROM vendo_apps ORDER BY subject ASC");
15
- const subjects = subjectRows.rows.map((row) => row.subject);
16
- if (subjects.length === 0)
17
- throw new Error("No local Vendo apps found in .vendo/data");
18
- let subject = options.subject;
19
- if (subject === undefined) {
20
- if (subjects.length > 1) {
21
- throw new Error(`Multiple local Vendo subjects found: ${subjects.join(", ")}. Pass --subject <subject>.`);
22
- }
23
- subject = subjects[0];
24
- }
25
- else if (!subjects.includes(subject)) {
26
- throw new Error(`Subject ${subject} was not found in the local Vendo store`);
27
- }
28
- const appIds = await driver.query("SELECT id FROM vendo_apps WHERE subject = $1 ORDER BY created_at ASC, id ASC", [subject]);
29
- const apps = (await Promise.all(appIds.rows.map(async ({ id }) => {
30
- const row = await appStore(store).get(id);
31
- if (row === null || row.subject !== subject)
32
- return null;
33
- return { doc: row.doc, enabled: row.enabled };
34
- }))).filter((row) => row !== null);
35
- const grantIds = await driver.query(`SELECT id FROM vendo_grants
36
- WHERE subject = $1 AND source = 'automation'
37
- AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())
38
- ORDER BY granted_at ASC, id ASC`, [subject]);
39
- const grants = (await Promise.all(grantIds.rows.map(({ id }) => grantStore(store).get(id))))
40
- .filter((grant) => grant !== null);
41
- return { subject, apps, grants };
42
- }
43
- finally {
44
- await store.close();
45
- }
46
- }
47
- function machineOptions(args, context) {
48
- return {
49
- auth: "key",
50
- apiKey: option(args, "--key"),
51
- apiUrl: option(args, "--api-url"),
52
- env: context.env,
53
- };
54
- }
55
- function secretValues(args) {
56
- const values = new Map();
57
- for (const pair of repeatedOptions(args, "--secret")) {
58
- const separator = pair.indexOf("=");
59
- if (separator <= 0)
60
- throw new Error("--secret must use NAME=VALUE");
61
- values.set(pair.slice(0, separator), pair.slice(separator + 1));
62
- }
63
- return values;
64
- }
65
- function selectedApps(project, args) {
66
- const requested = [...new Set(repeatedOptions(args, "--app"))];
67
- if (requested.length === 0) {
68
- const selected = project.apps.filter((app) => app.enabled && app.doc.trigger !== undefined);
69
- if (selected.length === 0)
70
- throw new Error("No enabled local automations found");
71
- return selected;
72
- }
73
- const requestedIds = new Set(requested);
74
- for (const appId of requested) {
75
- const app = project.apps.find((candidate) => candidate.doc.id === appId);
76
- if (app === undefined)
77
- throw new Error(`App ${appId} was not found for subject ${project.subject}`);
78
- if (app.doc.trigger === undefined)
79
- throw new Error(`App ${appId} is not an automation`);
80
- }
81
- return project.apps.filter((app) => requestedIds.has(app.doc.id));
82
- }
83
- function warnUnsupportedFnSteps(context, apps) {
84
- for (const app of apps) {
85
- const run = app.doc.trigger?.run;
86
- if (run?.kind === "steps" && run.steps.some((step) => step.tool.startsWith("fn:"))) {
87
- context.output.error(`WARNING: Automation ${app.doc.id} has fn: steps that target the app's machine, which is unreachable from hosted sandboxes in v1; those steps will fail/park when fired hosted.`);
88
- }
89
- }
90
- }
91
- function deployResponse(value) {
92
- if (typeof value !== "object" || value === null)
93
- throw new Error("Vendo Cloud returned an invalid deploy response");
94
- const result = value;
95
- if (typeof result.org?.id !== "string"
96
- || typeof result.org.slug !== "string"
97
- || typeof result.instance?.status !== "string"
98
- || typeof result.applied?.apps !== "number"
99
- || typeof result.applied.grants !== "number"
100
- || typeof result.applied.secrets !== "number"
101
- || !Array.isArray(result.webhooks)
102
- || result.webhooks.some((webhook) => typeof webhook.app_id !== "string"
103
- || typeof webhook.source !== "string"
104
- || typeof webhook.url !== "string")) {
105
- throw new Error("Vendo Cloud returned an invalid deploy response");
106
- }
107
- return result;
108
- }
109
- function printDeploySummary(context, response) {
110
- const lines = [
111
- `Vendo Cloud deploy: ${response.org.slug} (${response.instance.status})`,
112
- "",
113
- formatTable(["APPLIED", "COUNT"], [
114
- ["apps", String(response.applied.apps)],
115
- ["grants", String(response.applied.grants)],
116
- ["secrets", String(response.applied.secrets)],
117
- ]),
118
- "",
119
- ];
120
- if (response.webhooks.length === 0) {
121
- lines.push("Webhooks: none");
122
- }
123
- else {
124
- lines.push(formatTable(["AUTOMATION", "SOURCE", "WEBHOOK URL"], response.webhooks.map((webhook) => [
125
- webhook.app_id,
126
- webhook.source,
127
- webhook.url,
128
- ])));
129
- }
130
- context.output.log(lines.join("\n"));
131
- }
132
- export async function runDeploy(args, options = {}) {
133
- const context = commandContext(options);
134
- try {
135
- const project = await (options.localProjectReader ?? readLocalProject)({
136
- subject: option(args, "--subject"),
137
- cwd: options.cwd ?? process.cwd(),
138
- });
139
- const apps = selectedApps(project, args);
140
- warnUnsupportedFnSteps(context, apps);
141
- const appIds = new Set(apps.map((app) => app.doc.id));
142
- const grants = project.grants.filter((grant) => grant.subject === project.subject
143
- && grant.source === "automation"
144
- && grant.appId !== undefined
145
- && appIds.has(grant.appId));
146
- const provided = secretValues(args);
147
- const referenced = new Set();
148
- for (const app of apps) {
149
- for (const name of app.doc.secrets ?? []) {
150
- referenced.add(name);
151
- if (!provided.has(name)) {
152
- context.output.error(`Automation ${app.doc.id} references missing secret ${name}; pass --secret ${name}=VALUE`);
153
- }
154
- }
155
- }
156
- const secrets = [...referenced]
157
- .filter((name) => provided.has(name))
158
- .map((name) => ({ name, value: provided.get(name) }));
159
- const requestOptions = machineOptions(args, context);
160
- if (secrets.length > 0 && new URL(resolveCloudBaseUrl(requestOptions)).protocol !== "https:") {
161
- throw new Error("Deploying secret values requires an HTTPS Vendo Cloud URL");
162
- }
163
- const response = deployResponse(await context.fetcher("/api/v1/hosted/deploy", {
164
- ...requestOptions,
165
- method: "POST",
166
- body: {
167
- apps: apps.map((app) => ({ doc: app.doc, enabled: app.enabled })),
168
- grants,
169
- secrets,
170
- },
171
- }));
172
- if (args.includes("--json"))
173
- printJson(context.output, response);
174
- else
175
- printDeploySummary(context, response);
176
- return 0;
177
- }
178
- catch (error) {
179
- if (error instanceof CloudError && error.code === "cloud-required") {
180
- context.output.error("This key's org needs a Cloud plan (cloud-required).");
181
- }
182
- else {
183
- context.output.error(errorMessage(error));
184
- }
185
- return 1;
186
- }
187
- }