@spinabot/brigade 1.0.1 → 1.1.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 (62) hide show
  1. package/convex/_generated/api.d.ts +85 -0
  2. package/convex/_generated/api.js +23 -0
  3. package/convex/_generated/dataModel.d.ts +60 -0
  4. package/convex/_generated/server.d.ts +143 -0
  5. package/convex/_generated/server.js +93 -0
  6. package/convex/admin.d.ts +57 -0
  7. package/convex/admin.ts +315 -0
  8. package/convex/auth.d.ts +159 -0
  9. package/convex/auth.ts +217 -0
  10. package/convex/blobs.d.ts +38 -0
  11. package/convex/blobs.ts +115 -0
  12. package/convex/channels.d.ts +150 -0
  13. package/convex/channels.ts +455 -0
  14. package/convex/config.d.ts +67 -0
  15. package/convex/config.ts +168 -0
  16. package/convex/cron.d.ts +237 -0
  17. package/convex/cron.ts +199 -0
  18. package/convex/execApprovals.d.ts +31 -0
  19. package/convex/execApprovals.ts +58 -0
  20. package/convex/extensions.d.ts +30 -0
  21. package/convex/extensions.ts +51 -0
  22. package/convex/health.d.ts +18 -0
  23. package/convex/health.ts +69 -0
  24. package/convex/instance.d.ts +34 -0
  25. package/convex/instance.ts +82 -0
  26. package/convex/logs.d.ts +178 -0
  27. package/convex/logs.ts +253 -0
  28. package/convex/memory.d.ts +354 -0
  29. package/convex/memory.ts +536 -0
  30. package/convex/messages.d.ts +124 -0
  31. package/convex/messages.ts +347 -0
  32. package/convex/org.d.ts +75 -0
  33. package/convex/org.ts +99 -0
  34. package/convex/schema.d.ts +1130 -0
  35. package/convex/schema.ts +847 -0
  36. package/convex/sessions.d.ts +100 -0
  37. package/convex/sessions.ts +105 -0
  38. package/convex/skills.d.ts +73 -0
  39. package/convex/skills.ts +102 -0
  40. package/convex/subagents.d.ts +214 -0
  41. package/convex/subagents.ts +99 -0
  42. package/convex/tsconfig.json +23 -0
  43. package/convex/whatsappAuth.d.ts +52 -0
  44. package/convex/whatsappAuth.ts +151 -0
  45. package/convex/workspace.d.ts +49 -0
  46. package/convex/workspace.ts +106 -0
  47. package/dist/buildstamp.json +1 -1
  48. package/dist/cli/commands/convex-cmd.d.ts +27 -0
  49. package/dist/cli/commands/convex-cmd.d.ts.map +1 -0
  50. package/dist/cli/commands/convex-cmd.js +162 -0
  51. package/dist/cli/commands/convex-cmd.js.map +1 -0
  52. package/dist/cli/program/build-program.d.ts.map +1 -1
  53. package/dist/cli/program/build-program.js +64 -0
  54. package/dist/cli/program/build-program.js.map +1 -1
  55. package/dist/config/paths.d.ts +3 -0
  56. package/dist/config/paths.d.ts.map +1 -1
  57. package/dist/config/paths.js +39 -0
  58. package/dist/config/paths.js.map +1 -1
  59. package/package.json +7 -1
  60. package/scripts/convex-dev.mjs +321 -0
  61. package/scripts/convex-push.mjs +69 -0
  62. package/scripts/install-convex.mjs +123 -0
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env node
2
+ // scripts/convex-dev.mjs
3
+ //
4
+ // Brigade local Convex orchestrator — fully air-gapped, zero cloud touch.
5
+ // Spawns the self-hosted convex-local-backend binary + serves the dashboard
6
+ // on 127.0.0.1:6791. Same path OSS users will use; no Convex account needed.
7
+ //
8
+ // npm run convex:dev
9
+ //
10
+ // Persists state under F:\Brigade\.convex-data\:
11
+ // identity.json — stable instance name + secret (do not commit)
12
+ // admin-key.txt — derived admin key, written each run
13
+ // convex_local_backend.sqlite3 — database file
14
+ // storage/ — Convex File Storage objects
15
+ // logs/ — backend stderr captures
16
+
17
+ import { spawn } from "node:child_process";
18
+ import { mkdirSync, existsSync, writeFileSync, readFileSync } from "node:fs";
19
+ import { randomBytes } from "node:crypto";
20
+ import { createServer } from "node:http";
21
+ import { readFile, stat } from "node:fs/promises";
22
+ import { extname, join, dirname, resolve } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
26
+ const BIN_DIR = join(ROOT, "bin");
27
+ const DATA_DIR = join(ROOT, ".convex-data");
28
+ const DASHBOARD_DIR = join(BIN_DIR, "dashboard");
29
+ const BACKEND_BIN = join(BIN_DIR, process.platform === "win32" ? "convex-local-backend.exe" : "convex-local-backend");
30
+
31
+ const BACKEND_HOST = "127.0.0.1";
32
+ const BACKEND_PORT = 3210;
33
+ const SITE_PROXY_PORT = 3211;
34
+ const DASHBOARD_PORT = 6791;
35
+
36
+ const INSTANCE_NAME = "brigade-local";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // 1. Sanity checks
40
+ // ---------------------------------------------------------------------------
41
+ if (!existsSync(BACKEND_BIN)) {
42
+ console.error(`✖ Backend binary not found: ${BACKEND_BIN}`);
43
+ console.error(` Run \`node scripts/install-convex.mjs\` to download it.`);
44
+ process.exit(1);
45
+ }
46
+ if (!existsSync(DASHBOARD_DIR)) {
47
+ console.error(`✖ Dashboard not found: ${DASHBOARD_DIR}`);
48
+ console.error(` Run \`node scripts/install-convex.mjs\` to download it.`);
49
+ process.exit(1);
50
+ }
51
+
52
+ mkdirSync(DATA_DIR, { recursive: true });
53
+ mkdirSync(join(DATA_DIR, "storage"), { recursive: true });
54
+ mkdirSync(join(DATA_DIR, "logs"), { recursive: true });
55
+
56
+ // Preflight: a second `npm run convex:dev` should say so plainly, not die
57
+ // with an unhandled EADDRINUSE stack trace from deep inside node:net.
58
+ import { createServer as createNetServer } from "node:net";
59
+ function portFree(port) {
60
+ return new Promise((resolve) => {
61
+ const probe = createNetServer();
62
+ probe.once("error", () => resolve(false));
63
+ probe.once("listening", () => probe.close(() => resolve(true)));
64
+ probe.listen(port, BACKEND_HOST);
65
+ });
66
+ }
67
+ for (const [port, what] of [[BACKEND_PORT, "backend"], [SITE_PROXY_PORT, "site proxy"], [DASHBOARD_PORT, "dashboard"]]) {
68
+ if (!(await portFree(port))) {
69
+ console.error(`✖ Port ${port} (${what}) is already in use.`);
70
+ console.error(` Is another \`npm run convex:dev\` already running?`);
71
+ console.error(` If so, its dashboard is at http://${BACKEND_HOST}:${DASHBOARD_PORT} — or stop it and re-run.`);
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // 2. Stable identity — generate once, persist forever
78
+ // ---------------------------------------------------------------------------
79
+ const IDENTITY_FILE = join(DATA_DIR, "identity.json");
80
+ let identity;
81
+ if (existsSync(IDENTITY_FILE)) {
82
+ identity = JSON.parse(readFileSync(IDENTITY_FILE, "utf8"));
83
+ } else {
84
+ identity = {
85
+ instanceName: INSTANCE_NAME,
86
+ instanceSecret: randomBytes(32).toString("hex"),
87
+ createdAt: new Date().toISOString(),
88
+ };
89
+ writeFileSync(IDENTITY_FILE, JSON.stringify(identity, null, 2));
90
+ console.log(`✓ Generated stable instance identity at ${IDENTITY_FILE}`);
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // 3. Derive admin key via `keygen admin-key`
95
+ // ---------------------------------------------------------------------------
96
+ import { execSync } from "node:child_process";
97
+ const adminKey = execSync(
98
+ `"${BACKEND_BIN}" keygen admin-key --instance-name "${identity.instanceName}" --instance-secret "${identity.instanceSecret}"`,
99
+ { encoding: "utf8" }
100
+ ).trim();
101
+ writeFileSync(join(DATA_DIR, "admin-key.txt"), adminKey);
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // 4. Write .env.local for the Convex CLI + Brigade gateway
105
+ // ---------------------------------------------------------------------------
106
+ const envContent = [
107
+ `# Auto-generated by scripts/convex-dev.mjs — do not commit`,
108
+ `CONVEX_SELF_HOSTED_URL=http://${BACKEND_HOST}:${BACKEND_PORT}`,
109
+ `CONVEX_SELF_HOSTED_ADMIN_KEY=${adminKey}`,
110
+ `# Also expose plain CONVEX_URL for client code:`,
111
+ `CONVEX_URL=http://${BACKEND_HOST}:${BACKEND_PORT}`,
112
+ ``,
113
+ ].join("\n");
114
+ writeFileSync(join(ROOT, ".env.local"), envContent);
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // 5. Spawn the backend
118
+ // ---------------------------------------------------------------------------
119
+ const dbPath = join(DATA_DIR, "convex_local_backend.sqlite3");
120
+ const storagePath = join(DATA_DIR, "storage");
121
+
122
+ console.log(`\x1b[36m▌ Starting Convex local backend\x1b[0m`);
123
+ console.log(`\x1b[36m▌ backend → http://${BACKEND_HOST}:${BACKEND_PORT}\x1b[0m`);
124
+ console.log(`\x1b[36m▌ site → http://${BACKEND_HOST}:${SITE_PROXY_PORT}\x1b[0m`);
125
+ console.log(`\x1b[36m▌ db → ${dbPath}\x1b[0m`);
126
+ console.log(`\x1b[36m▌ storage → ${storagePath}\x1b[0m`);
127
+
128
+ const backend = spawn(BACKEND_BIN, [
129
+ "--interface", BACKEND_HOST,
130
+ "--port", String(BACKEND_PORT),
131
+ "--site-proxy-port", String(SITE_PROXY_PORT),
132
+ "--instance-name", identity.instanceName,
133
+ "--instance-secret", identity.instanceSecret,
134
+ "--local-storage", storagePath,
135
+ "--do-not-require-ssl",
136
+ dbPath,
137
+ ], { stdio: ["ignore", "pipe", "pipe"] });
138
+
139
+ backend.stdout.on("data", (b) => process.stdout.write(`\x1b[90m[backend]\x1b[0m ${b}`));
140
+ backend.stderr.on("data", (b) => process.stderr.write(`\x1b[90m[backend]\x1b[0m ${b}`));
141
+ backend.on("exit", (code) => {
142
+ console.error(`\x1b[31m[backend] exited with code ${code}\x1b[0m`);
143
+ shutdown(code ?? 1);
144
+ });
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // 5b. Push the current convex/ functions once the backend answers.
148
+ //
149
+ // The runbook always promised "boots the local backend + pushes convex/
150
+ // functions" — but nothing ever pushed, so the deployed bundle silently
151
+ // drifted from the code: every function added after the last manual push
152
+ // failed at runtime with "Could not find public function 'auth:readAuthFile'"
153
+ // while the gateway limped along half-broken. Pushing here (and via the
154
+ // standalone `npm run convex:push`) keeps backend and code in lockstep; the
155
+ // boot-time bundle-version gate in src/storage/boot.ts is the backstop.
156
+ // ---------------------------------------------------------------------------
157
+ async function pushFunctionsWhenReady() {
158
+ const deadline = Date.now() + 60_000;
159
+ let up = false;
160
+ while (Date.now() < deadline && !shuttingDown) {
161
+ try {
162
+ const res = await fetch(`http://${BACKEND_HOST}:${BACKEND_PORT}/version`);
163
+ if (res.ok) {
164
+ up = true;
165
+ break;
166
+ }
167
+ } catch {
168
+ /* backend still starting */
169
+ }
170
+ await new Promise((r) => setTimeout(r, 500));
171
+ }
172
+ if (!up || shuttingDown) {
173
+ if (!shuttingDown) {
174
+ console.error(
175
+ `\x1b[31m▌ Backend didn't answer within 60s — skipped the function push. Run \`npm run convex:push\` once it's up.\x1b[0m`,
176
+ );
177
+ }
178
+ return;
179
+ }
180
+ console.log(`\x1b[36m▌ Pushing convex/ functions…\x1b[0m`);
181
+ const push = spawn(process.execPath, [join(ROOT, "scripts", "convex-push.mjs")], {
182
+ stdio: "inherit",
183
+ });
184
+ push.on("exit", (code) => {
185
+ if (code === 0) {
186
+ console.log(`\x1b[32m▌ Functions up to date.\x1b[0m`);
187
+ } else {
188
+ console.error(
189
+ `\x1b[31m▌ Function push failed (code ${code}) — run \`npm run convex:push\` manually.\x1b[0m`,
190
+ );
191
+ }
192
+ });
193
+ }
194
+ // NOTE: invoked at the BOTTOM of this file — it reads `shuttingDown`, which
195
+ // is declared in section 7 below; calling it here would hit the TDZ.
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // 6. Spawn the static dashboard server on 127.0.0.1:6791
199
+ // ---------------------------------------------------------------------------
200
+ const MIME = {
201
+ ".html": "text/html; charset=utf-8",
202
+ ".css": "text/css; charset=utf-8",
203
+ ".js": "application/javascript; charset=utf-8",
204
+ ".json": "application/json; charset=utf-8",
205
+ ".svg": "image/svg+xml",
206
+ ".png": "image/png",
207
+ ".ico": "image/x-icon",
208
+ ".webmanifest": "application/manifest+json",
209
+ ".woff": "font/woff",
210
+ ".woff2": "font/woff2",
211
+ };
212
+
213
+ const dashboardServer = createServer(async (req, res) => {
214
+ try {
215
+ let urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
216
+ if (urlPath === "/") urlPath = "/index.html";
217
+
218
+ let filePath = join(DASHBOARD_DIR, urlPath);
219
+
220
+ // SPA fallback — if the requested path has no extension and doesn't exist,
221
+ // try $path.html, then fall back to index.html.
222
+ let st;
223
+ try { st = await stat(filePath); } catch {}
224
+ if (!st || st.isDirectory()) {
225
+ const candidates = [
226
+ filePath + ".html",
227
+ join(filePath, "index.html"),
228
+ join(DASHBOARD_DIR, "index.html"),
229
+ ];
230
+ for (const c of candidates) {
231
+ try {
232
+ const s = await stat(c);
233
+ if (s.isFile()) { filePath = c; st = s; break; }
234
+ } catch {}
235
+ }
236
+ }
237
+ if (!st || !st.isFile()) { res.writeHead(404); res.end("Not Found"); return; }
238
+
239
+ const ext = extname(filePath).toLowerCase();
240
+ const mime = MIME[ext] ?? "application/octet-stream";
241
+ res.writeHead(200, { "content-type": mime, "cache-control": "no-store" });
242
+ // Zero-click login. The dashboard's logged-in flag is in-memory React
243
+ // state — sessionStorage alone never logs it in. The supported hook is
244
+ // a postMessage handshake: on load the app broadcasts
245
+ // "dashboard-credentials-request" to window.parent and auto-logs-in if
246
+ // anything replies with {type:"dashboard-credentials", adminKey,
247
+ // deploymentUrl, deploymentName}. The page is top-level here, so
248
+ // window.parent === window and our injected listener can be that
249
+ // responder. Always answer — "Log Out" is meaningless for a localhost
250
+ // dashboard whose admin key lives in a file on the same machine, and
251
+ // any logged-out heuristic is unreliable anyway (the dashboard's own
252
+ // storage hook writes the same empty marker on first load that Log Out
253
+ // writes). Localhost-only listener; same trust domain as the key file.
254
+ if (ext === ".html") {
255
+ const creds =
256
+ `{type:"dashboard-credentials",adminKey:${JSON.stringify(adminKey)},` +
257
+ `deploymentUrl:${JSON.stringify(`http://${BACKEND_HOST}:${BACKEND_PORT}`)},` +
258
+ `deploymentName:${JSON.stringify(identity.instanceName)}}`;
259
+ const html = (await readFile(filePath, "utf8")).replace(
260
+ /<head>/i,
261
+ `<head><script>(function(){try{` +
262
+ `window.addEventListener("message",function(ev){` +
263
+ `if(ev&&ev.data&&ev.data.type==="dashboard-credentials-request"){` +
264
+ `window.postMessage(${creds},"*");}});` +
265
+ `}catch(e){}})();</script>`,
266
+ );
267
+ res.end(html);
268
+ return;
269
+ }
270
+ res.end(await readFile(filePath));
271
+ } catch (err) {
272
+ res.writeHead(500);
273
+ res.end(String(err));
274
+ }
275
+ });
276
+
277
+ dashboardServer.listen(DASHBOARD_PORT, BACKEND_HOST, () => {
278
+ const dashboardUrl = `http://${BACKEND_HOST}:${DASHBOARD_PORT}`;
279
+ console.log(`\x1b[36m▌ dashboard → ${dashboardUrl} (logs in automatically)\x1b[0m`);
280
+ console.log(`\x1b[90m▌ admin key in .convex-data/admin-key.txt if you ever need it manually\x1b[0m`);
281
+
282
+ // Convenience: open the dashboard in the default browser — it logs in by
283
+ // itself via the credential handshake injected above, so there's nothing
284
+ // to paste. Opt out (CI / headless / "stop opening tabs") with
285
+ // BRIGADE_NO_BROWSER=1.
286
+ if (process.env.BRIGADE_NO_BROWSER !== "1") {
287
+ try {
288
+ if (process.platform === "win32") {
289
+ // `start` needs the empty-title arg.
290
+ spawn("cmd", ["/c", "start", "", dashboardUrl], { stdio: "ignore", detached: true }).unref();
291
+ } else if (process.platform === "darwin") {
292
+ spawn("open", [dashboardUrl], { stdio: "ignore", detached: true }).unref();
293
+ } else {
294
+ spawn("xdg-open", [dashboardUrl], { stdio: "ignore", detached: true }).unref();
295
+ }
296
+ } catch {
297
+ /* best-effort — the printed URL above always works */
298
+ }
299
+ }
300
+ console.log(``);
301
+ });
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // 7. Graceful shutdown
305
+ // ---------------------------------------------------------------------------
306
+ let shuttingDown = false;
307
+ function shutdown(code = 0) {
308
+ if (shuttingDown) return;
309
+ shuttingDown = true;
310
+ console.log(`\n\x1b[36m▌ Stopping...\x1b[0m`);
311
+ try { dashboardServer.close(); } catch {}
312
+ try { backend.kill("SIGTERM"); } catch {}
313
+ setTimeout(() => process.exit(code), 500);
314
+ }
315
+ process.on("SIGINT", () => shutdown(0));
316
+ process.on("SIGTERM", () => shutdown(0));
317
+
318
+ // Kick off the function push LAST — after `shuttingDown` above is
319
+ // initialized (pushFunctionsWhenReady reads it; an earlier call site hit
320
+ // the let-binding TDZ and crashed the whole orchestrator at startup).
321
+ void pushFunctionsWhenReady();
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ // scripts/convex-push.mjs — deploy convex/ functions to the LOCAL backend.
3
+ //
4
+ // npm run convex:push
5
+ //
6
+ // Reads the admin key minted by convex-dev.mjs and runs `convex deploy`
7
+ // against the self-hosted backend (default http://127.0.0.1:3210). Idempotent
8
+ // — run any time the functions or schema change. `npm run convex:dev` also
9
+ // runs this automatically once the backend is up, so the deployed bundle can
10
+ // no longer silently drift from the code (the exact production failure:
11
+ // per-domain "Could not find public function 'auth:readAuthFile'" spam while
12
+ // the gateway limps along half-broken).
13
+
14
+ import { spawnSync } from "node:child_process";
15
+ import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
16
+ import { join, dirname, resolve } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
20
+ const DATA_DIR = join(ROOT, ".convex-data");
21
+
22
+ // Pre-clean: compiled .js/.js.map artifacts INSIDE convex/ make the bundler
23
+ // fail with "Two output files share the same path" (it treats both the .ts
24
+ // and the stray .js as entry points). Historically the convex CLI's own
25
+ // deploy-time typecheck planted them (no convex/tsconfig.json → emitting
26
+ // mode — fixed by the noEmit tsconfig now in that folder), so pushes broke
27
+ // the NEXT push. Sweep them before every deploy as a belt-and-suspenders so
28
+ // no future emitter can re-break deploys. `_generated/` is the CLI's own
29
+ // output and is exempt.
30
+ const convexDir = join(ROOT, "convex");
31
+ let cleaned = 0;
32
+ for (const name of readdirSync(convexDir)) {
33
+ if (name.endsWith(".js") || name.endsWith(".js.map")) {
34
+ rmSync(join(convexDir, name), { force: true });
35
+ cleaned += 1;
36
+ }
37
+ }
38
+ if (cleaned > 0) {
39
+ console.log(`▌ Removed ${cleaned} stray compiled artifact(s) from convex/ (deploy-breaking).`);
40
+ }
41
+ const keyFile = join(DATA_DIR, "admin-key.txt");
42
+ const url = process.env.CONVEX_SELF_HOSTED_URL?.trim() || "http://127.0.0.1:3210";
43
+
44
+ if (!existsSync(keyFile) && !process.env.CONVEX_SELF_HOSTED_ADMIN_KEY) {
45
+ console.error(
46
+ "✖ No admin key found (.convex-data/admin-key.txt). Start the backend once first: npm run convex:dev",
47
+ );
48
+ process.exit(1);
49
+ }
50
+ const adminKey =
51
+ process.env.CONVEX_SELF_HOSTED_ADMIN_KEY?.trim() || readFileSync(keyFile, "utf8").trim();
52
+
53
+ console.log(`▌ Pushing convex/ functions → ${url}`);
54
+ const res = spawnSync("npx", ["convex", "deploy", "--yes"], {
55
+ cwd: ROOT,
56
+ stdio: "inherit",
57
+ shell: true, // resolves npx.cmd on Windows
58
+ env: {
59
+ ...process.env,
60
+ CONVEX_SELF_HOSTED_URL: url,
61
+ CONVEX_SELF_HOSTED_ADMIN_KEY: adminKey,
62
+ },
63
+ });
64
+ if (res.status === 0) {
65
+ console.log("✓ Convex functions are up to date.");
66
+ } else {
67
+ console.error(`✖ convex deploy exited with code ${res.status ?? "unknown"}.`);
68
+ }
69
+ process.exit(res.status ?? 1);
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ // scripts/install-convex.mjs
3
+ //
4
+ // Auto-download the self-hosted Convex backend + dashboard binaries into
5
+ // F:\Brigade\bin\ (or wherever the repo lives). Zero Convex Cloud account
6
+ // needed. Runs as `npm run convex:install` and also fires on first run of
7
+ // `npm run convex:dev` if binaries are missing.
8
+ //
9
+ // What it downloads:
10
+ // convex-local-backend-<platform>.zip (~46 MB)
11
+ // dashboard.zip (~3 MB)
12
+ // LICENSE.md
13
+ //
14
+ // Source: github.com/get-convex/convex-backend releases.
15
+ // License (verified): FSL-1.1-Apache-2.0 — Permitted Purpose for Brigade.
16
+
17
+ import { existsSync, mkdirSync, createWriteStream, readFileSync } from "node:fs";
18
+ import { writeFile, unlink, mkdir } from "node:fs/promises";
19
+ import { spawn } from "node:child_process";
20
+ import { dirname, join, resolve } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { pipeline } from "node:stream/promises";
23
+ import { Readable } from "node:stream";
24
+
25
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
26
+ const BIN_DIR = join(ROOT, "bin");
27
+ const BACKEND_BIN = join(BIN_DIR, process.platform === "win32" ? "convex-local-backend.exe" : "convex-local-backend");
28
+ const DASHBOARD_DIR = join(BIN_DIR, "dashboard");
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Pin to a known-good release. Update this when a new Convex backend is needed.
32
+ // Find latest tags at:
33
+ // https://github.com/get-convex/convex-backend/releases
34
+ // ---------------------------------------------------------------------------
35
+ const RELEASE_TAG = "precompiled-2026-06-03-7eff2e7";
36
+ const RELEASE_BASE = `https://github.com/get-convex/convex-backend/releases/download/${RELEASE_TAG}`;
37
+
38
+ function platformAsset() {
39
+ const p = process.platform;
40
+ const a = process.arch;
41
+ if (p === "win32" && a === "x64") return "convex-local-backend-x86_64-pc-windows-msvc.zip";
42
+ if (p === "darwin" && a === "arm64") return "convex-local-backend-aarch64-apple-darwin.zip";
43
+ if (p === "darwin" && a === "x64") return "convex-local-backend-x86_64-apple-darwin.zip";
44
+ if (p === "linux" && a === "x64") return "convex-local-backend-x86_64-unknown-linux-gnu.zip";
45
+ if (p === "linux" && a === "arm64") return "convex-local-backend-aarch64-unknown-linux-gnu.zip";
46
+ throw new Error(`Unsupported platform ${p}/${a}. Brigade ships Convex backend binaries for: win-x64, mac-x64, mac-arm64, linux-x64, linux-arm64.`);
47
+ }
48
+
49
+ async function download(url, destPath) {
50
+ console.log(` → ${url}`);
51
+ const res = await fetch(url, { redirect: "follow" });
52
+ if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
53
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
54
+ }
55
+
56
+ async function unzip(zipPath, destDir) {
57
+ // Cross-platform unzip via PowerShell on Windows, `unzip` elsewhere.
58
+ await mkdir(destDir, { recursive: true });
59
+ if (process.platform === "win32") {
60
+ await runCmd("powershell", [
61
+ "-NoProfile", "-Command",
62
+ `Expand-Archive -Force -LiteralPath '${zipPath}' -DestinationPath '${destDir}'`,
63
+ ]);
64
+ } else {
65
+ await runCmd("unzip", ["-o", zipPath, "-d", destDir]);
66
+ }
67
+ }
68
+
69
+ function runCmd(cmd, args) {
70
+ return new Promise((resolveProm, rejectProm) => {
71
+ const child = spawn(cmd, args, { stdio: ["ignore", "inherit", "inherit"] });
72
+ child.on("exit", (code) => code === 0 ? resolveProm() : rejectProm(new Error(`${cmd} exited ${code}`)));
73
+ child.on("error", rejectProm);
74
+ });
75
+ }
76
+
77
+ async function main() {
78
+ mkdirSync(BIN_DIR, { recursive: true });
79
+ const asset = platformAsset();
80
+ const backendZip = join(BIN_DIR, "_backend.zip");
81
+ const dashboardZip = join(BIN_DIR, "_dashboard.zip");
82
+ const licensePath = join(BIN_DIR, "LICENSE.md");
83
+
84
+ const haveBackend = existsSync(BACKEND_BIN);
85
+ const haveDashboard = existsSync(DASHBOARD_DIR) && existsSync(join(DASHBOARD_DIR, "index.html"));
86
+ const haveLicense = existsSync(licensePath);
87
+
88
+ if (haveBackend && haveDashboard && haveLicense) {
89
+ console.log(`✓ Convex binaries already present in ${BIN_DIR}`);
90
+ return;
91
+ }
92
+
93
+ console.log(`Installing Convex local backend + dashboard for ${process.platform}/${process.arch}`);
94
+ console.log(` Release: ${RELEASE_TAG}`);
95
+ console.log(` Bin dir: ${BIN_DIR}`);
96
+ console.log();
97
+
98
+ if (!haveBackend) {
99
+ await download(`${RELEASE_BASE}/${asset}`, backendZip);
100
+ await unzip(backendZip, BIN_DIR);
101
+ await unlink(backendZip).catch(() => {});
102
+ }
103
+
104
+ if (!haveDashboard) {
105
+ await download(`${RELEASE_BASE}/dashboard.zip`, dashboardZip);
106
+ await unzip(dashboardZip, DASHBOARD_DIR);
107
+ await unlink(dashboardZip).catch(() => {});
108
+ }
109
+
110
+ if (!haveLicense) {
111
+ await download(`${RELEASE_BASE}/LICENSE.md`, licensePath);
112
+ }
113
+
114
+ console.log();
115
+ console.log(`✓ Installed Convex backend + dashboard.`);
116
+ console.log(` License: FSL-1.1-Apache-2.0 (see bin/LICENSE.md)`);
117
+ console.log(` Next: npm run convex:dev`);
118
+ }
119
+
120
+ main().catch((err) => {
121
+ console.error(`✖ ${err.message}`);
122
+ process.exit(1);
123
+ });