@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.
- package/convex/_generated/api.d.ts +85 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/admin.d.ts +57 -0
- package/convex/admin.ts +315 -0
- package/convex/auth.d.ts +159 -0
- package/convex/auth.ts +217 -0
- package/convex/blobs.d.ts +38 -0
- package/convex/blobs.ts +115 -0
- package/convex/channels.d.ts +150 -0
- package/convex/channels.ts +455 -0
- package/convex/config.d.ts +67 -0
- package/convex/config.ts +168 -0
- package/convex/cron.d.ts +237 -0
- package/convex/cron.ts +199 -0
- package/convex/execApprovals.d.ts +31 -0
- package/convex/execApprovals.ts +58 -0
- package/convex/extensions.d.ts +30 -0
- package/convex/extensions.ts +51 -0
- package/convex/health.d.ts +18 -0
- package/convex/health.ts +69 -0
- package/convex/instance.d.ts +34 -0
- package/convex/instance.ts +82 -0
- package/convex/logs.d.ts +178 -0
- package/convex/logs.ts +253 -0
- package/convex/memory.d.ts +354 -0
- package/convex/memory.ts +536 -0
- package/convex/messages.d.ts +124 -0
- package/convex/messages.ts +347 -0
- package/convex/org.d.ts +75 -0
- package/convex/org.ts +99 -0
- package/convex/schema.d.ts +1130 -0
- package/convex/schema.ts +847 -0
- package/convex/sessions.d.ts +100 -0
- package/convex/sessions.ts +105 -0
- package/convex/skills.d.ts +73 -0
- package/convex/skills.ts +102 -0
- package/convex/subagents.d.ts +214 -0
- package/convex/subagents.ts +99 -0
- package/convex/tsconfig.json +23 -0
- package/convex/whatsappAuth.d.ts +52 -0
- package/convex/whatsappAuth.ts +151 -0
- package/convex/workspace.d.ts +49 -0
- package/convex/workspace.ts +106 -0
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/convex-cmd.d.ts +27 -0
- package/dist/cli/commands/convex-cmd.d.ts.map +1 -0
- package/dist/cli/commands/convex-cmd.js +162 -0
- package/dist/cli/commands/convex-cmd.js.map +1 -0
- package/dist/cli/program/build-program.d.ts.map +1 -1
- package/dist/cli/program/build-program.js +64 -0
- package/dist/cli/program/build-program.js.map +1 -1
- package/dist/config/paths.d.ts +3 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +39 -0
- package/dist/config/paths.js.map +1 -1
- package/package.json +7 -1
- package/scripts/convex-dev.mjs +321 -0
- package/scripts/convex-push.mjs +69 -0
- 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
|
+
});
|