blodemd 0.0.3 → 0.0.4

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 ADDED
@@ -0,0 +1,104 @@
1
+ <h1 align="center">Blode.md</h1>
2
+
3
+ <p align="center">Deploy and manage documentation sites from the command line.</p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/blodemd"><img src="https://img.shields.io/npm/v/blodemd.svg" alt="npm version"></a>
7
+ </p>
8
+
9
+ - **One-command deploy:** Push your entire docs folder to Blode.md with `blodemd push`.
10
+ - **Scaffold in seconds:** Generate a ready-to-edit docs folder with `blodemd init`.
11
+ - **Config validation:** Catch `docs.json` errors before deploying.
12
+ - **CI-friendly:** Authenticate via environment variables and use the GitHub Action for automated deploys.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install -g blodemd
18
+ ```
19
+
20
+ Or run without installing:
21
+
22
+ ```bash
23
+ npx blodemd
24
+ ```
25
+
26
+ Requires Node.js 18+.
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Scaffold a new docs folder
32
+ blodemd init
33
+
34
+ # Authenticate
35
+ blodemd login
36
+
37
+ # Deploy your docs
38
+ blodemd push
39
+ ```
40
+
41
+ ## Commands
42
+
43
+ ```bash
44
+ blodemd init [dir] Scaffold a docs folder (default: docs)
45
+ blodemd login Authenticate with your API key
46
+ blodemd logout Remove stored credentials
47
+ blodemd whoami Show current authentication
48
+ blodemd validate [dir] Validate docs.json
49
+ blodemd push [dir] Deploy docs
50
+ blodemd dev Show instructions for the local dev server
51
+ ```
52
+
53
+ ### `push` Options
54
+
55
+ ```
56
+ --project <slug> Project slug (env: BLODEMD_PROJECT)
57
+ --api-url <url> API URL (env: BLODEMD_API_URL)
58
+ --api-key <token> API key (env: BLODEMD_API_KEY)
59
+ --branch <name> Git branch (env: BLODEMD_BRANCH)
60
+ --message <msg> Deploy message (env: BLODEMD_COMMIT_MESSAGE)
61
+ ```
62
+
63
+ The CLI reads the project slug from the `name` field in `docs.json` when `--project` is not set.
64
+
65
+ ## CI / GitHub Actions
66
+
67
+ Use the `mblode/blodemd/packages/deploy-action` composite action to deploy on every push:
68
+
69
+ ```yaml
70
+ - uses: mblode/blodemd/packages/deploy-action@main
71
+ with:
72
+ api-key: ${{ secrets.BLODEMD_API_KEY }}
73
+ directory: docs
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ The CLI looks for a `docs.json` file in the docs directory. Minimal example:
79
+
80
+ ```json
81
+ {
82
+ "$schema": "https://mintlify.com/docs.json",
83
+ "name": "my-project",
84
+ "theme": "mint",
85
+ "colors": { "primary": "#0D9373" },
86
+ "navigation": {
87
+ "groups": [{ "group": "Getting Started", "pages": ["index"] }]
88
+ }
89
+ }
90
+ ```
91
+
92
+ The CLI searches for `docs.json` in the current directory, then `docs/`, then `apps/docs/`.
93
+
94
+ ## Programmatic API
95
+
96
+ ```typescript
97
+ import type { DeploymentResponse } from "blodemd";
98
+ ```
99
+
100
+ The package exports the `DeploymentResponse` type for use in custom deployment scripts.
101
+
102
+ ## License
103
+
104
+ [MIT](../../LICENSE)
package/dist/cli.mjs CHANGED
@@ -1,12 +1,363 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
- import fs from "node:fs/promises";
4
- import path from "node:path";
5
- import { styleText } from "node:util";
6
- import { intro, log, outro, spinner } from "@clack/prompts";
3
+ import fs, { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import path, { join } from "node:path";
5
+ import { confirm, intro, isCancel, log, password, spinner } from "@clack/prompts";
6
+ import chalk from "chalk";
7
7
  import { Command } from "commander";
8
+ import open from "open";
9
+ import { homedir } from "node:os";
10
+ import { createServer } from "node:http";
11
+ import { createHash, randomBytes } from "node:crypto";
12
+ //#region src/constants.ts
13
+ const CLI_NAME = "blodemd";
14
+ const OAUTH_CLIENT_ID = "6b5f9860-fe96-4a83-b1ad-266260523c91";
15
+ const DEFAULT_OAUTH_CALLBACK_PORT = 8787;
16
+ const DEFAULT_OAUTH_CALLBACK_PATH = "/auth/callback";
17
+ const getDefaultConfigBaseDir = () => {
18
+ if (process.platform === "win32") return process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
19
+ return process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
20
+ };
21
+ const CONFIG_DIR = join(getDefaultConfigBaseDir(), CLI_NAME);
22
+ const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
23
+ //#endregion
24
+ //#region src/jwt.ts
25
+ const parseJwtBase64Url = (input) => {
26
+ const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
27
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
28
+ return Buffer.from(padded, "base64").toString("utf8");
29
+ };
30
+ const parseJwtClaims = (token) => {
31
+ const payloadPart = token.split(".").at(1);
32
+ if (!payloadPart) return null;
33
+ try {
34
+ const payload = parseJwtBase64Url(payloadPart);
35
+ const parsed = JSON.parse(payload);
36
+ if (typeof parsed !== "object" || parsed === null) return null;
37
+ const claims = parsed;
38
+ return {
39
+ email: typeof claims.email === "string" ? claims.email : void 0,
40
+ exp: typeof claims.exp === "number" ? claims.exp : void 0,
41
+ sub: typeof claims.sub === "string" ? claims.sub : void 0
42
+ };
43
+ } catch {
44
+ return null;
45
+ }
46
+ };
47
+ //#endregion
48
+ //#region src/errors.ts
49
+ const EXIT_CODES = {
50
+ AUTH_REQUIRED: 4,
51
+ CANCELLED: 2,
52
+ ERROR: 1,
53
+ NETWORK: 5,
54
+ SUCCESS: 0,
55
+ VALIDATION: 3
56
+ };
57
+ var CliError = class extends Error {
58
+ exitCode;
59
+ hint;
60
+ constructor(message, exitCode = EXIT_CODES.ERROR, hint) {
61
+ super(message);
62
+ this.name = "CliError";
63
+ this.exitCode = exitCode;
64
+ this.hint = hint ?? null;
65
+ }
66
+ };
67
+ const toCliError = (error) => {
68
+ if (error instanceof CliError) return error;
69
+ if (error instanceof Error) {
70
+ if (error instanceof TypeError && error.message.includes("fetch")) return new CliError("Cannot connect to Blode.md API.", EXIT_CODES.NETWORK, "Check your internet connection and API URL configuration.");
71
+ if (error.name === "TimeoutError" || error.name === "AbortError") return new CliError("Request timed out.", EXIT_CODES.NETWORK, "The API may be unavailable. Try again later.");
72
+ return new CliError(error.message, EXIT_CODES.ERROR);
73
+ }
74
+ return new CliError("Unknown error", EXIT_CODES.ERROR);
75
+ };
76
+ //#endregion
77
+ //#region src/oauth-token.ts
78
+ const postTokenRequest = async (url, body) => {
79
+ const response = await fetch(url, {
80
+ body: body.toString(),
81
+ headers: { "content-type": "application/x-www-form-urlencoded" },
82
+ method: "POST"
83
+ });
84
+ if (!response.ok) {
85
+ const text = await response.text().catch(() => "");
86
+ throw new CliError(`OAuth token request failed (${response.status}): ${text}`, EXIT_CODES.AUTH_REQUIRED);
87
+ }
88
+ return await response.json();
89
+ };
90
+ const exchangeAuthorizationCode = (config, code, codeVerifier, redirectUri) => {
91
+ const body = new URLSearchParams({
92
+ client_id: config.clientId,
93
+ code,
94
+ code_verifier: codeVerifier,
95
+ grant_type: "authorization_code",
96
+ redirect_uri: redirectUri
97
+ });
98
+ return postTokenRequest(config.tokenUrl, body);
99
+ };
100
+ const refreshAccessToken = (config, refreshToken) => {
101
+ const body = new URLSearchParams({
102
+ client_id: config.clientId,
103
+ grant_type: "refresh_token",
104
+ refresh_token: refreshToken
105
+ });
106
+ return postTokenRequest(config.tokenUrl, body);
107
+ };
108
+ //#endregion
109
+ //#region src/storage.ts
110
+ const isRecord = (value) => typeof value === "object" && value !== null;
111
+ const parseStoredAuthSession = (value) => {
112
+ if (!isRecord(value)) return null;
113
+ if (typeof value.accessToken !== "string") return null;
114
+ if (value.refreshToken !== null && typeof value.refreshToken !== "string") return null;
115
+ if (value.expiresAt !== null && typeof value.expiresAt !== "string") return null;
116
+ const { user } = value;
117
+ if (user !== null && (!isRecord(user) || typeof user.id !== "string" || user.email !== null && typeof user.email !== "string")) return null;
118
+ if (typeof value.createdAt !== "string") return null;
119
+ const parsedUser = user === null || !isRecord(user) ? null : {
120
+ email: user.email ?? null,
121
+ id: user.id
122
+ };
123
+ return {
124
+ accessToken: value.accessToken,
125
+ createdAt: value.createdAt,
126
+ expiresAt: value.expiresAt ?? null,
127
+ refreshToken: value.refreshToken ?? null,
128
+ user: parsedUser
129
+ };
130
+ };
131
+ const parseApiKeyCredentials = (value) => {
132
+ if (!isRecord(value)) return null;
133
+ if (typeof value.apiKey !== "string") return null;
134
+ return {
135
+ apiKey: value.apiKey,
136
+ type: "api-key"
137
+ };
138
+ };
139
+ const readAuthFile = async () => {
140
+ try {
141
+ const raw = await readFile(CREDENTIALS_FILE, "utf8");
142
+ const parsed = JSON.parse(raw);
143
+ if (!isRecord(parsed) || parsed.version !== 1) throw new CliError(`Invalid credentials format in ${CREDENTIALS_FILE}`, EXIT_CODES.ERROR);
144
+ return {
145
+ apiKey: parseApiKeyCredentials(parsed.apiKey) ?? void 0,
146
+ session: parseStoredAuthSession(parsed.session) ?? void 0,
147
+ version: 1
148
+ };
149
+ } catch (error) {
150
+ if (isRecord(error) && error.code === "ENOENT") return null;
151
+ if (error instanceof CliError) throw error;
152
+ return null;
153
+ }
154
+ };
155
+ const writeAuthFile = async (data) => {
156
+ await mkdir(CONFIG_DIR, {
157
+ mode: 448,
158
+ recursive: true
159
+ });
160
+ await writeFile(CREDENTIALS_FILE, `${JSON.stringify(data, null, 2)}\n`, {
161
+ encoding: "utf8",
162
+ mode: 384
163
+ });
164
+ };
165
+ const writeStoredAuthSession = async (session) => {
166
+ await writeAuthFile({
167
+ session,
168
+ version: 1
169
+ });
170
+ };
171
+ const writeStoredApiKey = async (apiKey) => {
172
+ await writeAuthFile({
173
+ apiKey,
174
+ version: 1
175
+ });
176
+ };
177
+ const clearStoredCredentials = async () => {
178
+ await rm(CREDENTIALS_FILE, { force: true });
179
+ };
180
+ //#endregion
181
+ //#region src/supabase.ts
182
+ const resolveSupabaseConfig = () => {
183
+ return { url: process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL ?? "https://bwnxwgkgyklzzmpbzuoz.supabase.co" };
184
+ };
185
+ const buildOAuthUrls = (config) => ({
186
+ authorizeUrl: `${config.url}/auth/v1/oauth/authorize`,
187
+ tokenUrl: `${config.url}/auth/v1/oauth/token`
188
+ });
189
+ const tokenResponseToStoredSession = (response) => {
190
+ const claims = parseJwtClaims(response.access_token);
191
+ let expiresAt = null;
192
+ if (typeof claims?.exp === "number") expiresAt = (/* @__PURE__ */ new Date(claims.exp * 1e3)).toISOString();
193
+ else if (response.expires_in > 0) expiresAt = new Date(Date.now() + response.expires_in * 1e3).toISOString();
194
+ return {
195
+ accessToken: response.access_token,
196
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
197
+ expiresAt,
198
+ refreshToken: response.refresh_token ?? null,
199
+ user: claims?.sub || claims?.email ? {
200
+ email: claims.email ?? null,
201
+ id: claims.sub ?? "unknown"
202
+ } : null
203
+ };
204
+ };
205
+ //#endregion
206
+ //#region src/auth-session.ts
207
+ const expiresInMs = (session) => {
208
+ if (!session.expiresAt) return null;
209
+ const expiresAtMs = Date.parse(session.expiresAt);
210
+ if (Number.isNaN(expiresAtMs)) return null;
211
+ return expiresAtMs - Date.now();
212
+ };
213
+ const isExpired = (session) => {
214
+ const ms = expiresInMs(session);
215
+ return ms !== null && ms <= 0;
216
+ };
217
+ const shouldRefresh = (session) => {
218
+ const ms = expiresInMs(session);
219
+ return ms !== null && ms <= 6e4;
220
+ };
221
+ const tokenFromRaw = (token, source) => {
222
+ const claims = parseJwtClaims(token);
223
+ return {
224
+ expiresAt: typeof claims?.exp === "number" ? (/* @__PURE__ */ new Date(claims.exp * 1e3)).toISOString() : null,
225
+ source,
226
+ token,
227
+ user: claims?.sub || claims?.email ? {
228
+ email: claims.email ?? null,
229
+ id: claims.sub ?? "unknown"
230
+ } : null
231
+ };
232
+ };
233
+ const sessionToResolvedToken = (session) => ({
234
+ expiresAt: session.expiresAt,
235
+ source: "stored",
236
+ token: session.accessToken,
237
+ user: session.user
238
+ });
239
+ const resolveAuthToken = async (optApiKey) => {
240
+ const envToken = (optApiKey ?? process.env["BLODEMD_API_KEY"])?.trim();
241
+ if (envToken) return tokenFromRaw(envToken, optApiKey ? "flag" : "environment");
242
+ const data = await readAuthFile();
243
+ const session = data?.session;
244
+ if (session) {
245
+ if (!(shouldRefresh(session) || isExpired(session))) return sessionToResolvedToken(session);
246
+ if (session.refreshToken) try {
247
+ const { tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
248
+ const updatedSession = tokenResponseToStoredSession(await refreshAccessToken({
249
+ clientId: OAUTH_CLIENT_ID,
250
+ tokenUrl
251
+ }, session.refreshToken));
252
+ await writeStoredAuthSession(updatedSession);
253
+ return sessionToResolvedToken(updatedSession);
254
+ } catch {}
255
+ if (isExpired(session)) {
256
+ await clearStoredCredentials();
257
+ return null;
258
+ }
259
+ return sessionToResolvedToken(session);
260
+ }
261
+ if (data?.apiKey) return {
262
+ expiresAt: null,
263
+ source: "stored",
264
+ token: data.apiKey.apiKey,
265
+ user: null
266
+ };
267
+ return null;
268
+ };
269
+ const resolveTokenStatus = (token) => {
270
+ if (!token.expiresAt) return {
271
+ expired: false,
272
+ expiresInSeconds: null
273
+ };
274
+ const expiresAtMs = Date.parse(token.expiresAt);
275
+ if (Number.isNaN(expiresAtMs)) return {
276
+ expired: false,
277
+ expiresInSeconds: null
278
+ };
279
+ const expiresInSeconds = Math.floor((expiresAtMs - Date.now()) / 1e3);
280
+ return {
281
+ expired: expiresInSeconds <= 0,
282
+ expiresInSeconds
283
+ };
284
+ };
285
+ //#endregion
286
+ //#region src/oauth-callback.ts
287
+ 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
+ const escapeHtml = (text) => text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
289
+ const errorHtml = (message) => `<!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>`;
290
+ const waitForOAuthCode = (options) => {
291
+ const host = options.redirectUrl.hostname;
292
+ const port = Number(options.redirectUrl.port);
293
+ const { pathname } = options.redirectUrl;
294
+ if (!Number.isInteger(port) || port <= 0) return Promise.reject(new CliError("OAuth redirect URL requires an explicit port", EXIT_CODES.ERROR));
295
+ return new Promise((resolve, reject) => {
296
+ let settled = false;
297
+ const settle = (ok, value) => {
298
+ if (settled) return;
299
+ settled = true;
300
+ clearTimeout(timer);
301
+ httpServer.close(() => {
302
+ if (ok) resolve(value);
303
+ else reject(value);
304
+ });
305
+ };
306
+ const httpServer = createServer((request, response) => {
307
+ if (!request.url) {
308
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
309
+ response.end(errorHtml("Missing request URL"));
310
+ settle(false, new CliError("OAuth callback is missing a request URL", EXIT_CODES.ERROR));
311
+ return;
312
+ }
313
+ const url = new URL(request.url, options.redirectUrl.origin);
314
+ if (url.pathname !== pathname) {
315
+ response.writeHead(404, { "content-type": "text/html; charset=utf-8" });
316
+ response.end(errorHtml("Invalid callback path"));
317
+ return;
318
+ }
319
+ const providerError = url.searchParams.get("error");
320
+ if (providerError) {
321
+ const description = url.searchParams.get("error_description") ?? providerError;
322
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
323
+ response.end(errorHtml(description));
324
+ settle(false, new CliError(`OAuth provider returned an error: ${description}`, EXIT_CODES.ERROR));
325
+ return;
326
+ }
327
+ if (url.searchParams.get("state") !== options.expectedState) {
328
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
329
+ response.end(errorHtml("State verification failed"));
330
+ settle(false, new CliError("OAuth state verification failed", EXIT_CODES.ERROR));
331
+ return;
332
+ }
333
+ const code = url.searchParams.get("code");
334
+ if (!code) {
335
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
336
+ response.end(errorHtml("Authorization code was missing"));
337
+ settle(false, new CliError("OAuth callback is missing an authorization code", EXIT_CODES.ERROR));
338
+ return;
339
+ }
340
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
341
+ response.end(SUCCESS_HTML);
342
+ settle(true, code);
343
+ });
344
+ httpServer.on("error", (error) => {
345
+ settle(false, new CliError(`Failed to start callback server on ${host}:${port}: ${error.message}`, EXIT_CODES.ERROR));
346
+ });
347
+ const timer = setTimeout(() => {
348
+ settle(false, new CliError("Login timed out. Please try again.", EXIT_CODES.CANCELLED));
349
+ }, options.timeoutMs);
350
+ httpServer.listen(port, host);
351
+ });
352
+ };
353
+ //#endregion
354
+ //#region src/pkce.ts
355
+ const createOAuthState = () => randomBytes(24).toString("hex");
356
+ const createCodeVerifier = () => randomBytes(64).toString("base64url");
357
+ const createCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest().toString("base64url");
358
+ //#endregion
8
359
  //#region src/cli.ts
9
- const CONTENT_CONFIG_FILE = "docs.json";
360
+ const CONFIG_FILE = "docs.json";
10
361
  const TEXT_CONTENT_TYPES = {
11
362
  ".css": "text/css; charset=utf-8",
12
363
  ".html": "text/html; charset=utf-8",
@@ -20,22 +371,34 @@ const TEXT_CONTENT_TYPES = {
20
371
  ".yml": "application/yaml; charset=utf-8"
21
372
  };
22
373
  const ensureFile = async (filePath, content) => {
374
+ try {
375
+ await fs.writeFile(filePath, content, { flag: "wx" });
376
+ } catch {}
377
+ };
378
+ const fileExists = async (filePath) => {
23
379
  try {
24
380
  await fs.access(filePath);
381
+ return true;
25
382
  } catch {
26
- await fs.writeFile(filePath, content);
383
+ return false;
27
384
  }
28
385
  };
29
- const isMissingFileError = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
30
- const validateConfigFile = async (root) => {
31
- try {
32
- const raw = await fs.readFile(path.join(root, CONTENT_CONFIG_FILE), "utf8");
33
- JSON.parse(raw);
34
- return CONTENT_CONFIG_FILE;
35
- } catch (error) {
36
- if (isMissingFileError(error)) throw new Error(`${CONTENT_CONFIG_FILE} not found.`, { cause: error });
37
- throw error;
38
- }
386
+ const readConfig = async (root) => {
387
+ const raw = await fs.readFile(path.join(root, CONFIG_FILE), "utf8");
388
+ return {
389
+ name: JSON.parse(raw).name,
390
+ raw
391
+ };
392
+ };
393
+ const resolveDocsRoot = async (dir) => {
394
+ if (dir) return path.resolve(process.cwd(), dir);
395
+ const candidates = [
396
+ process.cwd(),
397
+ path.join(process.cwd(), "docs"),
398
+ path.join(process.cwd(), "apps/docs")
399
+ ];
400
+ for (const candidate of candidates) if (await fileExists(path.join(candidate, CONFIG_FILE))) return candidate;
401
+ return process.cwd();
39
402
  };
40
403
  const readGitValue = (gitArgs) => {
41
404
  const result = spawnSync("git", gitArgs, {
@@ -84,17 +447,204 @@ const requestJson = async (url, init, message) => {
84
447
  }
85
448
  return data;
86
449
  };
450
+ const parsePositiveInteger = (value, label) => {
451
+ const parsed = Number.parseInt(value, 10);
452
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new CliError(`${label} must be a positive integer.`, EXIT_CODES.VALIDATION);
453
+ return parsed;
454
+ };
455
+ const reportCommandError = (prefix, error) => {
456
+ const cliError = toCliError(error);
457
+ log.error(`${prefix}: ${cliError.message}`);
458
+ if (cliError.hint) log.info(cliError.hint);
459
+ log.info("Failed");
460
+ process.exitCode = cliError.exitCode;
461
+ };
462
+ const fetchUserEmail = async (apiUrl, token) => {
463
+ try {
464
+ return (await requestJson(`${apiUrl}/auth/me`, { headers: { Authorization: `Bearer ${token}` } }, "Failed to fetch user info")).email;
465
+ } catch {
466
+ return null;
467
+ }
468
+ };
469
+ const resolvePushConfig = async (config, options) => {
470
+ const project = options.project ?? process.env["BLODEMD_PROJECT"] ?? config.name;
471
+ const apiUrl = options.apiUrl ?? process.env["BLODEMD_API_URL"] ?? "https://api.blode.md";
472
+ const authToken = (await resolveAuthToken(options.apiKey))?.token;
473
+ const branch = options.branch ?? process.env["BLODEMD_BRANCH"] ?? process.env.GITHUB_REF_NAME ?? readGitValue([
474
+ "rev-parse",
475
+ "--abbrev-ref",
476
+ "HEAD"
477
+ ]) ?? "main";
478
+ const commitMessage = options.message ?? process.env["BLODEMD_COMMIT_MESSAGE"] ?? readGitValue([
479
+ "log",
480
+ "-1",
481
+ "--pretty=%s"
482
+ ]);
483
+ if (!project) throw new Error("Missing project slug. Set \"name\" in docs.json, pass --project, or set BLODEMD_PROJECT.");
484
+ if (!authToken) throw new Error("Missing credentials. Run \"blodemd login\", pass --api-key, or set BLODEMD_API_KEY.");
485
+ return {
486
+ apiUrl,
487
+ authToken,
488
+ branch,
489
+ commitMessage,
490
+ project
491
+ };
492
+ };
493
+ const autoCreateProject = async (project, apiUrl, headers) => {
494
+ if (!(await readAuthFile())?.session) throw new Error(`Project "${project}" not found. Create it at blode.md or login with "blodemd login" to auto-create.`);
495
+ const shouldCreate = await confirm({ message: `Project "${project}" doesn't exist. Create it?` });
496
+ if (isCancel(shouldCreate) || !shouldCreate) return false;
497
+ const createResult = await requestJson(new URL("/projects", apiUrl).toString(), {
498
+ body: JSON.stringify({
499
+ name: project,
500
+ slug: project
501
+ }),
502
+ headers,
503
+ method: "POST"
504
+ }, "Failed to create project");
505
+ log.success(`Project ${chalk.cyan(createResult.project.slug)} created`);
506
+ log.info(`API key for CI: ${chalk.dim(createResult.token)}`);
507
+ return true;
508
+ };
509
+ const uploadFiles = async (files, root, apiPath, deploymentId, headers, s) => {
510
+ s.start(`Uploading ${files.length} files`);
511
+ for (const [index, filePath] of files.entries()) {
512
+ const relativePath = normalizeRelativePath(root, filePath);
513
+ const content = await fs.readFile(filePath);
514
+ await requestJson(apiPath(`/${deploymentId}/files`), {
515
+ body: JSON.stringify({
516
+ contentBase64: content.toString("base64"),
517
+ contentType: getContentType(filePath),
518
+ path: relativePath
519
+ }),
520
+ headers,
521
+ method: "POST"
522
+ }, `Failed to upload ${relativePath}`);
523
+ s.message(`Uploading files (${index + 1}/${files.length})`);
524
+ }
525
+ s.stop(`Uploaded ${chalk.cyan(String(files.length))} files`);
526
+ };
87
527
  const program = new Command();
