blodemd 0.0.4 → 0.0.5
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/README.md +12 -1
- package/dist/cli.mjs +219 -15
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -31,6 +31,9 @@ Requires Node.js 18+.
|
|
|
31
31
|
# Scaffold a new docs folder
|
|
32
32
|
blodemd init
|
|
33
33
|
|
|
34
|
+
# Preview locally
|
|
35
|
+
blodemd dev
|
|
36
|
+
|
|
34
37
|
# Authenticate
|
|
35
38
|
blodemd login
|
|
36
39
|
|
|
@@ -47,7 +50,7 @@ blodemd logout Remove stored credentials
|
|
|
47
50
|
blodemd whoami Show current authentication
|
|
48
51
|
blodemd validate [dir] Validate docs.json
|
|
49
52
|
blodemd push [dir] Deploy docs
|
|
50
|
-
blodemd dev
|
|
53
|
+
blodemd dev [dir] Start the local docs preview server
|
|
51
54
|
```
|
|
52
55
|
|
|
53
56
|
### `push` Options
|
|
@@ -62,6 +65,14 @@ blodemd dev Show instructions for the local dev server
|
|
|
62
65
|
|
|
63
66
|
The CLI reads the project slug from the `name` field in `docs.json` when `--project` is not set.
|
|
64
67
|
|
|
68
|
+
### `dev` Options
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
--dir <dir> Docs directory
|
|
72
|
+
--port <port> Local preview port (default: 3030)
|
|
73
|
+
--no-open Don't open the browser automatically
|
|
74
|
+
```
|
|
75
|
+
|
|
65
76
|
## CI / GitHub Actions
|
|
66
77
|
|
|
67
78
|
Use the `mblode/blodemd/packages/deploy-action` composite action to deploy on every push:
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import fs, { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import path, { join } from "node:path";
|
|
5
5
|
import { confirm, intro, isCancel, log, password, spinner } from "@clack/prompts";
|
|
@@ -7,6 +7,11 @@ import chalk from "chalk";
|
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import open from "open";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
+
import { once } from "node:events";
|
|
11
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { createFsSource, loadSiteConfig } from "@repo/previewing";
|
|
14
|
+
import { watch } from "chokidar";
|
|
10
15
|
import { createServer } from "node:http";
|
|
11
16
|
import { createHash, randomBytes } from "node:crypto";
|
|
12
17
|
//#region src/constants.ts
|
|
@@ -283,6 +288,207 @@ const resolveTokenStatus = (token) => {
|
|
|
283
288
|
};
|
|
284
289
|
};
|
|
285
290
|
//#endregion
|
|
291
|
+
//#region src/dev/resolve-root.ts
|
|
292
|
+
const CONFIG_FILE$1 = "docs.json";
|
|
293
|
+
const fileExists$2 = async (filePath) => {
|
|
294
|
+
try {
|
|
295
|
+
await fs.access(filePath);
|
|
296
|
+
return true;
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const resolveDocsRoot$1 = async (dir) => {
|
|
302
|
+
if (dir) return path.resolve(process.cwd(), dir);
|
|
303
|
+
const candidates = [
|
|
304
|
+
process.cwd(),
|
|
305
|
+
path.join(process.cwd(), "docs"),
|
|
306
|
+
path.join(process.cwd(), "apps/docs")
|
|
307
|
+
];
|
|
308
|
+
for (const candidate of candidates) if (await fileExists$2(path.join(candidate, CONFIG_FILE$1))) return candidate;
|
|
309
|
+
return process.cwd();
|
|
310
|
+
};
|
|
311
|
+
const validateDocsRoot = async (root) => {
|
|
312
|
+
const result = await loadSiteConfig(createFsSource(root));
|
|
313
|
+
if (!result.ok) throw new CliError(result.errors.join("\n"), EXIT_CODES.VALIDATION, `Make sure ${CONFIG_FILE$1} exists and is valid JSON.`);
|
|
314
|
+
return result;
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/dev/watcher.ts
|
|
318
|
+
const INVALIDATE_ENDPOINT = "/blodemd-dev/invalidate";
|
|
319
|
+
const WATCH_DEBOUNCE_MS = 100;
|
|
320
|
+
const normalizeRelativePath$1 = (root, filePath) => path.relative(root, filePath).split(path.sep).join("/");
|
|
321
|
+
const isDirectoryEvent = (event) => event === "addDir" || event === "unlinkDir";
|
|
322
|
+
const createDevWatcher = ({ port, root }) => {
|
|
323
|
+
const watcher = watch(root, {
|
|
324
|
+
ignoreInitial: true,
|
|
325
|
+
ignored: [
|
|
326
|
+
"**/.git/**",
|
|
327
|
+
"**/.next/**",
|
|
328
|
+
"**/dist/**",
|
|
329
|
+
"**/node_modules/**"
|
|
330
|
+
]
|
|
331
|
+
});
|
|
332
|
+
let flushTimer = null;
|
|
333
|
+
let pendingKind = "content";
|
|
334
|
+
const pendingPaths = /* @__PURE__ */ new Set();
|
|
335
|
+
const flush = async () => {
|
|
336
|
+
flushTimer = null;
|
|
337
|
+
const paths = [...pendingPaths];
|
|
338
|
+
const kind = pendingKind;
|
|
339
|
+
pendingPaths.clear();
|
|
340
|
+
pendingKind = "content";
|
|
341
|
+
if (!paths.length) return;
|
|
342
|
+
try {
|
|
343
|
+
const response = await fetch(`http://127.0.0.1:${port}${INVALIDATE_ENDPOINT}`, {
|
|
344
|
+
body: JSON.stringify({
|
|
345
|
+
kind,
|
|
346
|
+
paths
|
|
347
|
+
}),
|
|
348
|
+
headers: { "Content-Type": "application/json" },
|
|
349
|
+
method: "POST"
|
|
350
|
+
});
|
|
351
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
log.error(`Failed to invalidate preview cache: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
watcher.on("all", (event, changedPath) => {
|
|
357
|
+
if (isDirectoryEvent(event)) return;
|
|
358
|
+
const relativePath = normalizeRelativePath$1(root, changedPath);
|
|
359
|
+
pendingPaths.add(relativePath);
|
|
360
|
+
if (path.basename(changedPath) === "docs.json") pendingKind = "config";
|
|
361
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
362
|
+
flushTimer = setTimeout(() => {
|
|
363
|
+
flush();
|
|
364
|
+
}, WATCH_DEBOUNCE_MS);
|
|
365
|
+
});
|
|
366
|
+
return { async close() {
|
|
367
|
+
if (flushTimer) {
|
|
368
|
+
clearTimeout(flushTimer);
|
|
369
|
+
await flush();
|
|
370
|
+
}
|
|
371
|
+
await watcher.close();
|
|
372
|
+
} };
|
|
373
|
+
};
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/dev/command.ts
|
|
376
|
+
const DEV_READY_ENDPOINT = "/blodemd-dev/version";
|
|
377
|
+
const DEV_READY_TIMEOUT_MS = 45e3;
|
|
378
|
+
const parsePositiveInteger$1 = (value, label) => {
|
|
379
|
+
const parsed = Number.parseInt(value, 10);
|
|
380
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new CliError(`${label} must be a positive integer.`, EXIT_CODES.VALIDATION);
|
|
381
|
+
return parsed;
|
|
382
|
+
};
|
|
383
|
+
const fileExists$1 = async (filePath) => {
|
|
384
|
+
try {
|
|
385
|
+
await fs.access(filePath);
|
|
386
|
+
return true;
|
|
387
|
+
} catch {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const findMonorepoRoot = async (start) => {
|
|
392
|
+
let current = start;
|
|
393
|
+
while (true) {
|
|
394
|
+
const packageJsonPath = path.join(current, "package.json");
|
|
395
|
+
if (await fileExists$1(packageJsonPath)) {
|
|
396
|
+
const raw = await fs.readFile(packageJsonPath, "utf8");
|
|
397
|
+
const workspaces = JSON.parse(raw).workspaces ?? [];
|
|
398
|
+
if (workspaces.includes("apps/*") && workspaces.includes("packages/*")) return current;
|
|
399
|
+
}
|
|
400
|
+
const parent = path.dirname(current);
|
|
401
|
+
if (parent === current) break;
|
|
402
|
+
current = parent;
|
|
403
|
+
}
|
|
404
|
+
throw new CliError("Could not locate the blodemd monorepo root.", EXIT_CODES.ERROR, "The monorepo-only dev server must be run from this repository checkout.");
|
|
405
|
+
};
|
|
406
|
+
const waitForServer = async ({ child, port }) => {
|
|
407
|
+
const url = `http://localhost:${port}${DEV_READY_ENDPOINT}`;
|
|
408
|
+
const startedAt = Date.now();
|
|
409
|
+
while (Date.now() - startedAt < DEV_READY_TIMEOUT_MS) {
|
|
410
|
+
if (child.exitCode !== null) throw new CliError("The local dev server exited before it became ready.", EXIT_CODES.ERROR);
|
|
411
|
+
try {
|
|
412
|
+
if ((await fetch(url, {
|
|
413
|
+
cache: "no-store",
|
|
414
|
+
headers: { accept: "application/json" }
|
|
415
|
+
})).ok) return;
|
|
416
|
+
} catch {}
|
|
417
|
+
await setTimeout$1(500);
|
|
418
|
+
}
|
|
419
|
+
throw new CliError("Timed out waiting for the local dev server to start.", EXIT_CODES.ERROR);
|
|
420
|
+
};
|
|
421
|
+
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
422
|
+
const devCommand = async ({ dir, openBrowser, port: portValue }) => {
|
|
423
|
+
intro(chalk.bold("blodemd dev"));
|
|
424
|
+
try {
|
|
425
|
+
const port = parsePositiveInteger$1(portValue, "Port");
|
|
426
|
+
const root = await resolveDocsRoot$1(dir);
|
|
427
|
+
await validateDocsRoot(root);
|
|
428
|
+
const cliFilePath = fileURLToPath(import.meta.url);
|
|
429
|
+
const repoRoot = await findMonorepoRoot(path.dirname(cliFilePath));
|
|
430
|
+
const localUrl = `http://localhost:${port}`;
|
|
431
|
+
log.info(`Docs root: ${chalk.cyan(root)}`);
|
|
432
|
+
const child = spawn(npmCommand, [
|
|
433
|
+
"run",
|
|
434
|
+
"dev",
|
|
435
|
+
"--workspace=dev-server"
|
|
436
|
+
], {
|
|
437
|
+
cwd: repoRoot,
|
|
438
|
+
env: {
|
|
439
|
+
...process.env,
|
|
440
|
+
DOCS_ROOT: root,
|
|
441
|
+
PORT: String(port)
|
|
442
|
+
},
|
|
443
|
+
stdio: "inherit"
|
|
444
|
+
});
|
|
445
|
+
let watcher = null;
|
|
446
|
+
let shuttingDown = false;
|
|
447
|
+
const closeAll = async () => {
|
|
448
|
+
if (shuttingDown) return;
|
|
449
|
+
shuttingDown = true;
|
|
450
|
+
if (watcher) {
|
|
451
|
+
await watcher.close();
|
|
452
|
+
watcher = null;
|
|
453
|
+
}
|
|
454
|
+
if (child.exitCode === null && !child.killed) child.kill("SIGTERM");
|
|
455
|
+
};
|
|
456
|
+
const onSignal = async () => {
|
|
457
|
+
await closeAll();
|
|
458
|
+
};
|
|
459
|
+
process.once("SIGINT", onSignal);
|
|
460
|
+
process.once("SIGTERM", onSignal);
|
|
461
|
+
try {
|
|
462
|
+
await waitForServer({
|
|
463
|
+
child,
|
|
464
|
+
port
|
|
465
|
+
});
|
|
466
|
+
watcher = await createDevWatcher({
|
|
467
|
+
port,
|
|
468
|
+
root
|
|
469
|
+
});
|
|
470
|
+
log.success(`Dev server running at ${chalk.cyan(localUrl)}`);
|
|
471
|
+
if (openBrowser) await open(localUrl);
|
|
472
|
+
const [code, signal] = await once(child, "exit");
|
|
473
|
+
await closeAll();
|
|
474
|
+
process.removeListener("SIGINT", onSignal);
|
|
475
|
+
process.removeListener("SIGTERM", onSignal);
|
|
476
|
+
if (shuttingDown || signal === "SIGINT" || signal === "SIGTERM") return;
|
|
477
|
+
if (code !== 0) throw new CliError(`The local dev server exited with code ${code ?? "unknown"}.`, EXIT_CODES.ERROR);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
await closeAll();
|
|
480
|
+
process.removeListener("SIGINT", onSignal);
|
|
481
|
+
process.removeListener("SIGTERM", onSignal);
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
} catch (error) {
|
|
485
|
+
const cliError = toCliError(error);
|
|
486
|
+
log.error(cliError.message);
|
|
487
|
+
if (cliError.hint) log.info(cliError.hint);
|
|
488
|
+
process.exitCode = cliError.exitCode;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
//#endregion
|
|
286
492
|
//#region src/oauth-callback.ts
|
|
287
493
|
const SUCCESS_HTML = "<!doctype html><html><head><meta charset=\"utf-8\"/><title>Blode.md CLI</title></head><body><h2>Logged in! You can close this tab.</h2></body></html>";
|
|
288
494
|
const escapeHtml = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
@@ -294,6 +500,7 @@ const waitForOAuthCode = (options) => {
|
|
|
294
500
|
if (!Number.isInteger(port) || port <= 0) return Promise.reject(new CliError("OAuth redirect URL requires an explicit port", EXIT_CODES.ERROR));
|
|
295
501
|
return new Promise((resolve, reject) => {
|
|
296
502
|
let settled = false;
|
|
503
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
297
504
|
const settle = (ok, value) => {
|
|
298
505
|
if (settled) return;
|
|
299
506
|
settled = true;
|
|
@@ -302,6 +509,7 @@ const waitForOAuthCode = (options) => {
|
|
|
302
509
|
if (ok) resolve(value);
|
|
303
510
|
else reject(value);
|
|
304
511
|
});
|
|
512
|
+
for (const socket of sockets) socket.destroy();
|
|
305
513
|
};
|
|
306
514
|
const httpServer = createServer((request, response) => {
|
|
307
515
|
if (!request.url) {
|
|
@@ -341,6 +549,10 @@ const waitForOAuthCode = (options) => {
|
|
|
341
549
|
response.end(SUCCESS_HTML);
|
|
342
550
|
settle(true, code);
|
|
343
551
|
});
|
|
552
|
+
httpServer.on("connection", (socket) => {
|
|
553
|
+
sockets.add(socket);
|
|
554
|
+
socket.once("close", () => sockets.delete(socket));
|
|
555
|
+
});
|
|
344
556
|
httpServer.on("error", (error) => {
|
|
345
557
|
settle(false, new CliError(`Failed to start callback server on ${host}:${port}: ${error.message}`, EXIT_CODES.ERROR));
|
|
346
558
|
});
|
|
@@ -624,14 +836,7 @@ program.command("whoami").description("Show current authentication").action(asyn
|
|
|
624
836
|
const email = resolved.user?.email ?? await fetchUserEmail(process.env["BLODEMD_API_URL"] ?? "https://api.blode.md", resolved.token);
|
|
625
837
|
if (email) log.info(`Logged in as ${chalk.cyan(email)}`);
|
|
626
838
|
else log.info("Logged in (could not fetch user details).");
|
|
627
|
-
if (resolved.expiresAt)
|
|
628
|
-
if (status.expired) log.warn("Session has expired. Run \"blodemd login\" to re-authenticate.");
|
|
629
|
-
else if (status.expiresInSeconds !== null) {
|
|
630
|
-
const hours = Math.floor(status.expiresInSeconds / 3600);
|
|
631
|
-
const minutes = Math.floor(status.expiresInSeconds % 3600 / 60);
|
|
632
|
-
log.info(`Session expires in ${hours}h ${minutes}m`);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
839
|
+
if (resolved.expiresAt && status.expired) log.warn("Session has expired. Run \"blodemd login\" to re-authenticate.");
|
|
635
840
|
} catch (error) {
|
|
636
841
|
reportCommandError("Whoami failed", error);
|
|
637
842
|
}
|
|
@@ -731,12 +936,11 @@ program.command("push").description("Deploy docs").argument("[dir]", "docs direc
|
|
|
731
936
|
reportCommandError("Push failed", error);
|
|
732
937
|
}
|
|
733
938
|
});
|
|
734
|
-
program.command("dev").description("Start the docs dev server").action(() => {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
});
|
|
939
|
+
program.command("dev").description("Start the local docs dev server").option("-p, --port <port>", "Port number", "3030").option("-d, --dir <dir>", "Docs directory").option("--no-open", "Don't open browser").action(async (options) => await devCommand({
|
|
940
|
+
dir: options.dir,
|
|
941
|
+
openBrowser: options.open ?? true,
|
|
942
|
+
port: options.port
|
|
943
|
+
}));
|
|
740
944
|
program.parse();
|
|
741
945
|
//#endregion
|
|
742
946
|
export {};
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/constants.ts","../src/jwt.ts","../src/errors.ts","../src/oauth-token.ts","../src/storage.ts","../src/supabase.ts","../src/auth-session.ts","../src/oauth-callback.ts","../src/pkce.ts","../src/cli.ts"],"sourcesContent":["import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const CLI_NAME = \"blodemd\";\n\nexport const BLODE_TOKEN_ENV = \"BLODEMD_API_KEY\";\nexport const BLODE_API_URL_ENV = \"BLODEMD_API_URL\";\nexport const BLODE_PROJECT_ENV = \"BLODEMD_PROJECT\";\nexport const BLODE_BRANCH_ENV = \"BLODEMD_BRANCH\";\nexport const BLODE_COMMIT_MESSAGE_ENV = \"BLODEMD_COMMIT_MESSAGE\";\n\nexport const DEFAULT_API_URL = \"https://api.blode.md\";\nexport const DEFAULT_SUPABASE_URL = \"https://bwnxwgkgyklzzmpbzuoz.supabase.co\";\n\nexport const OAUTH_CLIENT_ID = \"6b5f9860-fe96-4a83-b1ad-266260523c91\";\n\nexport const DEFAULT_OAUTH_CALLBACK_PORT = 8787;\nexport const DEFAULT_OAUTH_CALLBACK_PATH = \"/auth/callback\";\nexport const DEFAULT_OAUTH_TIMEOUT_SECONDS = 180;\n\nconst getDefaultConfigBaseDir = (): string => {\n if (process.platform === \"win32\") {\n return process.env.APPDATA ?? join(homedir(), \"AppData\", \"Roaming\");\n }\n\n return process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\");\n};\n\nconst configBaseDir = getDefaultConfigBaseDir();\n\nexport const CONFIG_DIR = join(configBaseDir, CLI_NAME);\nexport const CREDENTIALS_FILE = join(CONFIG_DIR, \"credentials.json\");\n","export interface JwtClaims {\n exp?: number;\n email?: string;\n sub?: string;\n}\n\nconst parseJwtBase64Url = (input: string): string => {\n const normalized = input.replaceAll(\"-\", \"+\").replaceAll(\"_\", \"/\");\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, \"=\");\n return Buffer.from(padded, \"base64\").toString(\"utf8\");\n};\n\nexport const parseJwtClaims = (token: string): JwtClaims | null => {\n const parts = token.split(\".\");\n const payloadPart = parts.at(1);\n\n if (!payloadPart) {\n return null;\n }\n\n try {\n const payload = parseJwtBase64Url(payloadPart);\n const parsed = JSON.parse(payload) as unknown;\n\n if (typeof parsed !== \"object\" || parsed === null) {\n return null;\n }\n\n const claims = parsed as Record<string, unknown>;\n\n return {\n email: typeof claims.email === \"string\" ? claims.email : undefined,\n exp: typeof claims.exp === \"number\" ? claims.exp : undefined,\n sub: typeof claims.sub === \"string\" ? claims.sub : undefined,\n };\n } catch {\n return null;\n }\n};\n","export const EXIT_CODES = {\n AUTH_REQUIRED: 4,\n CANCELLED: 2,\n ERROR: 1,\n NETWORK: 5,\n SUCCESS: 0,\n VALIDATION: 3,\n} as const;\n\ntype ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];\n\nexport class CliError extends Error {\n readonly exitCode: ExitCode;\n readonly hint: string | null;\n\n constructor(\n message: string,\n exitCode: ExitCode = EXIT_CODES.ERROR,\n hint?: string\n ) {\n super(message);\n this.name = \"CliError\";\n this.exitCode = exitCode;\n this.hint = hint ?? null;\n }\n}\n\nexport const toCliError = (error: unknown): CliError => {\n if (error instanceof CliError) {\n return error;\n }\n\n if (error instanceof Error) {\n if (error instanceof TypeError && error.message.includes(\"fetch\")) {\n return new CliError(\n \"Cannot connect to Blode.md API.\",\n EXIT_CODES.NETWORK,\n \"Check your internet connection and API URL configuration.\"\n );\n }\n\n if (error.name === \"TimeoutError\" || error.name === \"AbortError\") {\n return new CliError(\n \"Request timed out.\",\n EXIT_CODES.NETWORK,\n \"The API may be unavailable. Try again later.\"\n );\n }\n\n return new CliError(error.message, EXIT_CODES.ERROR);\n }\n\n return new CliError(\"Unknown error\", EXIT_CODES.ERROR);\n};\n","import { CliError, EXIT_CODES } from \"./errors.js\";\n\nexport interface OAuthTokenConfig {\n tokenUrl: string;\n clientId: string;\n}\n\nexport interface OAuthTokenResponse {\n access_token: string;\n refresh_token?: string;\n token_type: string;\n expires_in: number;\n}\n\nconst postTokenRequest = async (\n url: string,\n body: URLSearchParams\n): Promise<OAuthTokenResponse> => {\n const response = await fetch(url, {\n body: body.toString(),\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n method: \"POST\",\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n throw new CliError(\n `OAuth token request failed (${response.status}): ${text}`,\n EXIT_CODES.AUTH_REQUIRED\n );\n }\n\n return (await response.json()) as OAuthTokenResponse;\n};\n\nexport const exchangeAuthorizationCode = (\n config: OAuthTokenConfig,\n code: string,\n codeVerifier: string,\n redirectUri: string\n): Promise<OAuthTokenResponse> => {\n const body = new URLSearchParams({\n client_id: config.clientId,\n code,\n code_verifier: codeVerifier,\n grant_type: \"authorization_code\",\n redirect_uri: redirectUri,\n });\n\n return postTokenRequest(config.tokenUrl, body);\n};\n\nexport const refreshAccessToken = (\n config: OAuthTokenConfig,\n refreshToken: string\n): Promise<OAuthTokenResponse> => {\n const body = new URLSearchParams({\n client_id: config.clientId,\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n });\n\n return postTokenRequest(config.tokenUrl, body);\n};\n","import { mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\n\nimport { CONFIG_DIR, CREDENTIALS_FILE } from \"./constants.js\";\nimport { CliError, EXIT_CODES } from \"./errors.js\";\nimport type {\n ApiKeyCredentials,\n AuthFileData,\n StoredAuthSession,\n} from \"./types.js\";\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\nconst parseStoredAuthSession = (value: unknown): StoredAuthSession | null => {\n if (!isRecord(value)) {\n return null;\n }\n\n if (typeof value.accessToken !== \"string\") {\n return null;\n }\n\n if (value.refreshToken !== null && typeof value.refreshToken !== \"string\") {\n return null;\n }\n\n if (value.expiresAt !== null && typeof value.expiresAt !== \"string\") {\n return null;\n }\n\n const { user } = value;\n if (\n user !== null &&\n (!isRecord(user) ||\n typeof user.id !== \"string\" ||\n (user.email !== null && typeof user.email !== \"string\"))\n ) {\n return null;\n }\n\n if (typeof value.createdAt !== \"string\") {\n return null;\n }\n\n const parsedUser =\n user === null || !isRecord(user)\n ? null\n : {\n email: (user.email as string | null) ?? null,\n id: user.id as string,\n };\n\n return {\n accessToken: value.accessToken,\n createdAt: value.createdAt,\n expiresAt: (value.expiresAt as string | null) ?? null,\n refreshToken: (value.refreshToken as string | null) ?? null,\n user: parsedUser,\n };\n};\n\nconst parseApiKeyCredentials = (value: unknown): ApiKeyCredentials | null => {\n if (!isRecord(value)) {\n return null;\n }\n\n if (typeof value.apiKey !== \"string\") {\n return null;\n }\n\n return { apiKey: value.apiKey, type: \"api-key\" };\n};\n\nexport const readAuthFile = async (): Promise<AuthFileData | null> => {\n try {\n const raw = await readFile(CREDENTIALS_FILE, \"utf8\");\n const parsed = JSON.parse(raw) as unknown;\n\n if (!isRecord(parsed) || parsed.version !== 1) {\n throw new CliError(\n `Invalid credentials format in ${CREDENTIALS_FILE}`,\n EXIT_CODES.ERROR\n );\n }\n\n return {\n apiKey: parseApiKeyCredentials(parsed.apiKey) ?? undefined,\n session: parseStoredAuthSession(parsed.session) ?? undefined,\n version: 1,\n };\n } catch (error) {\n if (isRecord(error) && error.code === \"ENOENT\") {\n return null;\n }\n\n if (error instanceof CliError) {\n throw error;\n }\n\n return null;\n }\n};\n\nexport const readStoredAuthSession =\n async (): Promise<StoredAuthSession | null> => {\n const data = await readAuthFile();\n return data?.session ?? null;\n };\n\nexport const readStoredApiKey = async (): Promise<ApiKeyCredentials | null> => {\n const data = await readAuthFile();\n return data?.apiKey ?? null;\n};\n\nconst writeAuthFile = async (data: AuthFileData): Promise<void> => {\n await mkdir(CONFIG_DIR, { mode: 0o700, recursive: true });\n await writeFile(CREDENTIALS_FILE, `${JSON.stringify(data, null, 2)}\\n`, {\n encoding: \"utf8\",\n mode: 0o600,\n });\n};\n\nexport const writeStoredAuthSession = async (\n session: StoredAuthSession\n): Promise<void> => {\n await writeAuthFile({\n session,\n version: 1,\n });\n};\n\nexport const writeStoredApiKey = async (\n apiKey: ApiKeyCredentials\n): Promise<void> => {\n await writeAuthFile({\n apiKey,\n version: 1,\n });\n};\n\nexport const clearStoredCredentials = async (): Promise<void> => {\n await rm(CREDENTIALS_FILE, { force: true });\n};\n","import { DEFAULT_SUPABASE_URL } from \"./constants.js\";\nimport { parseJwtClaims } from \"./jwt.js\";\nimport type { OAuthTokenResponse } from \"./oauth-token.js\";\nimport type { StoredAuthSession, SupabaseConfig } from \"./types.js\";\n\nexport const resolveSupabaseConfig = (): SupabaseConfig => {\n const url =\n process.env.SUPABASE_URL ??\n process.env.NEXT_PUBLIC_SUPABASE_URL ??\n DEFAULT_SUPABASE_URL;\n\n return { url };\n};\n\nexport const buildOAuthUrls = (\n config: SupabaseConfig\n): {\n authorizeUrl: string;\n tokenUrl: string;\n} => ({\n authorizeUrl: `${config.url}/auth/v1/oauth/authorize`,\n tokenUrl: `${config.url}/auth/v1/oauth/token`,\n});\n\nexport const tokenResponseToStoredSession = (\n response: OAuthTokenResponse\n): StoredAuthSession => {\n const claims = parseJwtClaims(response.access_token);\n\n let expiresAt: string | null = null;\n if (typeof claims?.exp === \"number\") {\n expiresAt = new Date(claims.exp * 1000).toISOString();\n } else if (response.expires_in > 0) {\n expiresAt = new Date(Date.now() + response.expires_in * 1000).toISOString();\n }\n\n return {\n accessToken: response.access_token,\n createdAt: new Date().toISOString(),\n expiresAt,\n refreshToken: response.refresh_token ?? null,\n user:\n claims?.sub || claims?.email\n ? {\n email: claims.email ?? null,\n id: claims.sub ?? \"unknown\",\n }\n : null,\n };\n};\n","import { BLODE_TOKEN_ENV, OAUTH_CLIENT_ID } from \"./constants.js\";\nimport { parseJwtClaims } from \"./jwt.js\";\nimport { refreshAccessToken } from \"./oauth-token.js\";\nimport {\n clearStoredCredentials,\n readAuthFile,\n writeStoredAuthSession,\n} from \"./storage.js\";\nimport {\n buildOAuthUrls,\n resolveSupabaseConfig,\n tokenResponseToStoredSession,\n} from \"./supabase.js\";\nimport type { ResolvedAuthToken, StoredAuthSession } from \"./types.js\";\n\nconst expiresInMs = (session: StoredAuthSession): number | null => {\n if (!session.expiresAt) {\n return null;\n }\n\n const expiresAtMs = Date.parse(session.expiresAt);\n\n if (Number.isNaN(expiresAtMs)) {\n return null;\n }\n\n return expiresAtMs - Date.now();\n};\n\nconst isExpired = (session: StoredAuthSession): boolean => {\n const ms = expiresInMs(session);\n return ms !== null && ms <= 0;\n};\n\nconst shouldRefresh = (session: StoredAuthSession): boolean => {\n const ms = expiresInMs(session);\n return ms !== null && ms <= 60_000;\n};\n\nconst tokenFromRaw = (\n token: string,\n source: ResolvedAuthToken[\"source\"]\n): ResolvedAuthToken => {\n const claims = parseJwtClaims(token);\n\n const expiresAt =\n typeof claims?.exp === \"number\"\n ? new Date(claims.exp * 1000).toISOString()\n : null;\n\n return {\n expiresAt,\n source,\n token,\n user:\n claims?.sub || claims?.email\n ? { email: claims.email ?? null, id: claims.sub ?? \"unknown\" }\n : null,\n };\n};\n\nconst sessionToResolvedToken = (\n session: StoredAuthSession\n): ResolvedAuthToken => ({\n expiresAt: session.expiresAt,\n source: \"stored\",\n token: session.accessToken,\n user: session.user,\n});\n\nexport const resolveAuthToken = async (\n optApiKey?: string\n): Promise<ResolvedAuthToken | null> => {\n const envToken = (optApiKey ?? process.env[BLODE_TOKEN_ENV])?.trim();\n\n if (envToken) {\n return tokenFromRaw(envToken, optApiKey ? \"flag\" : \"environment\");\n }\n\n const data = await readAuthFile();\n const session = data?.session;\n\n if (session) {\n if (!(shouldRefresh(session) || isExpired(session))) {\n return sessionToResolvedToken(session);\n }\n\n if (session.refreshToken) {\n try {\n const config = resolveSupabaseConfig();\n const { tokenUrl } = buildOAuthUrls(config);\n const tokenResponse = await refreshAccessToken(\n { clientId: OAUTH_CLIENT_ID, tokenUrl },\n session.refreshToken\n );\n const updatedSession = tokenResponseToStoredSession(tokenResponse);\n await writeStoredAuthSession(updatedSession);\n\n return sessionToResolvedToken(updatedSession);\n } catch {\n // Refresh failed — fall through to expiry check\n }\n }\n\n if (isExpired(session)) {\n await clearStoredCredentials();\n return null;\n }\n\n return sessionToResolvedToken(session);\n }\n\n if (data?.apiKey) {\n return {\n expiresAt: null,\n source: \"stored\",\n token: data.apiKey.apiKey,\n user: null,\n };\n }\n\n return null;\n};\n\nexport const resolveTokenStatus = (\n token: ResolvedAuthToken\n): {\n expiresInSeconds: number | null;\n expired: boolean;\n} => {\n if (!token.expiresAt) {\n return { expired: false, expiresInSeconds: null };\n }\n\n const expiresAtMs = Date.parse(token.expiresAt);\n\n if (Number.isNaN(expiresAtMs)) {\n return { expired: false, expiresInSeconds: null };\n }\n\n const expiresInSeconds = Math.floor((expiresAtMs - Date.now()) / 1000);\n\n return {\n expired: expiresInSeconds <= 0,\n expiresInSeconds,\n };\n};\n","// oxlint-disable no-use-before-define -- circular reference in callback pattern\nimport { createServer } from \"node:http\";\n\nimport { CliError, EXIT_CODES } from \"./errors.js\";\n\ninterface OAuthCallbackOptions {\n redirectUrl: URL;\n expectedState: string;\n timeoutMs: number;\n}\n\nconst SUCCESS_HTML =\n '<!doctype html><html><head><meta charset=\"utf-8\"/><title>Blode.md CLI</title></head><body><h2>Logged in! You can close this tab.</h2></body></html>';\n\nconst escapeHtml = (text: string): string =>\n text\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\");\n\nconst errorHtml = (message: string): string =>\n `<!doctype html><html><head><meta charset=\"utf-8\"/><title>Blode.md CLI</title></head><body><h2>Login failed</h2><p>${escapeHtml(message)}</p></body></html>`;\n\nexport const waitForOAuthCode = (\n options: OAuthCallbackOptions\n): Promise<string> => {\n const host = options.redirectUrl.hostname;\n const port = Number(options.redirectUrl.port);\n const { pathname } = options.redirectUrl;\n\n if (!Number.isInteger(port) || port <= 0) {\n return Promise.reject(\n new CliError(\n \"OAuth redirect URL requires an explicit port\",\n EXIT_CODES.ERROR\n )\n );\n }\n\n // oxlint-disable-next-line eslint-plugin-promise/avoid-new -- wrapping callback-based HTTP server\n return new Promise<string>((resolve, reject) => {\n let settled = false;\n\n const settle = (ok: boolean, value: string | CliError): void => {\n if (settled) {\n return;\n }\n\n settled = true;\n clearTimeout(timer);\n\n httpServer.close(() => {\n if (ok) {\n resolve(value as string);\n } else {\n reject(value);\n }\n });\n };\n\n const httpServer = createServer((request, response) => {\n if (!request.url) {\n response.writeHead(400, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(errorHtml(\"Missing request URL\"));\n settle(\n false,\n new CliError(\n \"OAuth callback is missing a request URL\",\n EXIT_CODES.ERROR\n )\n );\n return;\n }\n\n const url = new URL(request.url, options.redirectUrl.origin);\n\n if (url.pathname !== pathname) {\n response.writeHead(404, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(errorHtml(\"Invalid callback path\"));\n return;\n }\n\n const providerError = url.searchParams.get(\"error\");\n if (providerError) {\n const description =\n url.searchParams.get(\"error_description\") ?? providerError;\n\n response.writeHead(400, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(errorHtml(description));\n\n settle(\n false,\n new CliError(\n `OAuth provider returned an error: ${description}`,\n EXIT_CODES.ERROR\n )\n );\n return;\n }\n\n const state = url.searchParams.get(\"state\");\n if (state !== options.expectedState) {\n response.writeHead(400, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(errorHtml(\"State verification failed\"));\n\n settle(\n false,\n new CliError(\"OAuth state verification failed\", EXIT_CODES.ERROR)\n );\n return;\n }\n\n const code = url.searchParams.get(\"code\");\n if (!code) {\n response.writeHead(400, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(errorHtml(\"Authorization code was missing\"));\n\n settle(\n false,\n new CliError(\n \"OAuth callback is missing an authorization code\",\n EXIT_CODES.ERROR\n )\n );\n return;\n }\n\n response.writeHead(200, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(SUCCESS_HTML);\n settle(true, code);\n });\n\n httpServer.on(\"error\", (error) => {\n settle(\n false,\n new CliError(\n `Failed to start callback server on ${host}:${port}: ${error.message}`,\n EXIT_CODES.ERROR\n )\n );\n });\n\n const timer = setTimeout(() => {\n settle(\n false,\n new CliError(\"Login timed out. Please try again.\", EXIT_CODES.CANCELLED)\n );\n }, options.timeoutMs);\n\n httpServer.listen(port, host);\n });\n};\n","import { createHash, randomBytes } from \"node:crypto\";\n\nexport const createOAuthState = (): string => randomBytes(24).toString(\"hex\");\n\nexport const createCodeVerifier = (): string =>\n randomBytes(64).toString(\"base64url\");\n\nexport const createCodeChallenge = (verifier: string): string =>\n createHash(\"sha256\").update(verifier).digest().toString(\"base64url\");\n","import { spawnSync } from \"node:child_process\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport {\n confirm,\n intro,\n isCancel,\n log,\n password,\n spinner,\n} from \"@clack/prompts\";\nimport chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport open from \"open\";\n\nimport { resolveAuthToken, resolveTokenStatus } from \"./auth-session.js\";\nimport {\n BLODE_API_URL_ENV,\n BLODE_BRANCH_ENV,\n BLODE_COMMIT_MESSAGE_ENV,\n BLODE_PROJECT_ENV,\n DEFAULT_API_URL,\n DEFAULT_OAUTH_CALLBACK_PATH,\n DEFAULT_OAUTH_CALLBACK_PORT,\n DEFAULT_OAUTH_TIMEOUT_SECONDS,\n OAUTH_CLIENT_ID,\n} from \"./constants.js\";\nimport { CliError, EXIT_CODES, toCliError } from \"./errors.js\";\nimport { waitForOAuthCode } from \"./oauth-callback.js\";\nimport { exchangeAuthorizationCode } from \"./oauth-token.js\";\nimport {\n createCodeChallenge,\n createCodeVerifier,\n createOAuthState,\n} from \"./pkce.js\";\nimport {\n clearStoredCredentials,\n readAuthFile,\n writeStoredApiKey,\n writeStoredAuthSession,\n} from \"./storage.js\";\nimport {\n buildOAuthUrls,\n resolveSupabaseConfig,\n tokenResponseToStoredSession,\n} from \"./supabase.js\";\nimport type { DeploymentResponse } from \"./types.js\";\n\nconst CONFIG_FILE = \"docs.json\";\n\nconst TEXT_CONTENT_TYPES: Record<string, string> = {\n \".css\": \"text/css; charset=utf-8\",\n \".html\": \"text/html; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".json\": \"application/json; charset=utf-8\",\n \".md\": \"text/markdown; charset=utf-8\",\n \".mdx\": \"text/markdown; charset=utf-8\",\n \".svg\": \"image/svg+xml\",\n \".txt\": \"text/plain; charset=utf-8\",\n \".yaml\": \"application/yaml; charset=utf-8\",\n \".yml\": \"application/yaml; charset=utf-8\",\n};\n\n// --- File helpers ---\n\nconst ensureFile = async (filePath: string, content: string): Promise<void> => {\n try {\n await fs.writeFile(filePath, content, { flag: \"wx\" });\n } catch {\n // File already exists\n }\n};\n\nconst fileExists = async (filePath: string): Promise<boolean> => {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n};\n\nconst readConfig = async (\n root: string\n): Promise<{ name?: string; raw: string }> => {\n const raw = await fs.readFile(path.join(root, CONFIG_FILE), \"utf8\");\n const parsed = JSON.parse(raw) as { name?: string };\n return { name: parsed.name, raw };\n};\n\nconst resolveDocsRoot = async (dir?: string): Promise<string> => {\n if (dir) {\n return path.resolve(process.cwd(), dir);\n }\n\n const candidates = [\n process.cwd(),\n path.join(process.cwd(), \"docs\"),\n path.join(process.cwd(), \"apps/docs\"),\n ];\n\n for (const candidate of candidates) {\n if (await fileExists(path.join(candidate, CONFIG_FILE))) {\n return candidate;\n }\n }\n\n return process.cwd();\n};\n\nconst readGitValue = (gitArgs: string[]): string | undefined => {\n const result = spawnSync(\"git\", gitArgs, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n });\n\n if (result.status !== 0) {\n return;\n }\n\n const value = result.stdout.trim();\n return value || undefined;\n};\n\nconst normalizeRelativePath = (root: string, filePath: string): string =>\n path.relative(root, filePath).split(path.sep).join(\"/\");\n\nconst shouldSkipEntry = (name: string): boolean =>\n name.startsWith(\".\") || name === \"node_modules\";\n\nconst collectFiles = async (root: string): Promise<string[]> => {\n const entries = await fs.readdir(root, { withFileTypes: true });\n const files: string[] = [];\n\n for (const entry of entries) {\n if (shouldSkipEntry(entry.name)) {\n continue;\n }\n\n const absolutePath = path.join(root, entry.name);\n if (entry.isDirectory()) {\n files.push(...(await collectFiles(absolutePath)));\n continue;\n }\n\n if (entry.isFile()) {\n files.push(absolutePath);\n }\n }\n\n return files.toSorted((left, right) => left.localeCompare(right));\n};\n\nconst getContentType = (filePath: string): string =>\n TEXT_CONTENT_TYPES[path.extname(filePath).toLowerCase()] ??\n \"application/octet-stream\";\n\nconst readJson = async (response: Response): Promise<unknown> => {\n const text = await response.text();\n if (!text) {\n return null;\n }\n\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n};\n\nconst requestJson = async <T>(\n url: string,\n init: RequestInit,\n message: string\n): Promise<T> => {\n const response = await fetch(url, init);\n const data = await readJson(response);\n if (!response.ok) {\n const detail =\n typeof data === \"string\" ? data : JSON.stringify(data ?? {}, null, 2);\n throw new Error(`${message}: ${response.status} ${detail}`);\n }\n\n return data as T;\n};\n\nconst parsePositiveInteger = (value: string, label: string): number => {\n const parsed = Number.parseInt(value, 10);\n\n if (!Number.isInteger(parsed) || parsed <= 0) {\n throw new CliError(\n `${label} must be a positive integer.`,\n EXIT_CODES.VALIDATION\n );\n }\n\n return parsed;\n};\n\nconst reportCommandError = (prefix: string, error: unknown): void => {\n const cliError = toCliError(error);\n\n log.error(`${prefix}: ${cliError.message}`);\n if (cliError.hint) {\n log.info(cliError.hint);\n }\n log.info(\"Failed\");\n process.exitCode = cliError.exitCode;\n};\n\n// --- Auth helpers ---\n\nconst fetchUserEmail = async (\n apiUrl: string,\n token: string\n): Promise<string | null> => {\n try {\n const user = await requestJson<{ email: string }>(\n `${apiUrl}/auth/me`,\n { headers: { Authorization: `Bearer ${token}` } },\n \"Failed to fetch user info\"\n );\n return user.email;\n } catch {\n return null;\n }\n};\n\n// --- Push helpers ---\n\ninterface PushConfig {\n project: string;\n apiUrl: string;\n authToken: string;\n branch: string;\n commitMessage?: string;\n}\n\nconst resolvePushConfig = async (\n config: { name?: string },\n options: {\n apiKey?: string;\n apiUrl?: string;\n branch?: string;\n message?: string;\n project?: string;\n }\n): Promise<PushConfig> => {\n const project =\n options.project ?? process.env[BLODE_PROJECT_ENV] ?? config.name;\n const apiUrl =\n options.apiUrl ?? process.env[BLODE_API_URL_ENV] ?? DEFAULT_API_URL;\n\n const resolved = await resolveAuthToken(options.apiKey);\n const authToken = resolved?.token;\n\n const branch =\n options.branch ??\n process.env[BLODE_BRANCH_ENV] ??\n process.env.GITHUB_REF_NAME ??\n readGitValue([\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]) ??\n \"main\";\n const commitMessage =\n options.message ??\n process.env[BLODE_COMMIT_MESSAGE_ENV] ??\n readGitValue([\"log\", \"-1\", \"--pretty=%s\"]);\n\n if (!project) {\n throw new Error(\n 'Missing project slug. Set \"name\" in docs.json, pass --project, or set BLODEMD_PROJECT.'\n );\n }\n if (!authToken) {\n throw new Error(\n 'Missing credentials. Run \"blodemd login\", pass --api-key, or set BLODEMD_API_KEY.'\n );\n }\n\n return { apiUrl, authToken, branch, commitMessage, project };\n};\n\nconst autoCreateProject = async (\n project: string,\n apiUrl: string,\n headers: Record<string, string>\n): Promise<boolean> => {\n const authData = await readAuthFile();\n if (!authData?.session) {\n throw new Error(\n `Project \"${project}\" not found. Create it at blode.md or login with \"blodemd login\" to auto-create.`\n );\n }\n\n const shouldCreate = await confirm({\n message: `Project \"${project}\" doesn't exist. Create it?`,\n });\n\n if (isCancel(shouldCreate) || !shouldCreate) {\n return false;\n }\n\n const createResult = await requestJson<{\n project: { id: string; slug: string };\n token: string;\n }>(\n new URL(\"/projects\", apiUrl).toString(),\n {\n body: JSON.stringify({ name: project, slug: project }),\n headers,\n method: \"POST\",\n },\n \"Failed to create project\"\n );\n\n log.success(`Project ${chalk.cyan(createResult.project.slug)} created`);\n log.info(`API key for CI: ${chalk.dim(createResult.token)}`);\n return true;\n};\n\nconst uploadFiles = async (\n files: string[],\n root: string,\n apiPath: (suffix: string) => string,\n deploymentId: string,\n headers: Record<string, string>,\n s: ReturnType<typeof spinner>\n) => {\n s.start(`Uploading ${files.length} files`);\n for (const [index, filePath] of files.entries()) {\n const relativePath = normalizeRelativePath(root, filePath);\n const content = await fs.readFile(filePath);\n\n await requestJson(\n apiPath(`/${deploymentId}/files`),\n {\n body: JSON.stringify({\n contentBase64: content.toString(\"base64\"),\n contentType: getContentType(filePath),\n path: relativePath,\n }),\n headers,\n method: \"POST\",\n },\n `Failed to upload ${relativePath}`\n );\n\n s.message(`Uploading files (${index + 1}/${files.length})`);\n }\n s.stop(`Uploaded ${chalk.cyan(String(files.length))} files`);\n};\n\n// --- CLI ---\n\nconst program = new Command();\n\nprogram.name(\"blodemd\").description(\"Blode.md CLI\").version(\"0.0.3\");\n\n// login\n\nprogram\n .command(\"login\")\n .description(\"Authenticate with Blode.md\")\n .option(\"--token\", \"Paste an API key instead of using browser login\")\n .option(\n \"--port <port>\",\n \"Loopback callback port\",\n String(DEFAULT_OAUTH_CALLBACK_PORT)\n )\n .option(\n \"--timeout <seconds>\",\n \"OAuth timeout in seconds\",\n String(DEFAULT_OAUTH_TIMEOUT_SECONDS)\n )\n .option(\"--no-open\", \"Print URL instead of opening the browser\")\n .action(\n async (options: {\n token?: boolean;\n port: string;\n timeout: string;\n open: boolean;\n }) => {\n intro(chalk.bold(\"blodemd login\"));\n\n try {\n if (options.token) {\n const apiKey = await password({\n message: \"Enter your API key\",\n validate: (value) => {\n if (!value) {\n return \"API key is required.\";\n }\n },\n });\n\n if (isCancel(apiKey)) {\n log.warn(\"Cancelled\");\n return;\n }\n\n await writeStoredApiKey({ apiKey, type: \"api-key\" });\n\n const prefix = apiKey.split(\".\")[0] ?? apiKey.slice(0, 12);\n log.success(`Authenticated as ${chalk.cyan(prefix)}`);\n log.info(\"Done\");\n return;\n }\n\n // OAuth 2.1 authorization code flow with PKCE\n const config = resolveSupabaseConfig();\n const { authorizeUrl, tokenUrl } = buildOAuthUrls(config);\n const clientId = OAUTH_CLIENT_ID;\n\n const port = parsePositiveInteger(options.port, \"Port\");\n const timeoutSeconds = parsePositiveInteger(options.timeout, \"Timeout\");\n const redirectUrl = new URL(\n `http://127.0.0.1:${port}${DEFAULT_OAUTH_CALLBACK_PATH}`\n );\n\n const state = createOAuthState();\n const codeVerifier = createCodeVerifier();\n const codeChallenge = createCodeChallenge(codeVerifier);\n\n const authUrl = new URL(authorizeUrl);\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"client_id\", clientId);\n authUrl.searchParams.set(\"redirect_uri\", redirectUrl.toString());\n authUrl.searchParams.set(\"code_challenge\", codeChallenge);\n authUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n authUrl.searchParams.set(\"state\", state);\n authUrl.searchParams.set(\"scope\", \"openid email profile\");\n\n const callbackPromise = waitForOAuthCode({\n expectedState: state,\n redirectUrl,\n timeoutMs: timeoutSeconds * 1000,\n });\n\n if (options.open) {\n log.info(\"Opening browser for authentication...\");\n log.info(\n `If the browser doesn't open, visit: ${chalk.cyan(authUrl.toString())}`\n );\n await open(authUrl.toString());\n } else {\n log.info(\"Open this URL to continue authentication:\");\n log.info(chalk.cyan(authUrl.toString()));\n }\n\n const code = await callbackPromise;\n\n const tokenResponse = await exchangeAuthorizationCode(\n { clientId, tokenUrl },\n code,\n codeVerifier,\n redirectUrl.toString()\n );\n\n const storedSession = tokenResponseToStoredSession(tokenResponse);\n await writeStoredAuthSession(storedSession);\n\n const email =\n storedSession.user?.email ??\n (await fetchUserEmail(\n process.env[BLODE_API_URL_ENV] ?? DEFAULT_API_URL,\n storedSession.accessToken\n ));\n\n if (email) {\n log.success(`Logged in as ${chalk.cyan(email)}`);\n } else {\n log.success(\"Logged in successfully.\");\n }\n\n log.info(\"Done\");\n } catch (error: unknown) {\n reportCommandError(\"Login failed\", error);\n }\n }\n );\n\n// logout\n\nprogram\n .command(\"logout\")\n .description(\"Remove stored credentials\")\n .action(async () => {\n intro(chalk.bold(\"blodemd logout\"));\n\n try {\n const existing = await readAuthFile();\n await clearStoredCredentials();\n\n if (existing?.session || existing?.apiKey) {\n log.success(\"Credentials removed.\");\n } else {\n log.info(\"No stored credentials found.\");\n }\n log.info(\"Done\");\n } catch (error: unknown) {\n reportCommandError(\"Logout failed\", error);\n }\n });\n\n// whoami\n\nprogram\n .command(\"whoami\")\n .description(\"Show current authentication\")\n .action(async () => {\n try {\n const resolved = await resolveAuthToken();\n\n if (!resolved) {\n log.warn('Not logged in. Run \"blodemd login\" to authenticate.');\n return;\n }\n\n if (resolved.source === \"environment\") {\n log.info(\"Authenticated via BLODEMD_API_KEY environment variable\");\n return;\n }\n\n // API keys have no expiry and no user info from JWT\n if (!resolved.expiresAt && !resolved.user) {\n const prefix =\n resolved.token.split(\".\")[0] ?? resolved.token.slice(0, 12);\n log.info(`Logged in with API key ${chalk.cyan(prefix)}`);\n return;\n }\n\n const status = resolveTokenStatus(resolved);\n\n const email =\n resolved.user?.email ??\n (await fetchUserEmail(\n process.env[BLODE_API_URL_ENV] ?? DEFAULT_API_URL,\n resolved.token\n ));\n\n if (email) {\n log.info(`Logged in as ${chalk.cyan(email)}`);\n } else {\n log.info(\"Logged in (could not fetch user details).\");\n }\n\n if (resolved.expiresAt) {\n if (status.expired) {\n log.warn(\n 'Session has expired. Run \"blodemd login\" to re-authenticate.'\n );\n } else if (status.expiresInSeconds !== null) {\n const hours = Math.floor(status.expiresInSeconds / 3600);\n const minutes = Math.floor((status.expiresInSeconds % 3600) / 60);\n log.info(`Session expires in ${hours}h ${minutes}m`);\n }\n }\n } catch (error: unknown) {\n reportCommandError(\"Whoami failed\", error);\n }\n });\n\n// init\n\nprogram\n .command(\"init\")\n .description(\"Scaffold a docs folder\")\n .argument(\"[dir]\", \"target directory\", \"docs\")\n .action(async (dir: string) => {\n intro(chalk.bold(\"blodemd init\"));\n\n try {\n const root = path.resolve(process.cwd(), dir);\n await fs.mkdir(root, { recursive: true });\n\n const docsJson = {\n $schema: \"https://mintlify.com/docs.json\",\n colors: { primary: \"#0D9373\" },\n name: \"my-project\",\n navigation: {\n groups: [{ group: \"Getting Started\", pages: [\"index\"] }],\n },\n theme: \"mint\",\n };\n\n await ensureFile(\n path.join(root, CONFIG_FILE),\n `${JSON.stringify(docsJson, null, 2)}\\n`\n );\n await ensureFile(\n path.join(root, \"index.mdx\"),\n \"---\\ntitle: Welcome\\n---\\n\\nStart writing your docs here.\\n\"\n );\n\n log.success(`Docs scaffolded in ${chalk.cyan(root)}`);\n log.info(`Set ${chalk.cyan(\"name\")} in docs.json to your project slug.`);\n log.info(\"Done\");\n } catch (error: unknown) {\n reportCommandError(\"Init failed\", error);\n }\n });\n\n// validate\n\nprogram\n .command(\"validate\")\n .description(\"Validate docs.json\")\n .argument(\"[dir]\", \"docs directory\")\n .action(async (dir?: string) => {\n intro(chalk.bold(\"blodemd validate\"));\n\n try {\n const root = await resolveDocsRoot(dir);\n await readConfig(root);\n log.success(`${chalk.cyan(CONFIG_FILE)} is valid.`);\n log.info(\"Done\");\n } catch (error: unknown) {\n reportCommandError(\"Validation failed\", error);\n }\n });\n\n// push\n\nprogram\n .command(\"push\")\n .description(\"Deploy docs\")\n .argument(\"[dir]\", \"docs directory\")\n .option(\"--project <slug>\", \"project slug (env: BLODEMD_PROJECT)\")\n .option(\"--api-url <url>\", \"API URL (env: BLODEMD_API_URL)\")\n .option(\"--api-key <token>\", \"API key (env: BLODEMD_API_KEY)\")\n .option(\"--branch <name>\", \"git branch (env: BLODEMD_BRANCH)\")\n .option(\"--message <msg>\", \"deploy message (env: BLODEMD_COMMIT_MESSAGE)\")\n .action(\n async (\n dir: string | undefined,\n options: {\n apiKey?: string;\n apiUrl?: string;\n branch?: string;\n message?: string;\n project?: string;\n }\n ) => {\n intro(chalk.bold(\"blodemd push\"));\n const s = spinner();\n\n try {\n const root = await resolveDocsRoot(dir);\n\n s.start(\"Validating configuration\");\n const config = await readConfig(root);\n s.stop(\"Configuration valid\");\n\n const { project, apiUrl, authToken, branch, commitMessage } =\n await resolvePushConfig(config, options);\n\n s.start(\"Collecting files\");\n const files = await collectFiles(root);\n if (files.length === 0) {\n throw new Error(\"No files found to deploy.\");\n }\n s.stop(`Found ${chalk.cyan(String(files.length))} files`);\n\n const headers = {\n Authorization: `Bearer ${authToken}`,\n \"Content-Type\": \"application/json\",\n };\n\n const apiPath = (suffix: string): string =>\n new URL(\n `/projects/slug/${project}/deployments${suffix}`,\n apiUrl\n ).toString();\n\n const createDeploymentBody = JSON.stringify({ branch, commitMessage });\n\n // Try creating the deployment — if 404, offer to create the project\n s.start(\"Creating deployment\");\n let deployment: DeploymentResponse;\n try {\n deployment = await requestJson<DeploymentResponse>(\n apiPath(\"\"),\n { body: createDeploymentBody, headers, method: \"POST\" },\n \"Failed to create deployment\"\n );\n } catch (error: unknown) {\n const errorMessage = error instanceof Error ? error.message : \"\";\n if (!errorMessage.includes(\"404\")) {\n throw error;\n }\n\n s.stop(\"Project not found\");\n\n const created = await autoCreateProject(project, apiUrl, headers);\n if (!created) {\n log.info(\"Cancelled\");\n return;\n }\n\n s.start(\"Creating deployment\");\n deployment = await requestJson<DeploymentResponse>(\n apiPath(\"\"),\n { body: createDeploymentBody, headers, method: \"POST\" },\n \"Failed to create deployment\"\n );\n }\n s.stop(`Deployment ${chalk.cyan(deployment.id)} created`);\n\n await uploadFiles(files, root, apiPath, deployment.id, headers, s);\n\n s.start(\"Finalizing deployment\");\n const finalized = await requestJson<DeploymentResponse>(\n apiPath(`/${deployment.id}/finalize`),\n {\n body: JSON.stringify({ promote: true }),\n headers,\n method: \"POST\",\n },\n \"Failed to finalize deployment\"\n );\n s.stop(\"Deployment finalized\");\n\n log.success(`Published ${chalk.cyan(finalized.id)}`);\n if (finalized.manifestUrl) {\n log.info(`Manifest: ${finalized.manifestUrl}`);\n }\n if (typeof finalized.fileCount === \"number\") {\n log.info(`Files: ${finalized.fileCount}`);\n }\n\n log.info(\"Done\");\n } catch (error: unknown) {\n s.stop(\"Failed\");\n reportCommandError(\"Push failed\", error);\n }\n }\n );\n\n// dev\n\nprogram\n .command(\"dev\")\n .description(\"Start the docs dev server\")\n .action(() => {\n intro(chalk.bold(\"blodemd dev\"));\n log.info(\n `Run ${chalk.cyan(\"npm run dev --filter=docs\")} from the repo root.`\n );\n log.info(\n `Then open ${chalk.cyan(\"http://localhost:3001\")} to view the docs site.`\n );\n log.info(\"Done\");\n });\n\nprogram.parse();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["CONFIG_FILE","fileExists","resolveDocsRoot","normalizeRelativePath","parsePositiveInteger","fileExists","delay","resolveDocsRoot"],"sources":["../src/constants.ts","../src/jwt.ts","../src/errors.ts","../src/oauth-token.ts","../src/storage.ts","../src/supabase.ts","../src/auth-session.ts","../src/dev/resolve-root.ts","../src/dev/watcher.ts","../src/dev/command.ts","../src/oauth-callback.ts","../src/pkce.ts","../src/cli.ts"],"sourcesContent":["import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const CLI_NAME = \"blodemd\";\n\nexport const BLODE_TOKEN_ENV = \"BLODEMD_API_KEY\";\nexport const BLODE_API_URL_ENV = \"BLODEMD_API_URL\";\nexport const BLODE_PROJECT_ENV = \"BLODEMD_PROJECT\";\nexport const BLODE_BRANCH_ENV = \"BLODEMD_BRANCH\";\nexport const BLODE_COMMIT_MESSAGE_ENV = \"BLODEMD_COMMIT_MESSAGE\";\n\nexport const DEFAULT_API_URL = \"https://api.blode.md\";\nexport const DEFAULT_SUPABASE_URL = \"https://bwnxwgkgyklzzmpbzuoz.supabase.co\";\n\nexport const OAUTH_CLIENT_ID = \"6b5f9860-fe96-4a83-b1ad-266260523c91\";\n\nexport const DEFAULT_OAUTH_CALLBACK_PORT = 8787;\nexport const DEFAULT_OAUTH_CALLBACK_PATH = \"/auth/callback\";\nexport const DEFAULT_OAUTH_TIMEOUT_SECONDS = 180;\n\nconst getDefaultConfigBaseDir = (): string => {\n if (process.platform === \"win32\") {\n return process.env.APPDATA ?? join(homedir(), \"AppData\", \"Roaming\");\n }\n\n return process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\");\n};\n\nconst configBaseDir = getDefaultConfigBaseDir();\n\nexport const CONFIG_DIR = join(configBaseDir, CLI_NAME);\nexport const CREDENTIALS_FILE = join(CONFIG_DIR, \"credentials.json\");\n","export interface JwtClaims {\n exp?: number;\n email?: string;\n sub?: string;\n}\n\nconst parseJwtBase64Url = (input: string): string => {\n const normalized = input.replaceAll(\"-\", \"+\").replaceAll(\"_\", \"/\");\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, \"=\");\n return Buffer.from(padded, \"base64\").toString(\"utf8\");\n};\n\nexport const parseJwtClaims = (token: string): JwtClaims | null => {\n const parts = token.split(\".\");\n const payloadPart = parts.at(1);\n\n if (!payloadPart) {\n return null;\n }\n\n try {\n const payload = parseJwtBase64Url(payloadPart);\n const parsed = JSON.parse(payload) as unknown;\n\n if (typeof parsed !== \"object\" || parsed === null) {\n return null;\n }\n\n const claims = parsed as Record<string, unknown>;\n\n return {\n email: typeof claims.email === \"string\" ? claims.email : undefined,\n exp: typeof claims.exp === \"number\" ? claims.exp : undefined,\n sub: typeof claims.sub === \"string\" ? claims.sub : undefined,\n };\n } catch {\n return null;\n }\n};\n","export const EXIT_CODES = {\n AUTH_REQUIRED: 4,\n CANCELLED: 2,\n ERROR: 1,\n NETWORK: 5,\n SUCCESS: 0,\n VALIDATION: 3,\n} as const;\n\ntype ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];\n\nexport class CliError extends Error {\n readonly exitCode: ExitCode;\n readonly hint: string | null;\n\n constructor(\n message: string,\n exitCode: ExitCode = EXIT_CODES.ERROR,\n hint?: string\n ) {\n super(message);\n this.name = \"CliError\";\n this.exitCode = exitCode;\n this.hint = hint ?? null;\n }\n}\n\nexport const toCliError = (error: unknown): CliError => {\n if (error instanceof CliError) {\n return error;\n }\n\n if (error instanceof Error) {\n if (error instanceof TypeError && error.message.includes(\"fetch\")) {\n return new CliError(\n \"Cannot connect to Blode.md API.\",\n EXIT_CODES.NETWORK,\n \"Check your internet connection and API URL configuration.\"\n );\n }\n\n if (error.name === \"TimeoutError\" || error.name === \"AbortError\") {\n return new CliError(\n \"Request timed out.\",\n EXIT_CODES.NETWORK,\n \"The API may be unavailable. Try again later.\"\n );\n }\n\n return new CliError(error.message, EXIT_CODES.ERROR);\n }\n\n return new CliError(\"Unknown error\", EXIT_CODES.ERROR);\n};\n","import { CliError, EXIT_CODES } from \"./errors.js\";\n\nexport interface OAuthTokenConfig {\n tokenUrl: string;\n clientId: string;\n}\n\nexport interface OAuthTokenResponse {\n access_token: string;\n refresh_token?: string;\n token_type: string;\n expires_in: number;\n}\n\nconst postTokenRequest = async (\n url: string,\n body: URLSearchParams\n): Promise<OAuthTokenResponse> => {\n const response = await fetch(url, {\n body: body.toString(),\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n method: \"POST\",\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n throw new CliError(\n `OAuth token request failed (${response.status}): ${text}`,\n EXIT_CODES.AUTH_REQUIRED\n );\n }\n\n return (await response.json()) as OAuthTokenResponse;\n};\n\nexport const exchangeAuthorizationCode = (\n config: OAuthTokenConfig,\n code: string,\n codeVerifier: string,\n redirectUri: string\n): Promise<OAuthTokenResponse> => {\n const body = new URLSearchParams({\n client_id: config.clientId,\n code,\n code_verifier: codeVerifier,\n grant_type: \"authorization_code\",\n redirect_uri: redirectUri,\n });\n\n return postTokenRequest(config.tokenUrl, body);\n};\n\nexport const refreshAccessToken = (\n config: OAuthTokenConfig,\n refreshToken: string\n): Promise<OAuthTokenResponse> => {\n const body = new URLSearchParams({\n client_id: config.clientId,\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n });\n\n return postTokenRequest(config.tokenUrl, body);\n};\n","import { mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\n\nimport { CONFIG_DIR, CREDENTIALS_FILE } from \"./constants.js\";\nimport { CliError, EXIT_CODES } from \"./errors.js\";\nimport type {\n ApiKeyCredentials,\n AuthFileData,\n StoredAuthSession,\n} from \"./types.js\";\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\nconst parseStoredAuthSession = (value: unknown): StoredAuthSession | null => {\n if (!isRecord(value)) {\n return null;\n }\n\n if (typeof value.accessToken !== \"string\") {\n return null;\n }\n\n if (value.refreshToken !== null && typeof value.refreshToken !== \"string\") {\n return null;\n }\n\n if (value.expiresAt !== null && typeof value.expiresAt !== \"string\") {\n return null;\n }\n\n const { user } = value;\n if (\n user !== null &&\n (!isRecord(user) ||\n typeof user.id !== \"string\" ||\n (user.email !== null && typeof user.email !== \"string\"))\n ) {\n return null;\n }\n\n if (typeof value.createdAt !== \"string\") {\n return null;\n }\n\n const parsedUser =\n user === null || !isRecord(user)\n ? null\n : {\n email: (user.email as string | null) ?? null,\n id: user.id as string,\n };\n\n return {\n accessToken: value.accessToken,\n createdAt: value.createdAt,\n expiresAt: (value.expiresAt as string | null) ?? null,\n refreshToken: (value.refreshToken as string | null) ?? null,\n user: parsedUser,\n };\n};\n\nconst parseApiKeyCredentials = (value: unknown): ApiKeyCredentials | null => {\n if (!isRecord(value)) {\n return null;\n }\n\n if (typeof value.apiKey !== \"string\") {\n return null;\n }\n\n return { apiKey: value.apiKey, type: \"api-key\" };\n};\n\nexport const readAuthFile = async (): Promise<AuthFileData | null> => {\n try {\n const raw = await readFile(CREDENTIALS_FILE, \"utf8\");\n const parsed = JSON.parse(raw) as unknown;\n\n if (!isRecord(parsed) || parsed.version !== 1) {\n throw new CliError(\n `Invalid credentials format in ${CREDENTIALS_FILE}`,\n EXIT_CODES.ERROR\n );\n }\n\n return {\n apiKey: parseApiKeyCredentials(parsed.apiKey) ?? undefined,\n session: parseStoredAuthSession(parsed.session) ?? undefined,\n version: 1,\n };\n } catch (error) {\n if (isRecord(error) && error.code === \"ENOENT\") {\n return null;\n }\n\n if (error instanceof CliError) {\n throw error;\n }\n\n return null;\n }\n};\n\nexport const readStoredAuthSession =\n async (): Promise<StoredAuthSession | null> => {\n const data = await readAuthFile();\n return data?.session ?? null;\n };\n\nexport const readStoredApiKey = async (): Promise<ApiKeyCredentials | null> => {\n const data = await readAuthFile();\n return data?.apiKey ?? null;\n};\n\nconst writeAuthFile = async (data: AuthFileData): Promise<void> => {\n await mkdir(CONFIG_DIR, { mode: 0o700, recursive: true });\n await writeFile(CREDENTIALS_FILE, `${JSON.stringify(data, null, 2)}\\n`, {\n encoding: \"utf8\",\n mode: 0o600,\n });\n};\n\nexport const writeStoredAuthSession = async (\n session: StoredAuthSession\n): Promise<void> => {\n await writeAuthFile({\n session,\n version: 1,\n });\n};\n\nexport const writeStoredApiKey = async (\n apiKey: ApiKeyCredentials\n): Promise<void> => {\n await writeAuthFile({\n apiKey,\n version: 1,\n });\n};\n\nexport const clearStoredCredentials = async (): Promise<void> => {\n await rm(CREDENTIALS_FILE, { force: true });\n};\n","import { DEFAULT_SUPABASE_URL } from \"./constants.js\";\nimport { parseJwtClaims } from \"./jwt.js\";\nimport type { OAuthTokenResponse } from \"./oauth-token.js\";\nimport type { StoredAuthSession, SupabaseConfig } from \"./types.js\";\n\nexport const resolveSupabaseConfig = (): SupabaseConfig => {\n const url =\n process.env.SUPABASE_URL ??\n process.env.NEXT_PUBLIC_SUPABASE_URL ??\n DEFAULT_SUPABASE_URL;\n\n return { url };\n};\n\nexport const buildOAuthUrls = (\n config: SupabaseConfig\n): {\n authorizeUrl: string;\n tokenUrl: string;\n} => ({\n authorizeUrl: `${config.url}/auth/v1/oauth/authorize`,\n tokenUrl: `${config.url}/auth/v1/oauth/token`,\n});\n\nexport const tokenResponseToStoredSession = (\n response: OAuthTokenResponse\n): StoredAuthSession => {\n const claims = parseJwtClaims(response.access_token);\n\n let expiresAt: string | null = null;\n if (typeof claims?.exp === \"number\") {\n expiresAt = new Date(claims.exp * 1000).toISOString();\n } else if (response.expires_in > 0) {\n expiresAt = new Date(Date.now() + response.expires_in * 1000).toISOString();\n }\n\n return {\n accessToken: response.access_token,\n createdAt: new Date().toISOString(),\n expiresAt,\n refreshToken: response.refresh_token ?? null,\n user:\n claims?.sub || claims?.email\n ? {\n email: claims.email ?? null,\n id: claims.sub ?? \"unknown\",\n }\n : null,\n };\n};\n","import { BLODE_TOKEN_ENV, OAUTH_CLIENT_ID } from \"./constants.js\";\nimport { parseJwtClaims } from \"./jwt.js\";\nimport { refreshAccessToken } from \"./oauth-token.js\";\nimport {\n clearStoredCredentials,\n readAuthFile,\n writeStoredAuthSession,\n} from \"./storage.js\";\nimport {\n buildOAuthUrls,\n resolveSupabaseConfig,\n tokenResponseToStoredSession,\n} from \"./supabase.js\";\nimport type { ResolvedAuthToken, StoredAuthSession } from \"./types.js\";\n\nconst expiresInMs = (session: StoredAuthSession): number | null => {\n if (!session.expiresAt) {\n return null;\n }\n\n const expiresAtMs = Date.parse(session.expiresAt);\n\n if (Number.isNaN(expiresAtMs)) {\n return null;\n }\n\n return expiresAtMs - Date.now();\n};\n\nconst isExpired = (session: StoredAuthSession): boolean => {\n const ms = expiresInMs(session);\n return ms !== null && ms <= 0;\n};\n\nconst shouldRefresh = (session: StoredAuthSession): boolean => {\n const ms = expiresInMs(session);\n return ms !== null && ms <= 60_000;\n};\n\nconst tokenFromRaw = (\n token: string,\n source: ResolvedAuthToken[\"source\"]\n): ResolvedAuthToken => {\n const claims = parseJwtClaims(token);\n\n const expiresAt =\n typeof claims?.exp === \"number\"\n ? new Date(claims.exp * 1000).toISOString()\n : null;\n\n return {\n expiresAt,\n source,\n token,\n user:\n claims?.sub || claims?.email\n ? { email: claims.email ?? null, id: claims.sub ?? \"unknown\" }\n : null,\n };\n};\n\nconst sessionToResolvedToken = (\n session: StoredAuthSession\n): ResolvedAuthToken => ({\n expiresAt: session.expiresAt,\n source: \"stored\",\n token: session.accessToken,\n user: session.user,\n});\n\nexport const resolveAuthToken = async (\n optApiKey?: string\n): Promise<ResolvedAuthToken | null> => {\n const envToken = (optApiKey ?? process.env[BLODE_TOKEN_ENV])?.trim();\n\n if (envToken) {\n return tokenFromRaw(envToken, optApiKey ? \"flag\" : \"environment\");\n }\n\n const data = await readAuthFile();\n const session = data?.session;\n\n if (session) {\n if (!(shouldRefresh(session) || isExpired(session))) {\n return sessionToResolvedToken(session);\n }\n\n if (session.refreshToken) {\n try {\n const config = resolveSupabaseConfig();\n const { tokenUrl } = buildOAuthUrls(config);\n const tokenResponse = await refreshAccessToken(\n { clientId: OAUTH_CLIENT_ID, tokenUrl },\n session.refreshToken\n );\n const updatedSession = tokenResponseToStoredSession(tokenResponse);\n await writeStoredAuthSession(updatedSession);\n\n return sessionToResolvedToken(updatedSession);\n } catch {\n // Refresh failed — fall through to expiry check\n }\n }\n\n if (isExpired(session)) {\n await clearStoredCredentials();\n return null;\n }\n\n return sessionToResolvedToken(session);\n }\n\n if (data?.apiKey) {\n return {\n expiresAt: null,\n source: \"stored\",\n token: data.apiKey.apiKey,\n user: null,\n };\n }\n\n return null;\n};\n\nexport const resolveTokenStatus = (\n token: ResolvedAuthToken\n): {\n expiresInSeconds: number | null;\n expired: boolean;\n} => {\n if (!token.expiresAt) {\n return { expired: false, expiresInSeconds: null };\n }\n\n const expiresAtMs = Date.parse(token.expiresAt);\n\n if (Number.isNaN(expiresAtMs)) {\n return { expired: false, expiresInSeconds: null };\n }\n\n const expiresInSeconds = Math.floor((expiresAtMs - Date.now()) / 1000);\n\n return {\n expired: expiresInSeconds <= 0,\n expiresInSeconds,\n };\n};\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { createFsSource, loadSiteConfig } from \"@repo/previewing\";\n\nimport { CliError, EXIT_CODES } from \"../errors.js\";\n\nconst CONFIG_FILE = \"docs.json\";\n\nconst fileExists = async (filePath: string): Promise<boolean> => {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n};\n\nexport const resolveDocsRoot = async (dir?: string): Promise<string> => {\n if (dir) {\n return path.resolve(process.cwd(), dir);\n }\n\n const candidates = [\n process.cwd(),\n path.join(process.cwd(), \"docs\"),\n path.join(process.cwd(), \"apps/docs\"),\n ];\n\n for (const candidate of candidates) {\n if (await fileExists(path.join(candidate, CONFIG_FILE))) {\n return candidate;\n }\n }\n\n return process.cwd();\n};\n\nexport const validateDocsRoot = async (root: string) => {\n const result = await loadSiteConfig(createFsSource(root));\n\n if (!result.ok) {\n throw new CliError(\n result.errors.join(\"\\n\"),\n EXIT_CODES.VALIDATION,\n `Make sure ${CONFIG_FILE} exists and is valid JSON.`\n );\n }\n\n return result;\n};\n","import path from \"node:path\";\n\nimport { log } from \"@clack/prompts\";\nimport { watch } from \"chokidar\";\n\nconst INVALIDATE_ENDPOINT = \"/blodemd-dev/invalidate\";\nconst WATCH_DEBOUNCE_MS = 100;\n\nconst normalizeRelativePath = (root: string, filePath: string) =>\n path.relative(root, filePath).split(path.sep).join(\"/\");\n\nconst isDirectoryEvent = (event: string) =>\n event === \"addDir\" || event === \"unlinkDir\";\n\nexport const createDevWatcher = ({\n port,\n root,\n}: {\n port: number;\n root: string;\n}) => {\n const watcher = watch(root, {\n ignoreInitial: true,\n ignored: [\"**/.git/**\", \"**/.next/**\", \"**/dist/**\", \"**/node_modules/**\"],\n });\n\n let flushTimer: NodeJS.Timeout | null = null;\n let pendingKind: \"config\" | \"content\" = \"content\";\n const pendingPaths = new Set<string>();\n\n const flush = async () => {\n flushTimer = null;\n\n const paths = [...pendingPaths];\n const kind = pendingKind;\n pendingPaths.clear();\n pendingKind = \"content\";\n\n if (!paths.length) {\n return;\n }\n\n try {\n const response = await fetch(\n `http://127.0.0.1:${port}${INVALIDATE_ENDPOINT}`,\n {\n body: JSON.stringify({ kind, paths }),\n headers: {\n \"Content-Type\": \"application/json\",\n },\n method: \"POST\",\n }\n );\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n } catch (error) {\n log.error(\n `Failed to invalidate preview cache: ${\n error instanceof Error ? error.message : \"unknown error\"\n }`\n );\n }\n };\n\n watcher.on(\"all\", (event, changedPath) => {\n if (isDirectoryEvent(event)) {\n return;\n }\n\n const relativePath = normalizeRelativePath(root, changedPath);\n pendingPaths.add(relativePath);\n\n if (path.basename(changedPath) === \"docs.json\") {\n pendingKind = \"config\";\n }\n\n if (flushTimer) {\n clearTimeout(flushTimer);\n }\n\n flushTimer = setTimeout(() => {\n flush();\n }, WATCH_DEBOUNCE_MS);\n });\n\n return {\n async close() {\n if (flushTimer) {\n clearTimeout(flushTimer);\n await flush();\n }\n\n await watcher.close();\n },\n };\n};\n","import { spawn } from \"node:child_process\";\nimport { once } from \"node:events\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { intro, log } from \"@clack/prompts\";\nimport chalk from \"chalk\";\nimport open from \"open\";\n\nimport { CliError, EXIT_CODES, toCliError } from \"../errors.js\";\nimport { resolveDocsRoot, validateDocsRoot } from \"./resolve-root.js\";\nimport { createDevWatcher } from \"./watcher.js\";\n\nconst DEV_READY_ENDPOINT = \"/blodemd-dev/version\";\nconst DEV_READY_TIMEOUT_MS = 45_000;\n\nconst parsePositiveInteger = (value: string, label: string): number => {\n const parsed = Number.parseInt(value, 10);\n\n if (!Number.isInteger(parsed) || parsed <= 0) {\n throw new CliError(\n `${label} must be a positive integer.`,\n EXIT_CODES.VALIDATION\n );\n }\n\n return parsed;\n};\n\nconst fileExists = async (filePath: string): Promise<boolean> => {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n};\n\nconst findMonorepoRoot = async (start: string): Promise<string> => {\n let current = start;\n\n while (true) {\n const packageJsonPath = path.join(current, \"package.json\");\n if (await fileExists(packageJsonPath)) {\n const raw = await fs.readFile(packageJsonPath, \"utf8\");\n const parsed = JSON.parse(raw) as { workspaces?: string[] };\n const workspaces = parsed.workspaces ?? [];\n\n if (workspaces.includes(\"apps/*\") && workspaces.includes(\"packages/*\")) {\n return current;\n }\n }\n\n const parent = path.dirname(current);\n if (parent === current) {\n break;\n }\n current = parent;\n }\n\n throw new CliError(\n \"Could not locate the blodemd monorepo root.\",\n EXIT_CODES.ERROR,\n \"The monorepo-only dev server must be run from this repository checkout.\"\n );\n};\n\nconst waitForServer = async ({\n child,\n port,\n}: {\n child: ReturnType<typeof spawn>;\n port: number;\n}) => {\n const url = `http://localhost:${port}${DEV_READY_ENDPOINT}`;\n const startedAt = Date.now();\n\n while (Date.now() - startedAt < DEV_READY_TIMEOUT_MS) {\n if (child.exitCode !== null) {\n throw new CliError(\n \"The local dev server exited before it became ready.\",\n EXIT_CODES.ERROR\n );\n }\n\n try {\n const response = await fetch(url, {\n cache: \"no-store\",\n headers: {\n accept: \"application/json\",\n },\n });\n\n if (response.ok) {\n return;\n }\n } catch {\n // Server is still starting.\n }\n\n await delay(500);\n }\n\n throw new CliError(\n \"Timed out waiting for the local dev server to start.\",\n EXIT_CODES.ERROR\n );\n};\n\nconst npmCommand = process.platform === \"win32\" ? \"npm.cmd\" : \"npm\";\n\nexport const devCommand = async ({\n dir,\n openBrowser,\n port: portValue,\n}: {\n dir?: string;\n openBrowser: boolean;\n port: string;\n}) => {\n intro(chalk.bold(\"blodemd dev\"));\n\n try {\n const port = parsePositiveInteger(portValue, \"Port\");\n const root = await resolveDocsRoot(dir);\n await validateDocsRoot(root);\n\n const cliFilePath = fileURLToPath(import.meta.url);\n const repoRoot = await findMonorepoRoot(path.dirname(cliFilePath));\n const localUrl = `http://localhost:${port}`;\n\n log.info(`Docs root: ${chalk.cyan(root)}`);\n\n const child = spawn(npmCommand, [\"run\", \"dev\", \"--workspace=dev-server\"], {\n cwd: repoRoot,\n env: {\n ...process.env,\n DOCS_ROOT: root,\n PORT: String(port),\n },\n stdio: \"inherit\",\n });\n\n let watcher: Awaited<ReturnType<typeof createDevWatcher>> | null = null;\n let shuttingDown = false;\n\n const closeAll = async () => {\n if (shuttingDown) {\n return;\n }\n shuttingDown = true;\n\n if (watcher) {\n await watcher.close();\n watcher = null;\n }\n\n if (child.exitCode === null && !child.killed) {\n child.kill(\"SIGTERM\");\n }\n };\n\n const onSignal = async () => {\n await closeAll();\n };\n\n process.once(\"SIGINT\", onSignal);\n process.once(\"SIGTERM\", onSignal);\n\n try {\n await waitForServer({ child, port });\n\n watcher = await createDevWatcher({ port, root });\n log.success(`Dev server running at ${chalk.cyan(localUrl)}`);\n\n if (openBrowser) {\n await open(localUrl);\n }\n\n const [code, signal] = (await once(child, \"exit\")) as [\n number | null,\n NodeJS.Signals | null,\n ];\n\n await closeAll();\n process.removeListener(\"SIGINT\", onSignal);\n process.removeListener(\"SIGTERM\", onSignal);\n\n if (shuttingDown || signal === \"SIGINT\" || signal === \"SIGTERM\") {\n return;\n }\n\n if (code !== 0) {\n throw new CliError(\n `The local dev server exited with code ${code ?? \"unknown\"}.`,\n EXIT_CODES.ERROR\n );\n }\n } catch (error) {\n await closeAll();\n process.removeListener(\"SIGINT\", onSignal);\n process.removeListener(\"SIGTERM\", onSignal);\n throw error;\n }\n } catch (error) {\n const cliError = toCliError(error);\n\n log.error(cliError.message);\n if (cliError.hint) {\n log.info(cliError.hint);\n }\n\n process.exitCode = cliError.exitCode;\n }\n};\n","// oxlint-disable no-use-before-define -- circular reference in callback pattern\nimport { createServer } from \"node:http\";\nimport type { Socket } from \"node:net\";\n\nimport { CliError, EXIT_CODES } from \"./errors.js\";\n\ninterface OAuthCallbackOptions {\n redirectUrl: URL;\n expectedState: string;\n timeoutMs: number;\n}\n\nconst SUCCESS_HTML =\n '<!doctype html><html><head><meta charset=\"utf-8\"/><title>Blode.md CLI</title></head><body><h2>Logged in! You can close this tab.</h2></body></html>';\n\nconst escapeHtml = (text: string): string =>\n text\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\");\n\nconst errorHtml = (message: string): string =>\n `<!doctype html><html><head><meta charset=\"utf-8\"/><title>Blode.md CLI</title></head><body><h2>Login failed</h2><p>${escapeHtml(message)}</p></body></html>`;\n\nexport const waitForOAuthCode = (\n options: OAuthCallbackOptions\n): Promise<string> => {\n const host = options.redirectUrl.hostname;\n const port = Number(options.redirectUrl.port);\n const { pathname } = options.redirectUrl;\n\n if (!Number.isInteger(port) || port <= 0) {\n return Promise.reject(\n new CliError(\n \"OAuth redirect URL requires an explicit port\",\n EXIT_CODES.ERROR\n )\n );\n }\n\n // oxlint-disable-next-line eslint-plugin-promise/avoid-new -- wrapping callback-based HTTP server\n return new Promise<string>((resolve, reject) => {\n let settled = false;\n const sockets = new Set<Socket>();\n\n const settle = (ok: boolean, value: string | CliError): void => {\n if (settled) {\n return;\n }\n\n settled = true;\n clearTimeout(timer);\n\n httpServer.close(() => {\n if (ok) {\n resolve(value as string);\n } else {\n reject(value);\n }\n });\n\n // Destroy kept-alive connections so httpServer.close() can finish\n for (const socket of sockets) {\n socket.destroy();\n }\n };\n\n const httpServer = createServer((request, response) => {\n if (!request.url) {\n response.writeHead(400, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(errorHtml(\"Missing request URL\"));\n settle(\n false,\n new CliError(\n \"OAuth callback is missing a request URL\",\n EXIT_CODES.ERROR\n )\n );\n return;\n }\n\n const url = new URL(request.url, options.redirectUrl.origin);\n\n if (url.pathname !== pathname) {\n response.writeHead(404, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(errorHtml(\"Invalid callback path\"));\n return;\n }\n\n const providerError = url.searchParams.get(\"error\");\n if (providerError) {\n const description =\n url.searchParams.get(\"error_description\") ?? providerError;\n\n response.writeHead(400, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(errorHtml(description));\n\n settle(\n false,\n new CliError(\n `OAuth provider returned an error: ${description}`,\n EXIT_CODES.ERROR\n )\n );\n return;\n }\n\n const state = url.searchParams.get(\"state\");\n if (state !== options.expectedState) {\n response.writeHead(400, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(errorHtml(\"State verification failed\"));\n\n settle(\n false,\n new CliError(\"OAuth state verification failed\", EXIT_CODES.ERROR)\n );\n return;\n }\n\n const code = url.searchParams.get(\"code\");\n if (!code) {\n response.writeHead(400, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(errorHtml(\"Authorization code was missing\"));\n\n settle(\n false,\n new CliError(\n \"OAuth callback is missing an authorization code\",\n EXIT_CODES.ERROR\n )\n );\n return;\n }\n\n response.writeHead(200, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(SUCCESS_HTML);\n settle(true, code);\n });\n\n httpServer.on(\"connection\", (socket) => {\n sockets.add(socket);\n socket.once(\"close\", () => sockets.delete(socket));\n });\n\n httpServer.on(\"error\", (error) => {\n settle(\n false,\n new CliError(\n `Failed to start callback server on ${host}:${port}: ${error.message}`,\n EXIT_CODES.ERROR\n )\n );\n });\n\n const timer = setTimeout(() => {\n settle(\n false,\n new CliError(\"Login timed out. Please try again.\", EXIT_CODES.CANCELLED)\n );\n }, options.timeoutMs);\n\n httpServer.listen(port, host);\n });\n};\n","import { createHash, randomBytes } from \"node:crypto\";\n\nexport const createOAuthState = (): string => randomBytes(24).toString(\"hex\");\n\nexport const createCodeVerifier = (): string =>\n randomBytes(64).toString(\"base64url\");\n\nexport const createCodeChallenge = (verifier: string): string =>\n createHash(\"sha256\").update(verifier).digest().toString(\"base64url\");\n","import { spawnSync } from \"node:child_process\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport {\n confirm,\n intro,\n isCancel,\n log,\n password,\n spinner,\n} from \"@clack/prompts\";\nimport chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport open from \"open\";\n\nimport { resolveAuthToken, resolveTokenStatus } from \"./auth-session.js\";\nimport {\n BLODE_API_URL_ENV,\n BLODE_BRANCH_ENV,\n BLODE_COMMIT_MESSAGE_ENV,\n BLODE_PROJECT_ENV,\n DEFAULT_API_URL,\n DEFAULT_OAUTH_CALLBACK_PATH,\n DEFAULT_OAUTH_CALLBACK_PORT,\n DEFAULT_OAUTH_TIMEOUT_SECONDS,\n OAUTH_CLIENT_ID,\n} from \"./constants.js\";\nimport { devCommand } from \"./dev/command.js\";\nimport { CliError, EXIT_CODES, toCliError } from \"./errors.js\";\nimport { waitForOAuthCode } from \"./oauth-callback.js\";\nimport { exchangeAuthorizationCode } from \"./oauth-token.js\";\nimport {\n createCodeChallenge,\n createCodeVerifier,\n createOAuthState,\n} from \"./pkce.js\";\nimport {\n clearStoredCredentials,\n readAuthFile,\n writeStoredApiKey,\n writeStoredAuthSession,\n} from \"./storage.js\";\nimport {\n buildOAuthUrls,\n resolveSupabaseConfig,\n tokenResponseToStoredSession,\n} from \"./supabase.js\";\nimport type { DeploymentResponse } from \"./types.js\";\n\nconst CONFIG_FILE = \"docs.json\";\n\nconst TEXT_CONTENT_TYPES: Record<string, string> = {\n \".css\": \"text/css; charset=utf-8\",\n \".html\": \"text/html; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".json\": \"application/json; charset=utf-8\",\n \".md\": \"text/markdown; charset=utf-8\",\n \".mdx\": \"text/markdown; charset=utf-8\",\n \".svg\": \"image/svg+xml\",\n \".txt\": \"text/plain; charset=utf-8\",\n \".yaml\": \"application/yaml; charset=utf-8\",\n \".yml\": \"application/yaml; charset=utf-8\",\n};\n\n// --- File helpers ---\n\nconst ensureFile = async (filePath: string, content: string): Promise<void> => {\n try {\n await fs.writeFile(filePath, content, { flag: \"wx\" });\n } catch {\n // File already exists\n }\n};\n\nconst fileExists = async (filePath: string): Promise<boolean> => {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n};\n\nconst readConfig = async (\n root: string\n): Promise<{ name?: string; raw: string }> => {\n const raw = await fs.readFile(path.join(root, CONFIG_FILE), \"utf8\");\n const parsed = JSON.parse(raw) as { name?: string };\n return { name: parsed.name, raw };\n};\n\nconst resolveDocsRoot = async (dir?: string): Promise<string> => {\n if (dir) {\n return path.resolve(process.cwd(), dir);\n }\n\n const candidates = [\n process.cwd(),\n path.join(process.cwd(), \"docs\"),\n path.join(process.cwd(), \"apps/docs\"),\n ];\n\n for (const candidate of candidates) {\n if (await fileExists(path.join(candidate, CONFIG_FILE))) {\n return candidate;\n }\n }\n\n return process.cwd();\n};\n\nconst readGitValue = (gitArgs: string[]): string | undefined => {\n const result = spawnSync(\"git\", gitArgs, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n });\n\n if (result.status !== 0) {\n return;\n }\n\n const value = result.stdout.trim();\n return value || undefined;\n};\n\nconst normalizeRelativePath = (root: string, filePath: string): string =>\n path.relative(root, filePath).split(path.sep).join(\"/\");\n\nconst shouldSkipEntry = (name: string): boolean =>\n name.startsWith(\".\") || name === \"node_modules\";\n\nconst collectFiles = async (root: string): Promise<string[]> => {\n const entries = await fs.readdir(root, { withFileTypes: true });\n const files: string[] = [];\n\n for (const entry of entries) {\n if (shouldSkipEntry(entry.name)) {\n continue;\n }\n\n const absolutePath = path.join(root, entry.name);\n if (entry.isDirectory()) {\n files.push(...(await collectFiles(absolutePath)));\n continue;\n }\n\n if (entry.isFile()) {\n files.push(absolutePath);\n }\n }\n\n return files.toSorted((left, right) => left.localeCompare(right));\n};\n\nconst getContentType = (filePath: string): string =>\n TEXT_CONTENT_TYPES[path.extname(filePath).toLowerCase()] ??\n \"application/octet-stream\";\n\nconst readJson = async (response: Response): Promise<unknown> => {\n const text = await response.text();\n if (!text) {\n return null;\n }\n\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n};\n\nconst requestJson = async <T>(\n url: string,\n init: RequestInit,\n message: string\n): Promise<T> => {\n const response = await fetch(url, init);\n const data = await readJson(response);\n if (!response.ok) {\n const detail =\n typeof data === \"string\" ? data : JSON.stringify(data ?? {}, null, 2);\n throw new Error(`${message}: ${response.status} ${detail}`);\n }\n\n return data as T;\n};\n\nconst parsePositiveInteger = (value: string, label: string): number => {\n const parsed = Number.parseInt(value, 10);\n\n if (!Number.isInteger(parsed) || parsed <= 0) {\n throw new CliError(\n `${label} must be a positive integer.`,\n EXIT_CODES.VALIDATION\n );\n }\n\n return parsed;\n};\n\nconst reportCommandError = (prefix: string, error: unknown): void => {\n const cliError = toCliError(error);\n\n log.error(`${prefix}: ${cliError.message}`);\n if (cliError.hint) {\n log.info(cliError.hint);\n }\n log.info(\"Failed\");\n process.exitCode = cliError.exitCode;\n};\n\n// --- Auth helpers ---\n\nconst fetchUserEmail = async (\n apiUrl: string,\n token: string\n): Promise<string | null> => {\n try {\n const user = await requestJson<{ email: string }>(\n `${apiUrl}/auth/me`,\n { headers: { Authorization: `Bearer ${token}` } },\n \"Failed to fetch user info\"\n );\n return user.email;\n } catch {\n return null;\n }\n};\n\n// --- Push helpers ---\n\ninterface PushConfig {\n project: string;\n apiUrl: string;\n authToken: string;\n branch: string;\n commitMessage?: string;\n}\n\nconst resolvePushConfig = async (\n config: { name?: string },\n options: {\n apiKey?: string;\n apiUrl?: string;\n branch?: string;\n message?: string;\n project?: string;\n }\n): Promise<PushConfig> => {\n const project =\n options.project ?? process.env[BLODE_PROJECT_ENV] ?? config.name;\n const apiUrl =\n options.apiUrl ?? process.env[BLODE_API_URL_ENV] ?? DEFAULT_API_URL;\n\n const resolved = await resolveAuthToken(options.apiKey);\n const authToken = resolved?.token;\n\n const branch =\n options.branch ??\n process.env[BLODE_BRANCH_ENV] ??\n process.env.GITHUB_REF_NAME ??\n readGitValue([\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]) ??\n \"main\";\n const commitMessage =\n options.message ??\n process.env[BLODE_COMMIT_MESSAGE_ENV] ??\n readGitValue([\"log\", \"-1\", \"--pretty=%s\"]);\n\n if (!project) {\n throw new Error(\n 'Missing project slug. Set \"name\" in docs.json, pass --project, or set BLODEMD_PROJECT.'\n );\n }\n if (!authToken) {\n throw new Error(\n 'Missing credentials. Run \"blodemd login\", pass --api-key, or set BLODEMD_API_KEY.'\n );\n }\n\n return { apiUrl, authToken, branch, commitMessage, project };\n};\n\nconst autoCreateProject = async (\n project: string,\n apiUrl: string,\n headers: Record<string, string>\n): Promise<boolean> => {\n const authData = await readAuthFile();\n if (!authData?.session) {\n throw new Error(\n `Project \"${project}\" not found. Create it at blode.md or login with \"blodemd login\" to auto-create.`\n );\n }\n\n const shouldCreate = await confirm({\n message: `Project \"${project}\" doesn't exist. Create it?`,\n });\n\n if (isCancel(shouldCreate) || !shouldCreate) {\n return false;\n }\n\n const createResult = await requestJson<{\n project: { id: string; slug: string };\n token: string;\n }>(\n new URL(\"/projects\", apiUrl).toString(),\n {\n body: JSON.stringify({ name: project, slug: project }),\n headers,\n method: \"POST\",\n },\n \"Failed to create project\"\n );\n\n log.success(`Project ${chalk.cyan(createResult.project.slug)} created`);\n log.info(`API key for CI: ${chalk.dim(createResult.token)}`);\n return true;\n};\n\nconst uploadFiles = async (\n files: string[],\n root: string,\n apiPath: (suffix: string) => string,\n deploymentId: string,\n headers: Record<string, string>,\n s: ReturnType<typeof spinner>\n) => {\n s.start(`Uploading ${files.length} files`);\n for (const [index, filePath] of files.entries()) {\n const relativePath = normalizeRelativePath(root, filePath);\n const content = await fs.readFile(filePath);\n\n await requestJson(\n apiPath(`/${deploymentId}/files`),\n {\n body: JSON.stringify({\n contentBase64: content.toString(\"base64\"),\n contentType: getContentType(filePath),\n path: relativePath,\n }),\n headers,\n method: \"POST\",\n },\n `Failed to upload ${relativePath}`\n );\n\n s.message(`Uploading files (${index + 1}/${files.length})`);\n }\n s.stop(`Uploaded ${chalk.cyan(String(files.length))} files`);\n};\n\n// --- CLI ---\n\nconst program = new Command();\n\nprogram.name(\"blodemd\").description(\"Blode.md CLI\").version(\"0.0.3\");\n\n// login\n\nprogram\n .command(\"login\")\n .description(\"Authenticate with Blode.md\")\n .option(\"--token\", \"Paste an API key instead of using browser login\")\n .option(\n \"--port <port>\",\n \"Loopback callback port\",\n String(DEFAULT_OAUTH_CALLBACK_PORT)\n )\n .option(\n \"--timeout <seconds>\",\n \"OAuth timeout in seconds\",\n String(DEFAULT_OAUTH_TIMEOUT_SECONDS)\n )\n .option(\"--no-open\", \"Print URL instead of opening the browser\")\n .action(\n async (options: {\n token?: boolean;\n port: string;\n timeout: string;\n open: boolean;\n }) => {\n intro(chalk.bold(\"blodemd login\"));\n\n try {\n if (options.token) {\n const apiKey = await password({\n message: \"Enter your API key\",\n validate: (value) => {\n if (!value) {\n return \"API key is required.\";\n }\n },\n });\n\n if (isCancel(apiKey)) {\n log.warn(\"Cancelled\");\n return;\n }\n\n await writeStoredApiKey({ apiKey, type: \"api-key\" });\n\n const prefix = apiKey.split(\".\")[0] ?? apiKey.slice(0, 12);\n log.success(`Authenticated as ${chalk.cyan(prefix)}`);\n log.info(\"Done\");\n return;\n }\n\n // OAuth 2.1 authorization code flow with PKCE\n const config = resolveSupabaseConfig();\n const { authorizeUrl, tokenUrl } = buildOAuthUrls(config);\n const clientId = OAUTH_CLIENT_ID;\n\n const port = parsePositiveInteger(options.port, \"Port\");\n const timeoutSeconds = parsePositiveInteger(options.timeout, \"Timeout\");\n const redirectUrl = new URL(\n `http://127.0.0.1:${port}${DEFAULT_OAUTH_CALLBACK_PATH}`\n );\n\n const state = createOAuthState();\n const codeVerifier = createCodeVerifier();\n const codeChallenge = createCodeChallenge(codeVerifier);\n\n const authUrl = new URL(authorizeUrl);\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"client_id\", clientId);\n authUrl.searchParams.set(\"redirect_uri\", redirectUrl.toString());\n authUrl.searchParams.set(\"code_challenge\", codeChallenge);\n authUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n authUrl.searchParams.set(\"state\", state);\n authUrl.searchParams.set(\"scope\", \"openid email profile\");\n\n const callbackPromise = waitForOAuthCode({\n expectedState: state,\n redirectUrl,\n timeoutMs: timeoutSeconds * 1000,\n });\n\n if (options.open) {\n log.info(\"Opening browser for authentication...\");\n log.info(\n `If the browser doesn't open, visit: ${chalk.cyan(authUrl.toString())}`\n );\n await open(authUrl.toString());\n } else {\n log.info(\"Open this URL to continue authentication:\");\n log.info(chalk.cyan(authUrl.toString()));\n }\n\n const code = await callbackPromise;\n\n const tokenResponse = await exchangeAuthorizationCode(\n { clientId, tokenUrl },\n code,\n codeVerifier,\n redirectUrl.toString()\n );\n\n const storedSession = tokenResponseToStoredSession(tokenResponse);\n await writeStoredAuthSession(storedSession);\n\n const email =\n storedSession.user?.email ??\n (await fetchUserEmail(\n process.env[BLODE_API_URL_ENV] ?? DEFAULT_API_URL,\n storedSession.accessToken\n ));\n\n if (email) {\n log.success(`Logged in as ${chalk.cyan(email)}`);\n } else {\n log.success(\"Logged in successfully.\");\n }\n\n log.info(\"Done\");\n } catch (error: unknown) {\n reportCommandError(\"Login failed\", error);\n }\n }\n );\n\n// logout\n\nprogram\n .command(\"logout\")\n .description(\"Remove stored credentials\")\n .action(async () => {\n intro(chalk.bold(\"blodemd logout\"));\n\n try {\n const existing = await readAuthFile();\n await clearStoredCredentials();\n\n if (existing?.session || existing?.apiKey) {\n log.success(\"Credentials removed.\");\n } else {\n log.info(\"No stored credentials found.\");\n }\n log.info(\"Done\");\n } catch (error: unknown) {\n reportCommandError(\"Logout failed\", error);\n }\n });\n\n// whoami\n\nprogram\n .command(\"whoami\")\n .description(\"Show current authentication\")\n .action(async () => {\n try {\n const resolved = await resolveAuthToken();\n\n if (!resolved) {\n log.warn('Not logged in. Run \"blodemd login\" to authenticate.');\n return;\n }\n\n if (resolved.source === \"environment\") {\n log.info(\"Authenticated via BLODEMD_API_KEY environment variable\");\n return;\n }\n\n // API keys have no expiry and no user info from JWT\n if (!resolved.expiresAt && !resolved.user) {\n const prefix =\n resolved.token.split(\".\")[0] ?? resolved.token.slice(0, 12);\n log.info(`Logged in with API key ${chalk.cyan(prefix)}`);\n return;\n }\n\n const status = resolveTokenStatus(resolved);\n\n const email =\n resolved.user?.email ??\n (await fetchUserEmail(\n process.env[BLODE_API_URL_ENV] ?? DEFAULT_API_URL,\n resolved.token\n ));\n\n if (email) {\n log.info(`Logged in as ${chalk.cyan(email)}`);\n } else {\n log.info(\"Logged in (could not fetch user details).\");\n }\n\n if (resolved.expiresAt && status.expired) {\n log.warn(\n 'Session has expired. Run \"blodemd login\" to re-authenticate.'\n );\n }\n } catch (error: unknown) {\n reportCommandError(\"Whoami failed\", error);\n }\n });\n\n// init\n\nprogram\n .command(\"init\")\n .description(\"Scaffold a docs folder\")\n .argument(\"[dir]\", \"target directory\", \"docs\")\n .action(async (dir: string) => {\n intro(chalk.bold(\"blodemd init\"));\n\n try {\n const root = path.resolve(process.cwd(), dir);\n await fs.mkdir(root, { recursive: true });\n\n const docsJson = {\n $schema: \"https://mintlify.com/docs.json\",\n colors: { primary: \"#0D9373\" },\n name: \"my-project\",\n navigation: {\n groups: [{ group: \"Getting Started\", pages: [\"index\"] }],\n },\n theme: \"mint\",\n };\n\n await ensureFile(\n path.join(root, CONFIG_FILE),\n `${JSON.stringify(docsJson, null, 2)}\\n`\n );\n await ensureFile(\n path.join(root, \"index.mdx\"),\n \"---\\ntitle: Welcome\\n---\\n\\nStart writing your docs here.\\n\"\n );\n\n log.success(`Docs scaffolded in ${chalk.cyan(root)}`);\n log.info(`Set ${chalk.cyan(\"name\")} in docs.json to your project slug.`);\n log.info(\"Done\");\n } catch (error: unknown) {\n reportCommandError(\"Init failed\", error);\n }\n });\n\n// validate\n\nprogram\n .command(\"validate\")\n .description(\"Validate docs.json\")\n .argument(\"[dir]\", \"docs directory\")\n .action(async (dir?: string) => {\n intro(chalk.bold(\"blodemd validate\"));\n\n try {\n const root = await resolveDocsRoot(dir);\n await readConfig(root);\n log.success(`${chalk.cyan(CONFIG_FILE)} is valid.`);\n log.info(\"Done\");\n } catch (error: unknown) {\n reportCommandError(\"Validation failed\", error);\n }\n });\n\n// push\n\nprogram\n .command(\"push\")\n .description(\"Deploy docs\")\n .argument(\"[dir]\", \"docs directory\")\n .option(\"--project <slug>\", \"project slug (env: BLODEMD_PROJECT)\")\n .option(\"--api-url <url>\", \"API URL (env: BLODEMD_API_URL)\")\n .option(\"--api-key <token>\", \"API key (env: BLODEMD_API_KEY)\")\n .option(\"--branch <name>\", \"git branch (env: BLODEMD_BRANCH)\")\n .option(\"--message <msg>\", \"deploy message (env: BLODEMD_COMMIT_MESSAGE)\")\n .action(\n async (\n dir: string | undefined,\n options: {\n apiKey?: string;\n apiUrl?: string;\n branch?: string;\n message?: string;\n project?: string;\n }\n ) => {\n intro(chalk.bold(\"blodemd push\"));\n const s = spinner();\n\n try {\n const root = await resolveDocsRoot(dir);\n\n s.start(\"Validating configuration\");\n const config = await readConfig(root);\n s.stop(\"Configuration valid\");\n\n const { project, apiUrl, authToken, branch, commitMessage } =\n await resolvePushConfig(config, options);\n\n s.start(\"Collecting files\");\n const files = await collectFiles(root);\n if (files.length === 0) {\n throw new Error(\"No files found to deploy.\");\n }\n s.stop(`Found ${chalk.cyan(String(files.length))} files`);\n\n const headers = {\n Authorization: `Bearer ${authToken}`,\n \"Content-Type\": \"application/json\",\n };\n\n const apiPath = (suffix: string): string =>\n new URL(\n `/projects/slug/${project}/deployments${suffix}`,\n apiUrl\n ).toString();\n\n const createDeploymentBody = JSON.stringify({ branch, commitMessage });\n\n // Try creating the deployment — if 404, offer to create the project\n s.start(\"Creating deployment\");\n let deployment: DeploymentResponse;\n try {\n deployment = await requestJson<DeploymentResponse>(\n apiPath(\"\"),\n { body: createDeploymentBody, headers, method: \"POST\" },\n \"Failed to create deployment\"\n );\n } catch (error: unknown) {\n const errorMessage = error instanceof Error ? error.message : \"\";\n if (!errorMessage.includes(\"404\")) {\n throw error;\n }\n\n s.stop(\"Project not found\");\n\n const created = await autoCreateProject(project, apiUrl, headers);\n if (!created) {\n log.info(\"Cancelled\");\n return;\n }\n\n s.start(\"Creating deployment\");\n deployment = await requestJson<DeploymentResponse>(\n apiPath(\"\"),\n { body: createDeploymentBody, headers, method: \"POST\" },\n \"Failed to create deployment\"\n );\n }\n s.stop(`Deployment ${chalk.cyan(deployment.id)} created`);\n\n await uploadFiles(files, root, apiPath, deployment.id, headers, s);\n\n s.start(\"Finalizing deployment\");\n const finalized = await requestJson<DeploymentResponse>(\n apiPath(`/${deployment.id}/finalize`),\n {\n body: JSON.stringify({ promote: true }),\n headers,\n method: \"POST\",\n },\n \"Failed to finalize deployment\"\n );\n s.stop(\"Deployment finalized\");\n\n log.success(`Published ${chalk.cyan(finalized.id)}`);\n if (finalized.manifestUrl) {\n log.info(`Manifest: ${finalized.manifestUrl}`);\n }\n if (typeof finalized.fileCount === \"number\") {\n log.info(`Files: ${finalized.fileCount}`);\n }\n\n log.info(\"Done\");\n } catch (error: unknown) {\n s.stop(\"Failed\");\n reportCommandError(\"Push failed\", error);\n }\n }\n );\n\n// dev\n\nprogram\n .command(\"dev\")\n .description(\"Start the local docs dev server\")\n .option(\"-p, --port <port>\", \"Port number\", \"3030\")\n .option(\"-d, --dir <dir>\", \"Docs directory\")\n .option(\"--no-open\", \"Don't open browser\")\n .action(\n async (options: { dir?: string; open?: boolean; port: string }) =>\n await devCommand({\n dir: options.dir,\n openBrowser: options.open ?? true,\n port: options.port,\n })\n );\n\nprogram.parse();\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blodemd",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Blode.md CLI",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@clack/prompts": "^1.0.0",
|
|
35
|
+
"@repo/previewing": "*",
|
|
35
36
|
"chalk": "^5.6.2",
|
|
37
|
+
"chokidar": "^4.0.3",
|
|
36
38
|
"commander": "^14.0.0",
|
|
37
39
|
"open": "^10.2.0"
|
|
38
40
|
},
|