88
- program.name("blode-docs").description("Blode Docs CLI").version("0.0.3");
89
- program.command("init").description("Scaffold a content folder").argument("[dir]", "target directory", "docs").action(async (dir) => {
90
- intro(styleText("bold", "blode-docs init"));
528
+ program.name("blodemd").description("Blode.md CLI").version("0.0.3");
529
+ program.command("login").description("Authenticate with Blode.md").option("--token", "Paste an API key instead of using browser login").option("--port <port>", "Loopback callback port", String(DEFAULT_OAUTH_CALLBACK_PORT)).option("--timeout <seconds>", "OAuth timeout in seconds", String(180)).option("--no-open", "Print URL instead of opening the browser").action(async (options) => {
530
+ intro(chalk.bold("blodemd login"));
531
+ try {
532
+ if (options.token) {
533
+ const apiKey = await password({
534
+ message: "Enter your API key",
535
+ validate: (value) => {
536
+ if (!value) return "API key is required.";
537
+ }
538
+ });
539
+ if (isCancel(apiKey)) {
540
+ log.warn("Cancelled");
541
+ return;
542
+ }
543
+ await writeStoredApiKey({
544
+ apiKey,
545
+ type: "api-key"
546
+ });
547
+ const prefix = apiKey.split(".")[0] ?? apiKey.slice(0, 12);
548
+ log.success(`Authenticated as ${chalk.cyan(prefix)}`);
549
+ log.info("Done");
550
+ return;
551
+ }
552
+ const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
553
+ const clientId = OAUTH_CLIENT_ID;
554
+ const port = parsePositiveInteger(options.port, "Port");
555
+ const timeoutSeconds = parsePositiveInteger(options.timeout, "Timeout");
556
+ const redirectUrl = new URL(`http://127.0.0.1:${port}${DEFAULT_OAUTH_CALLBACK_PATH}`);
557
+ const state = createOAuthState();
558
+ const codeVerifier = createCodeVerifier();
559
+ const codeChallenge = createCodeChallenge(codeVerifier);
560
+ const authUrl = new URL(authorizeUrl);
561
+ authUrl.searchParams.set("response_type", "code");
562
+ authUrl.searchParams.set("client_id", clientId);
563
+ authUrl.searchParams.set("redirect_uri", redirectUrl.toString());
564
+ authUrl.searchParams.set("code_challenge", codeChallenge);
565
+ authUrl.searchParams.set("code_challenge_method", "S256");
566
+ authUrl.searchParams.set("state", state);
567
+ authUrl.searchParams.set("scope", "openid email profile");
568
+ const callbackPromise = waitForOAuthCode({
569
+ expectedState: state,
570
+ redirectUrl,
571
+ timeoutMs: timeoutSeconds * 1e3
572
+ });
573
+ if (options.open) {
574
+ log.info("Opening browser for authentication...");
575
+ log.info(`If the browser doesn't open, visit: ${chalk.cyan(authUrl.toString())}`);
576
+ await open(authUrl.toString());
577
+ } else {
578
+ log.info("Open this URL to continue authentication:");
579
+ log.info(chalk.cyan(authUrl.toString()));
580
+ }
581
+ const code = await callbackPromise;
582
+ const storedSession = tokenResponseToStoredSession(await exchangeAuthorizationCode({
583
+ clientId,
584
+ tokenUrl
585
+ }, code, codeVerifier, redirectUrl.toString()));
586
+ await writeStoredAuthSession(storedSession);
587
+ const email = storedSession.user?.email ?? await fetchUserEmail(process.env["BLODEMD_API_URL"] ?? "https://api.blode.md", storedSession.accessToken);
588
+ if (email) log.success(`Logged in as ${chalk.cyan(email)}`);
589
+ else log.success("Logged in successfully.");
590
+ log.info("Done");
591
+ } catch (error) {
592
+ reportCommandError("Login failed", error);
593
+ }
594
+ });
595
+ program.command("logout").description("Remove stored credentials").action(async () => {
596
+ intro(chalk.bold("blodemd logout"));
597
+ try {
598
+ const existing = await readAuthFile();
599
+ await clearStoredCredentials();
600
+ if (existing?.session || existing?.apiKey) log.success("Credentials removed.");
601
+ else log.info("No stored credentials found.");
602
+ log.info("Done");
603
+ } catch (error) {
604
+ reportCommandError("Logout failed", error);
605
+ }
606
+ });
607
+ program.command("whoami").description("Show current authentication").action(async () => {
608
+ try {
609
+ const resolved = await resolveAuthToken();
610
+ if (!resolved) {
611
+ log.warn("Not logged in. Run \"blodemd login\" to authenticate.");
612
+ return;
613
+ }
614
+ if (resolved.source === "environment") {
615
+ log.info("Authenticated via BLODEMD_API_KEY environment variable");
616
+ return;
617
+ }
618
+ if (!resolved.expiresAt && !resolved.user) {
619
+ const prefix = resolved.token.split(".")[0] ?? resolved.token.slice(0, 12);
620
+ log.info(`Logged in with API key ${chalk.cyan(prefix)}`);
621
+ return;
622
+ }
623
+ const status = resolveTokenStatus(resolved);
624
+ const email = resolved.user?.email ?? await fetchUserEmail(process.env["BLODEMD_API_URL"] ?? "https://api.blode.md", resolved.token);
625
+ if (email) log.info(`Logged in as ${chalk.cyan(email)}`);
626
+ 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
+ }
635
+ } catch (error) {
636
+ reportCommandError("Whoami failed", error);
637
+ }
638
+ });
639
+ program.command("init").description("Scaffold a docs folder").argument("[dir]", "target directory", "docs").action(async (dir) => {
640
+ intro(chalk.bold("blodemd init"));
91
641
  try {
92
642
  const root = path.resolve(process.cwd(), dir);
93
643
  await fs.mkdir(root, { recursive: true });
94
- await ensureFile(path.join(root, CONTENT_CONFIG_FILE), `${JSON.stringify({
644
+ await ensureFile(path.join(root, CONFIG_FILE), `${JSON.stringify({
95
645
  $schema: "https://mintlify.com/docs.json",
96
646
  colors: { primary: "#0D9373" },
97
- name: "My Site",
647
+ name: "my-project",
98
648
  navigation: { groups: [{
99
649
  group: "Getting Started",
100
650
  pages: ["index"]
@@ -102,85 +652,69 @@ program.command("init").description("Scaffold a content folder").argument("[dir]
102
652
  theme: "mint"
103
653
  }, null, 2)}\n`);
104
654
  await ensureFile(path.join(root, "index.mdx"), "---\ntitle: Welcome\n---\n\nStart writing your docs here.\n");
105
- log.success(`Docs scaffolded in ${styleText("cyan", root)}`);
106
- outro("Done");
655
+ log.success(`Docs scaffolded in ${chalk.cyan(root)}`);
656
+ log.info(`Set ${chalk.cyan("name")} in docs.json to your project slug.`);
657
+ log.info("Done");
107
658
  } catch (error) {
108
- log.error(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
109
- outro("Failed");
110
- process.exitCode = 1;
659
+ reportCommandError("Init failed", error);
111
660
  }
112
661
  });
113
- program.command("validate").description("Validate docs.json").argument("[dir]", "target directory", "docs").action(async (dir) => {
114
- intro(styleText("bold", "blode-docs validate"));
115
- const root = path.resolve(process.cwd(), dir);
662
+ program.command("validate").description("Validate docs.json").argument("[dir]", "docs directory").action(async (dir) => {
663
+ intro(chalk.bold("blodemd validate"));
116
664
  try {
117
- const configFile = await validateConfigFile(root);
118
- log.success(`${styleText("cyan", configFile)} is valid JSON.`);
119
- outro("Done");
665
+ await readConfig(await resolveDocsRoot(dir));
666
+ log.success(`${chalk.cyan(CONFIG_FILE)} is valid.`);
667
+ log.info("Done");
120
668
  } catch (error) {
121
- log.error(`Validation failed: ${error instanceof Error ? error.message : String(error)}`);
122
- outro("Failed");
123
- process.exitCode = 1;
669
+ reportCommandError("Validation failed", error);
124
670
  }
125
671
  });
126
- program.command("push").description("Publish docs content").argument("[dir]", "target directory", "docs").option("--project <slug>", "project slug (env: BLODE_DOCS_PROJECT)").option("--api-url <url>", "API endpoint URL (env: BLODE_DOCS_API_URL)").option("--api-key <token>", "API authentication token (env: BLODE_DOCS_API_KEY)").option("--branch <name>", "git branch name (env: BLODE_DOCS_BRANCH)").option("--commit-message <message>", "deployment message (env: BLODE_DOCS_COMMIT_MESSAGE)").action(async (dir, options) => {
127
- intro(styleText("bold", "blode-docs push"));
672
+ program.command("push").description("Deploy docs").argument("[dir]", "docs directory").option("--project <slug>", "project slug (env: BLODEMD_PROJECT)").option("--api-url <url>", "API URL (env: BLODEMD_API_URL)").option("--api-key <token>", "API key (env: BLODEMD_API_KEY)").option("--branch <name>", "git branch (env: BLODEMD_BRANCH)").option("--message <msg>", "deploy message (env: BLODEMD_COMMIT_MESSAGE)").action(async (dir, options) => {
673
+ intro(chalk.bold("blodemd push"));
128
674
  const s = spinner();
129
675
  try {
130
- const root = path.resolve(process.cwd(), dir);
676
+ const root = await resolveDocsRoot(dir);
131
677
  s.start("Validating configuration");
132
- await validateConfigFile(root);
678
+ const config = await readConfig(root);
133
679
  s.stop("Configuration valid");
134
- const project = options.project ?? process.env.BLODE_DOCS_PROJECT;
135
- const apiUrl = options.apiUrl ?? process.env.BLODE_DOCS_API_URL ?? process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
136
- const apiKey = options.apiKey ?? process.env.BLODE_DOCS_API_KEY;
137
- const branch = options.branch ?? process.env.BLODE_DOCS_BRANCH ?? process.env.GITHUB_REF_NAME ?? readGitValue([
138
- "rev-parse",
139
- "--abbrev-ref",
140
- "HEAD"
141
- ]) ?? "main";
142
- const commitMessage = options.commitMessage ?? process.env.BLODE_DOCS_COMMIT_MESSAGE ?? readGitValue([
143
- "log",
144
- "-1",
145
- "--pretty=%s"
146
- ]);
147
- if (!project) throw new Error("Missing project slug. Pass --project or set BLODE_DOCS_PROJECT.");
148
- if (!apiKey) throw new Error("Missing API key. Pass --api-key or set BLODE_DOCS_API_KEY.");
680
+ const { project, apiUrl, authToken, branch, commitMessage } = await resolvePushConfig(config, options);
149
681
  s.start("Collecting files");
150
682
  const files = await collectFiles(root);
151
- if (files.length === 0) throw new Error("No files found to publish.");
152
- s.stop(`Found ${styleText("cyan", String(files.length))} files`);
683
+ if (files.length === 0) throw new Error("No files found to deploy.");
684
+ s.stop(`Found ${chalk.cyan(String(files.length))} files`);
153
685
  const headers = {
154
- Authorization: `Bearer ${apiKey}`,
686
+ Authorization: `Bearer ${authToken}`,
155
687
  "Content-Type": "application/json"
156
688
  };
157
689
  const apiPath = (suffix) => new URL(`/projects/slug/${project}/deployments${suffix}`, apiUrl).toString();
690
+ const createDeploymentBody = JSON.stringify({
691
+ branch,
692
+ commitMessage
693
+ });
158
694
  s.start("Creating deployment");
159
- const deployment = await requestJson(apiPath(""), {
160
- body: JSON.stringify({
161
- branch,
162
- commitMessage
163
- }),
164
- headers,
165
- method: "POST"
166
- }, "Failed to create deployment");
167
- s.stop(`Deployment ${styleText("cyan", deployment.id)} created`);
168
- s.start(`Uploading ${files.length} files`);
169
- for (const [index, filePath] of files.entries()) {
170
- const relativePath = normalizeRelativePath(root, filePath);
171
- const content = await fs.readFile(filePath);
172
- await requestJson(apiPath(`/${deployment.id}/files`), {
173
- body: JSON.stringify({
174
- contentBase64: content.toString("base64"),
175
- contentType: getContentType(filePath),
176
- path: relativePath
177
- }),
695
+ let deployment;
696
+ try {
697
+ deployment = await requestJson(apiPath(""), {
698
+ body: createDeploymentBody,
699
+ headers,
700
+ method: "POST"
701
+ }, "Failed to create deployment");
702
+ } catch (error) {
703
+ if (!(error instanceof Error ? error.message : "").includes("404")) throw error;
704
+ s.stop("Project not found");
705
+ if (!await autoCreateProject(project, apiUrl, headers)) {
706
+ log.info("Cancelled");
707
+ return;
708
+ }
709
+ s.start("Creating deployment");
710
+ deployment = await requestJson(apiPath(""), {
711
+ body: createDeploymentBody,
178
712
  headers,
179
713
  method: "POST"
180
- }, `Failed to upload ${relativePath}`);
181
- s.message(`Uploading files (${index + 1}/${files.length})`);
714
+ }, "Failed to create deployment");
182
715
  }
183
- s.stop(`Uploaded ${styleText("cyan", String(files.length))} files`);
716
+ s.stop(`Deployment ${chalk.cyan(deployment.id)} created`);
717
+ await uploadFiles(files, root, apiPath, deployment.id, headers, s);
184
718
  s.start("Finalizing deployment");
185
719
  const finalized = await requestJson(apiPath(`/${deployment.id}/finalize`), {
186
720
  body: JSON.stringify({ promote: true }),
@@ -188,22 +722,20 @@ program.command("push").description("Publish docs content").argument("[dir]", "t
188
722
  method: "POST"
189
723
  }, "Failed to finalize deployment");
190
724
  s.stop("Deployment finalized");
191
- log.success(`Published deployment ${styleText("cyan", finalized.id)}`);
725
+ log.success(`Published ${chalk.cyan(finalized.id)}`);
192
726
  if (finalized.manifestUrl) log.info(`Manifest: ${finalized.manifestUrl}`);
193
727
  if (typeof finalized.fileCount === "number") log.info(`Files: ${finalized.fileCount}`);
194
- outro("Done");
728
+ log.info("Done");
195
729
  } catch (error) {
196
730
  s.stop("Failed");
197
- log.error(error instanceof Error ? error.message : String(error));
198
- outro("Failed");
199
- process.exitCode = 1;
731
+ reportCommandError("Push failed", error);
200
732
  }
201
733
  });
202
734
  program.command("dev").description("Start the docs dev server").action(() => {
203
- intro(styleText("bold", "blode-docs dev"));
204
- log.info(`Run ${styleText("cyan", "npm run dev --filter=docs")} from the repo root.`);
205
- log.info(`Then open ${styleText("cyan", "http://localhost:3001")} to view the docs site.`);
206
- outro("Done");
735
+ intro(chalk.bold("blodemd dev"));
736
+ log.info(`Run ${chalk.cyan("npm run dev --filter=docs")} from the repo root.`);
737
+ log.info(`Then open ${chalk.cyan("http://localhost:3001")} to view the docs site.`);
738
+ log.info("Done");
207
739
  });
208
740
  program.parse();
209
741
  //#endregion
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { styleText } from \"node:util\";\n\nimport { intro, log, outro, spinner } from \"@clack/prompts\";\nimport { Command } from \"commander\";\n\nimport type { DeploymentResponse } from \"./types.js\";\n\nconst CONTENT_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\nconst ensureFile = async (filePath: string, content: string): Promise<void> => {\n try {\n await fs.access(filePath);\n } catch {\n await fs.writeFile(filePath, content);\n }\n};\n\nconst isMissingFileError = (error: unknown): boolean =>\n error instanceof Error &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === \"ENOENT\";\n\nconst validateConfigFile = async (root: string): Promise<string> => {\n try {\n const raw = await fs.readFile(path.join(root, CONTENT_CONFIG_FILE), \"utf8\");\n JSON.parse(raw);\n return CONTENT_CONFIG_FILE;\n } catch (error) {\n if (isMissingFileError(error)) {\n throw new Error(`${CONTENT_CONFIG_FILE} not found.`, { cause: error });\n }\n throw error;\n }\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 program = new Command();\n\nprogram.name(\"blode-docs\").description(\"Blode Docs CLI\").version(\"0.0.3\");\n\nprogram\n .command(\"init\")\n .description(\"Scaffold a content folder\")\n .argument(\"[dir]\", \"target directory\", \"docs\")\n .action(async (dir: string) => {\n intro(styleText(\"bold\", \"blode-docs 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 Site\",\n navigation: {\n groups: [{ group: \"Getting Started\", pages: [\"index\"] }],\n },\n theme: \"mint\",\n };\n\n await ensureFile(\n path.join(root, CONTENT_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 ${styleText(\"cyan\", root)}`);\n outro(\"Done\");\n } catch (error: unknown) {\n log.error(\n `Init failed: ${error instanceof Error ? error.message : String(error)}`\n );\n outro(\"Failed\");\n process.exitCode = 1;\n }\n });\n\nprogram\n .command(\"validate\")\n .description(\"Validate docs.json\")\n .argument(\"[dir]\", \"target directory\", \"docs\")\n .action(async (dir: string) => {\n intro(styleText(\"bold\", \"blode-docs validate\"));\n\n const root = path.resolve(process.cwd(), dir);\n\n try {\n const configFile = await validateConfigFile(root);\n log.success(`${styleText(\"cyan\", configFile)} is valid JSON.`);\n outro(\"Done\");\n } catch (error: unknown) {\n log.error(\n `Validation failed: ${error instanceof Error ? error.message : String(error)}`\n );\n outro(\"Failed\");\n process.exitCode = 1;\n }\n });\n\nprogram\n .command(\"push\")\n .description(\"Publish docs content\")\n .argument(\"[dir]\", \"target directory\", \"docs\")\n .option(\"--project <slug>\", \"project slug (env: BLODE_DOCS_PROJECT)\")\n .option(\"--api-url <url>\", \"API endpoint URL (env: BLODE_DOCS_API_URL)\")\n .option(\n \"--api-key <token>\",\n \"API authentication token (env: BLODE_DOCS_API_KEY)\"\n )\n .option(\"--branch <name>\", \"git branch name (env: BLODE_DOCS_BRANCH)\")\n .option(\n \"--commit-message <message>\",\n \"deployment message (env: BLODE_DOCS_COMMIT_MESSAGE)\"\n )\n .action(\n async (\n dir: string,\n options: {\n apiKey?: string;\n apiUrl?: string;\n branch?: string;\n commitMessage?: string;\n project?: string;\n }\n ) => {\n intro(styleText(\"bold\", \"blode-docs push\"));\n const s = spinner();\n\n try {\n const root = path.resolve(process.cwd(), dir);\n\n s.start(\"Validating configuration\");\n await validateConfigFile(root);\n s.stop(\"Configuration valid\");\n\n const project = options.project ?? process.env.BLODE_DOCS_PROJECT;\n const apiUrl =\n options.apiUrl ??\n process.env.BLODE_DOCS_API_URL ??\n process.env.NEXT_PUBLIC_API_URL ??\n \"http://localhost:4000\";\n const apiKey = options.apiKey ?? process.env.BLODE_DOCS_API_KEY;\n const branch =\n options.branch ??\n process.env.BLODE_DOCS_BRANCH ??\n process.env.GITHUB_REF_NAME ??\n readGitValue([\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]) ??\n \"main\";\n const commitMessage =\n options.commitMessage ??\n process.env.BLODE_DOCS_COMMIT_MESSAGE ??\n readGitValue([\"log\", \"-1\", \"--pretty=%s\"]);\n\n if (!project) {\n throw new Error(\n \"Missing project slug. Pass --project or set BLODE_DOCS_PROJECT.\"\n );\n }\n if (!apiKey) {\n throw new Error(\n \"Missing API key. Pass --api-key or set BLODE_DOCS_API_KEY.\"\n );\n }\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 publish.\");\n }\n s.stop(`Found ${styleText(\"cyan\", String(files.length))} files`);\n\n const headers = {\n Authorization: `Bearer ${apiKey}`,\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 s.start(\"Creating deployment\");\n const deployment = await requestJson<DeploymentResponse>(\n apiPath(\"\"),\n {\n body: JSON.stringify({ branch, commitMessage }),\n headers,\n method: \"POST\",\n },\n \"Failed to create deployment\"\n );\n s.stop(`Deployment ${styleText(\"cyan\", deployment.id)} created`);\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(`/${deployment.id}/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 ${styleText(\"cyan\", String(files.length))} files`);\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 deployment ${styleText(\"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 outro(\"Done\");\n } catch (error: unknown) {\n s.stop(\"Failed\");\n log.error(error instanceof Error ? error.message : String(error));\n outro(\"Failed\");\n process.exitCode = 1;\n }\n }\n );\n\nprogram\n .command(\"dev\")\n .description(\"Start the docs dev server\")\n .action(() => {\n intro(styleText(\"bold\", \"blode-docs dev\"));\n log.info(\n `Run ${styleText(\"cyan\", \"npm run dev --filter=docs\")} from the repo root.`\n );\n log.info(\n `Then open ${styleText(\"cyan\", \"http://localhost:3001\")} to view the docs site.`\n );\n outro(\"Done\");\n });\n\nprogram.parse();\n"],"mappings":"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"}
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(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\");\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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"}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "blodemd",
3
- "version": "0.0.3",
4
- "description": "Blode Docs CLI",
3
+ "version": "0.0.4",
4
+ "description": "Blode.md CLI",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/mblode/blode-docs"
7
+ "url": "https://github.com/mblode/blodemd"
8
8
  },
9
9
  "bin": {
10
10
  "blodemd": "./dist/cli.mjs"
@@ -32,7 +32,9 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@clack/prompts": "^1.0.0",
35
- "commander": "^14.0.0"
35
+ "chalk": "^5.6.2",
36
+ "commander": "^14.0.0",
37
+ "open": "^10.2.0"
36
38
  },
37
39
  "devDependencies": {
38
40
  "@repo/typescript-config": "*",