blodemd 0.0.8 → 0.0.10
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 +25 -9
- package/dev-server/app/[[...slug]]/page.tsx +1 -0
- package/dev-server/app/favicon.ico +0 -0
- package/dev-server/next.config.js +11 -13
- package/dev-server/package.json +1 -1
- package/dev-server/tsconfig.json +3 -0
- package/dist/cli.mjs +869 -184
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +1 -1
- package/docs/components/animate-ui/primitives/buttons/button.tsx +14 -0
- package/docs/components/api/api-playground.tsx +255 -80
- package/docs/components/api/api-reference.tsx +11 -1
- package/docs/components/docs/contextual-menu.tsx +227 -142
- package/docs/components/docs/copy-page-menu.tsx +148 -85
- package/docs/components/docs/doc-header.tsx +13 -3
- package/docs/components/docs/doc-shell.tsx +25 -14
- package/docs/components/docs/mobile-nav.tsx +0 -6
- package/docs/components/mdx/code-group.tsx +171 -62
- package/docs/components/mdx/steps.tsx +1 -1
- package/docs/components/mdx/tabs.tsx +131 -26
- package/docs/components/ui/copy-button.tsx +122 -0
- package/docs/components/ui/input.tsx +0 -1
- package/docs/components/ui/search.tsx +241 -132
- package/docs/components/ui/site-footer.tsx +39 -0
- package/docs/lib/config.ts +7 -0
- package/docs/lib/content-root.ts +33 -0
- package/docs/lib/content-source.ts +70 -0
- package/docs/lib/contextual-options.ts +20 -0
- package/docs/lib/docs-runtime.tsx +595 -0
- package/docs/lib/edge-config.ts +95 -0
- package/docs/lib/env.ts +22 -0
- package/docs/lib/openapi-proxy.ts +88 -0
- package/docs/lib/platform-config.ts +6 -0
- package/docs/lib/routes.ts +39 -0
- package/docs/lib/supabase.ts +13 -0
- package/docs/lib/tenancy.ts +350 -0
- package/docs/lib/tenant-headers.ts +14 -0
- package/docs/lib/tenant-static.ts +529 -0
- package/docs/lib/tenant-utility-context.ts +62 -0
- package/docs/lib/tenants.ts +68 -0
- package/docs/lib/use-mobile.ts +19 -0
- package/package.json +3 -2
- package/packages/@repo/common/dist/index.d.ts +7 -0
- package/packages/@repo/common/dist/index.d.ts.map +1 -1
- package/packages/@repo/common/dist/index.js +42 -0
- package/packages/@repo/common/src/index.ts +50 -0
- package/packages/@repo/contracts/dist/project.d.ts +1 -1
- package/packages/@repo/contracts/dist/project.js +1 -1
- package/packages/@repo/contracts/src/project.ts +1 -1
- package/packages/@repo/models/dist/docs-config.d.ts +194 -29
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +3 -28
- package/packages/@repo/models/src/docs-config.ts +5 -31
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/blob-source.js +7 -2
- package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/fs-source.js +2 -3
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +20 -50
- package/packages/@repo/previewing/src/blob-source.ts +7 -4
- package/packages/@repo/previewing/src/fs-source.ts +2 -3
- package/packages/@repo/previewing/src/index.ts +29 -64
- package/packages/@repo/validation/dist/index.d.ts +2 -2
- package/packages/@repo/validation/dist/index.d.ts.map +1 -1
- package/packages/@repo/validation/dist/index.js +2 -2
- package/packages/@repo/validation/package.json +1 -0
- package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
- package/packages/@repo/validation/src/index.ts +4 -4
package/dist/cli.mjs
CHANGED
|
@@ -3,9 +3,10 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import fs, { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import path, { join } from "node:path";
|
|
6
|
-
import { confirm, intro, isCancel, log, password, spinner } from "@clack/prompts";
|
|
6
|
+
import { confirm, intro, isCancel, log, password, select, spinner, text } from "@clack/prompts";
|
|
7
|
+
import { shouldIgnoreRootDocsFile, slugify } from "@repo/common";
|
|
7
8
|
import chalk from "chalk";
|
|
8
|
-
import { Command } from "commander";
|
|
9
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
9
10
|
import open from "open";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { once } from "node:events";
|
|
@@ -116,24 +117,36 @@ const refreshAccessToken = (config, refreshToken) => {
|
|
|
116
117
|
//#endregion
|
|
117
118
|
//#region src/storage.ts
|
|
118
119
|
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
120
|
+
const readNullableString = (value) => {
|
|
121
|
+
if (value === void 0 || value === null) return null;
|
|
122
|
+
return typeof value === "string" ? value : void 0;
|
|
123
|
+
};
|
|
124
|
+
const parseStoredSessionUser = (value) => {
|
|
125
|
+
if (value === void 0 || value === null) return null;
|
|
126
|
+
if (!isRecord(value) || typeof value.id !== "string") return;
|
|
127
|
+
const email = readNullableString(value.email);
|
|
128
|
+
if (email === void 0) return;
|
|
129
|
+
return {
|
|
130
|
+
email,
|
|
131
|
+
id: value.id
|
|
132
|
+
};
|
|
133
|
+
};
|
|
119
134
|
const parseStoredAuthSession = (value) => {
|
|
120
135
|
if (!isRecord(value)) return null;
|
|
121
136
|
if (typeof value.accessToken !== "string") return null;
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
137
|
+
const refreshToken = readNullableString(value.refreshToken);
|
|
138
|
+
if (refreshToken === void 0) return null;
|
|
139
|
+
const expiresAt = readNullableString(value.expiresAt);
|
|
140
|
+
if (expiresAt === void 0) return null;
|
|
141
|
+
const user = parseStoredSessionUser(value.user);
|
|
142
|
+
if (user === void 0) return null;
|
|
126
143
|
if (typeof value.createdAt !== "string") return null;
|
|
127
|
-
const parsedUser = user === null || !isRecord(user) ? null : {
|
|
128
|
-
email: user.email ?? null,
|
|
129
|
-
id: user.id
|
|
130
|
-
};
|
|
131
144
|
return {
|
|
132
145
|
accessToken: value.accessToken,
|
|
133
146
|
createdAt: value.createdAt,
|
|
134
|
-
expiresAt
|
|
135
|
-
refreshToken
|
|
136
|
-
user
|
|
147
|
+
expiresAt,
|
|
148
|
+
refreshToken,
|
|
149
|
+
user
|
|
137
150
|
};
|
|
138
151
|
};
|
|
139
152
|
const parseApiKeyCredentials = (value) => {
|
|
@@ -144,20 +157,34 @@ const parseApiKeyCredentials = (value) => {
|
|
|
144
157
|
type: "api-key"
|
|
145
158
|
};
|
|
146
159
|
};
|
|
160
|
+
const createInvalidCredentialsError = (detail) => new CliError(detail ? `Invalid credentials format in ${CREDENTIALS_FILE}: ${detail}` : `Invalid credentials format in ${CREDENTIALS_FILE}`, EXIT_CODES.ERROR);
|
|
161
|
+
const parseAuthFile = (raw) => {
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(raw);
|
|
165
|
+
} catch {
|
|
166
|
+
throw new CliError(`Invalid credentials JSON in ${CREDENTIALS_FILE}`, EXIT_CODES.ERROR);
|
|
167
|
+
}
|
|
168
|
+
if (!isRecord(parsed) || parsed.version !== 1) throw createInvalidCredentialsError();
|
|
169
|
+
const hasSession = Object.hasOwn(parsed, "session");
|
|
170
|
+
const hasApiKey = Object.hasOwn(parsed, "apiKey");
|
|
171
|
+
const session = hasSession && parsed.session !== void 0 ? parseStoredAuthSession(parsed.session) : void 0;
|
|
172
|
+
const apiKey = hasApiKey && parsed.apiKey !== void 0 ? parseApiKeyCredentials(parsed.apiKey) : void 0;
|
|
173
|
+
if (hasSession && parsed.session !== void 0 && !session) throw createInvalidCredentialsError("stored session is malformed.");
|
|
174
|
+
if (hasApiKey && parsed.apiKey !== void 0 && !apiKey) throw createInvalidCredentialsError("stored API key is malformed.");
|
|
175
|
+
return {
|
|
176
|
+
apiKey: apiKey ?? void 0,
|
|
177
|
+
session: session ?? void 0,
|
|
178
|
+
version: 1
|
|
179
|
+
};
|
|
180
|
+
};
|
|
147
181
|
const readAuthFile = async () => {
|
|
148
182
|
try {
|
|
149
|
-
|
|
150
|
-
const parsed = JSON.parse(raw);
|
|
151
|
-
if (!isRecord(parsed) || parsed.version !== 1) throw new CliError(`Invalid credentials format in ${CREDENTIALS_FILE}`, EXIT_CODES.ERROR);
|
|
152
|
-
return {
|
|
153
|
-
apiKey: parseApiKeyCredentials(parsed.apiKey) ?? void 0,
|
|
154
|
-
session: parseStoredAuthSession(parsed.session) ?? void 0,
|
|
155
|
-
version: 1
|
|
156
|
-
};
|
|
183
|
+
return parseAuthFile(await readFile(CREDENTIALS_FILE, "utf8"));
|
|
157
184
|
} catch (error) {
|
|
158
185
|
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
159
186
|
if (error instanceof CliError) throw error;
|
|
160
|
-
|
|
187
|
+
throw new CliError(`Failed to read credentials file at ${CREDENTIALS_FILE}`, EXIT_CODES.ERROR);
|
|
161
188
|
}
|
|
162
189
|
};
|
|
163
190
|
const writeAuthFile = async (data) => {
|
|
@@ -291,11 +318,30 @@ const resolveTokenStatus = (token) => {
|
|
|
291
318
|
};
|
|
292
319
|
};
|
|
293
320
|
//#endregion
|
|
321
|
+
//#region src/validation.ts
|
|
322
|
+
const MAX_PORT = 65535;
|
|
323
|
+
const parsePositiveInteger = (value, label) => {
|
|
324
|
+
const trimmed = value.trim();
|
|
325
|
+
if (!/^\d+$/u.test(trimmed)) throw new CliError(`${label} must be a positive integer.`, EXIT_CODES.VALIDATION);
|
|
326
|
+
const parsed = Number(trimmed);
|
|
327
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new CliError(`${label} must be a positive integer.`, EXIT_CODES.VALIDATION);
|
|
328
|
+
return parsed;
|
|
329
|
+
};
|
|
330
|
+
const parsePort = (value, label = "Port") => {
|
|
331
|
+
const parsed = parsePositiveInteger(value, label);
|
|
332
|
+
if (parsed > MAX_PORT) throw new CliError(`${label} must be between 1 and ${MAX_PORT}.`, EXIT_CODES.VALIDATION);
|
|
333
|
+
return parsed;
|
|
334
|
+
};
|
|
335
|
+
//#endregion
|
|
294
336
|
//#region src/site-config.ts
|
|
295
337
|
const CONFIG_FILE$2 = "docs.json";
|
|
338
|
+
const getSiteConfigHint = (errors) => {
|
|
339
|
+
if (errors.includes(`${CONFIG_FILE$2} not found.`)) return `Make sure ${CONFIG_FILE$2} exists in the selected docs directory or pass the docs directory explicitly.`;
|
|
340
|
+
return `Fix the ${CONFIG_FILE$2} errors above and try again.`;
|
|
341
|
+
};
|
|
296
342
|
const loadValidatedSiteConfig = async (root) => {
|
|
297
343
|
const result = await loadSiteConfig(createFsSource(root));
|
|
298
|
-
if (!result.ok) throw new CliError(result.errors.join("\n"), EXIT_CODES.VALIDATION,
|
|
344
|
+
if (!result.ok) throw new CliError(result.errors.join("\n"), EXIT_CODES.VALIDATION, getSiteConfigHint(result.errors));
|
|
299
345
|
return {
|
|
300
346
|
config: result.config,
|
|
301
347
|
warnings: result.warnings
|
|
@@ -304,6 +350,12 @@ const loadValidatedSiteConfig = async (root) => {
|
|
|
304
350
|
//#endregion
|
|
305
351
|
//#region src/dev/resolve-root.ts
|
|
306
352
|
const CONFIG_FILE$1 = "docs.json";
|
|
353
|
+
const ROOT_CANDIDATES = [
|
|
354
|
+
"",
|
|
355
|
+
"docs",
|
|
356
|
+
path.join("apps", "docs")
|
|
357
|
+
];
|
|
358
|
+
const NESTED_DOCS_ROOT_CONTAINERS = [path.join("content"), path.join("apps", "docs", "content")];
|
|
307
359
|
const fileExists$1 = async (filePath) => {
|
|
308
360
|
try {
|
|
309
361
|
await fs.access(filePath);
|
|
@@ -312,17 +364,30 @@ const fileExists$1 = async (filePath) => {
|
|
|
312
364
|
return false;
|
|
313
365
|
}
|
|
314
366
|
};
|
|
315
|
-
const resolveDocsRoot = async (dir) => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
path.join(process.cwd(), "docs"),
|
|
320
|
-
path.join(process.cwd(), "apps/docs")
|
|
321
|
-
];
|
|
367
|
+
const resolveDocsRoot = async (dir, cwd = process.cwd()) => {
|
|
368
|
+
const currentWorkingDir = path.resolve(cwd);
|
|
369
|
+
if (dir) return path.resolve(currentWorkingDir, dir);
|
|
370
|
+
const candidates = ROOT_CANDIDATES.map((candidate) => path.join(currentWorkingDir, candidate));
|
|
322
371
|
for (const candidate of candidates) if (await fileExists$1(path.join(candidate, CONFIG_FILE$1))) return candidate;
|
|
323
|
-
|
|
372
|
+
for (const container of NESTED_DOCS_ROOT_CONTAINERS) {
|
|
373
|
+
const containerPath = path.join(currentWorkingDir, container);
|
|
374
|
+
if (!await fileExists$1(containerPath)) continue;
|
|
375
|
+
const docsRoots = (await fs.readdir(containerPath, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path.join(containerPath, entry.name)).toSorted((left, right) => {
|
|
376
|
+
const preferredNames = ["docs", "example"];
|
|
377
|
+
const leftRank = preferredNames.indexOf(path.basename(left));
|
|
378
|
+
const rightRank = preferredNames.indexOf(path.basename(right));
|
|
379
|
+
if (leftRank !== rightRank) {
|
|
380
|
+
if (leftRank === -1) return 1;
|
|
381
|
+
if (rightRank === -1) return -1;
|
|
382
|
+
return leftRank - rightRank;
|
|
383
|
+
}
|
|
384
|
+
return left.localeCompare(right);
|
|
385
|
+
});
|
|
386
|
+
for (const candidate of docsRoots) if (await fileExists$1(path.join(candidate, CONFIG_FILE$1))) return candidate;
|
|
387
|
+
}
|
|
388
|
+
return currentWorkingDir;
|
|
324
389
|
};
|
|
325
|
-
const validateDocsRoot =
|
|
390
|
+
const validateDocsRoot = loadValidatedSiteConfig;
|
|
326
391
|
//#endregion
|
|
327
392
|
//#region src/dev/watcher.ts
|
|
328
393
|
const INVALIDATE_ENDPOINT = "/blodemd-dev/invalidate";
|
|
@@ -393,11 +458,9 @@ const RUNTIME_EXCLUDE_DIRS = new Set([
|
|
|
393
458
|
".turbo",
|
|
394
459
|
"node_modules"
|
|
395
460
|
]);
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return parsed;
|
|
400
|
-
};
|
|
461
|
+
const STANDALONE_RUNTIME_MAX_AGE_MS = 1440 * 60 * 1e3;
|
|
462
|
+
const STANDALONE_RUNTIME_PREFIX = "standalone-runtime-";
|
|
463
|
+
const TURBOPACK_ARGS = ["dev", "--turbopack"];
|
|
401
464
|
const fileExists = async (filePath) => {
|
|
402
465
|
try {
|
|
403
466
|
await fs.access(filePath);
|
|
@@ -406,6 +469,35 @@ const fileExists = async (filePath) => {
|
|
|
406
469
|
return false;
|
|
407
470
|
}
|
|
408
471
|
};
|
|
472
|
+
const resolveCommonAncestor = (pathsToCompare) => {
|
|
473
|
+
const [firstPath, ...restPaths] = pathsToCompare;
|
|
474
|
+
if (!firstPath) return path.sep;
|
|
475
|
+
const first = path.resolve(firstPath);
|
|
476
|
+
const { root } = path.parse(first);
|
|
477
|
+
const firstSegments = first.slice(root.length).split(path.sep).filter(Boolean);
|
|
478
|
+
const sharedSegments = [];
|
|
479
|
+
for (const [index, segment] of firstSegments.entries()) {
|
|
480
|
+
if (!restPaths.every((candidatePath) => {
|
|
481
|
+
const candidate = path.resolve(candidatePath);
|
|
482
|
+
if (path.parse(candidate).root !== root) return false;
|
|
483
|
+
return candidate.slice(root.length).split(path.sep).filter(Boolean)[index] === segment;
|
|
484
|
+
})) break;
|
|
485
|
+
sharedSegments.push(segment);
|
|
486
|
+
}
|
|
487
|
+
return path.join(root, ...sharedSegments);
|
|
488
|
+
};
|
|
489
|
+
const cleanupStandaloneRuntimeRoots = async (configDir, maxAgeMs = STANDALONE_RUNTIME_MAX_AGE_MS) => {
|
|
490
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
491
|
+
const entries = await fs.readdir(configDir, { withFileTypes: true });
|
|
492
|
+
await Promise.all(entries.filter((entry) => entry.isDirectory() && entry.name.startsWith(STANDALONE_RUNTIME_PREFIX)).map(async (entry) => {
|
|
493
|
+
const entryPath = path.join(configDir, entry.name);
|
|
494
|
+
if ((await fs.stat(entryPath)).mtimeMs >= cutoff) return;
|
|
495
|
+
await fs.rm(entryPath, {
|
|
496
|
+
force: true,
|
|
497
|
+
recursive: true
|
|
498
|
+
});
|
|
499
|
+
}));
|
|
500
|
+
};
|
|
409
501
|
const probePortAvailability = async (port) => {
|
|
410
502
|
const server = createServer();
|
|
411
503
|
const listening = (async () => {
|
|
@@ -481,44 +573,53 @@ const isStandaloneCliInstall = async (cliPackageRoot) => {
|
|
|
481
573
|
return cliPackageRoot.split(path.sep).includes("node_modules");
|
|
482
574
|
}
|
|
483
575
|
};
|
|
576
|
+
const createStandaloneRuntimeRoot = async (configDir = CONFIG_DIR) => {
|
|
577
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
578
|
+
await cleanupStandaloneRuntimeRoots(configDir);
|
|
579
|
+
return await fs.mkdtemp(path.join(configDir, STANDALONE_RUNTIME_PREFIX));
|
|
580
|
+
};
|
|
484
581
|
const materializeStandaloneRuntime = async (cliPackageRoot) => {
|
|
485
|
-
const runtimeRoot =
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
"
|
|
493
|
-
"
|
|
494
|
-
"packages"
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
582
|
+
const runtimeRoot = await createStandaloneRuntimeRoot();
|
|
583
|
+
try {
|
|
584
|
+
for (const dir of [
|
|
585
|
+
"dev-server",
|
|
586
|
+
"docs",
|
|
587
|
+
"packages"
|
|
588
|
+
]) await copyStandaloneTree(path.join(cliPackageRoot, dir), path.join(runtimeRoot, dir));
|
|
589
|
+
await fs.symlink(path.join(cliPackageRoot, "node_modules"), path.join(runtimeRoot, "node_modules"), process.platform === "win32" ? "junction" : "dir");
|
|
590
|
+
await fs.mkdir(path.join(runtimeRoot, "dev-server", "node_modules"), { recursive: true });
|
|
591
|
+
await fs.symlink(path.join(runtimeRoot, "packages", "@repo"), path.join(runtimeRoot, "dev-server", "node_modules", "@repo"), process.platform === "win32" ? "junction" : "dir");
|
|
592
|
+
await fs.writeFile(path.join(runtimeRoot, "dev-server", "package.json"), `${JSON.stringify({
|
|
593
|
+
dependencies: {
|
|
594
|
+
next: "16.2.1",
|
|
595
|
+
react: "^19.2.0",
|
|
596
|
+
"react-dom": "^19.2.0"
|
|
597
|
+
},
|
|
598
|
+
devDependencies: {
|
|
599
|
+
"@types/node": "^22.19.15",
|
|
600
|
+
"@types/react": "19.2.14",
|
|
601
|
+
"@types/react-dom": "19.2.3",
|
|
602
|
+
typescript: "6.0.2"
|
|
603
|
+
},
|
|
604
|
+
name: "blodemd-dev-server",
|
|
605
|
+
private: true,
|
|
606
|
+
type: "module"
|
|
607
|
+
}, null, 2)}\n`);
|
|
608
|
+
return {
|
|
609
|
+
devServerDir: path.join(runtimeRoot, "dev-server"),
|
|
610
|
+
runtimeRoot
|
|
611
|
+
};
|
|
612
|
+
} catch (error) {
|
|
613
|
+
await fs.rm(runtimeRoot, {
|
|
614
|
+
force: true,
|
|
615
|
+
recursive: true
|
|
616
|
+
});
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
517
619
|
};
|
|
518
620
|
/**
|
|
519
|
-
* Check if a shipped dev-server exists alongside
|
|
520
|
-
*
|
|
521
|
-
* (it's a dependency when npm-installed, but not in the monorepo).
|
|
621
|
+
* Check if a shipped dev-server exists alongside an installed CLI package.
|
|
622
|
+
* We only use standalone mode when the package root lives under `node_modules`.
|
|
522
623
|
*/
|
|
523
624
|
const findStandaloneDevServer = async (cliPackageRoot) => {
|
|
524
625
|
const devServerDir = path.join(cliPackageRoot, "dev-server");
|
|
@@ -534,7 +635,7 @@ const findStandaloneDevServer = async (cliPackageRoot) => {
|
|
|
534
635
|
devServerDir: runtime.devServerDir,
|
|
535
636
|
mode: "standalone",
|
|
536
637
|
nextPackageRoot: cliPackageRoot,
|
|
537
|
-
|
|
638
|
+
runtimeRoot: runtime.runtimeRoot
|
|
538
639
|
};
|
|
539
640
|
};
|
|
540
641
|
/**
|
|
@@ -567,36 +668,39 @@ const resolveDevServer = async (cliFilePath) => {
|
|
|
567
668
|
repoRoot: await findMonorepoRoot(path.dirname(cliFilePath))
|
|
568
669
|
};
|
|
569
670
|
};
|
|
570
|
-
const
|
|
571
|
-
if (server.mode === "standalone") {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
671
|
+
const buildDevServerLaunch = (server, { root, port }) => {
|
|
672
|
+
if (server.mode === "standalone") return {
|
|
673
|
+
args: [resolveNextBin(server.nextPackageRoot), ...TURBOPACK_ARGS],
|
|
674
|
+
command: process.execPath,
|
|
675
|
+
cwd: server.devServerDir,
|
|
676
|
+
env: {
|
|
677
|
+
...process.env,
|
|
678
|
+
BLODEMD_PACKAGES_DIR: path.join(server.runtimeRoot, "packages"),
|
|
679
|
+
BLODEMD_TURBOPACK_ROOT: resolveCommonAncestor([server.nextPackageRoot, server.runtimeRoot]),
|
|
680
|
+
DOCS_ROOT: root,
|
|
681
|
+
PORT: String(port)
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
return {
|
|
685
|
+
args: [
|
|
686
|
+
"run",
|
|
575
687
|
"dev",
|
|
576
|
-
"--
|
|
577
|
-
],
|
|
578
|
-
|
|
579
|
-
env: {
|
|
580
|
-
...process.env,
|
|
581
|
-
BLODEMD_PACKAGES_DIR: server.packagesDir,
|
|
582
|
-
DOCS_ROOT: root,
|
|
583
|
-
NODE_PATH: [server.packagesDir, process.env.NODE_PATH].filter(Boolean).join(path.delimiter),
|
|
584
|
-
PORT: String(port)
|
|
585
|
-
},
|
|
586
|
-
stdio: "inherit"
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
return spawn(process.platform === "win32" ? "npm.cmd" : "npm", [
|
|
590
|
-
"run",
|
|
591
|
-
"dev",
|
|
592
|
-
"--workspace=dev-server"
|
|
593
|
-
], {
|
|
688
|
+
"--workspace=dev-server"
|
|
689
|
+
],
|
|
690
|
+
command: process.platform === "win32" ? "npm.cmd" : "npm",
|
|
594
691
|
cwd: server.repoRoot,
|
|
595
692
|
env: {
|
|
596
693
|
...process.env,
|
|
597
694
|
DOCS_ROOT: root,
|
|
598
695
|
PORT: String(port)
|
|
599
|
-
}
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
};
|
|
699
|
+
const spawnDevServer = (server, options) => {
|
|
700
|
+
const launch = buildDevServerLaunch(server, options);
|
|
701
|
+
return spawn(launch.command, launch.args, {
|
|
702
|
+
cwd: launch.cwd,
|
|
703
|
+
env: launch.env,
|
|
600
704
|
stdio: "inherit"
|
|
601
705
|
});
|
|
602
706
|
};
|
|
@@ -615,16 +719,35 @@ const waitForServer = async ({ child, port }) => {
|
|
|
615
719
|
}
|
|
616
720
|
throw new CliError("Timed out waiting for the local dev server to start.", EXIT_CODES.ERROR);
|
|
617
721
|
};
|
|
618
|
-
const
|
|
619
|
-
|
|
722
|
+
const defaultDevCommandDependencies = {
|
|
723
|
+
createWatcher: createDevWatcher,
|
|
724
|
+
getCliFilePath: () => fileURLToPath(import.meta.url),
|
|
725
|
+
getIntro: intro,
|
|
726
|
+
getLog: log,
|
|
727
|
+
getOpen: open,
|
|
728
|
+
parsePortValue: parsePort,
|
|
729
|
+
removeDirectory: fs.rm,
|
|
730
|
+
resolveDevPortValue: resolveDevPort,
|
|
731
|
+
resolveDocsRootValue: resolveDocsRoot,
|
|
732
|
+
resolveServer: resolveDevServer,
|
|
733
|
+
shutdownChild: shutdownChildProcess,
|
|
734
|
+
spawnServer: spawnDevServer,
|
|
735
|
+
validateDocsRootValue: validateDocsRoot,
|
|
736
|
+
waitForServerReady: waitForServer
|
|
737
|
+
};
|
|
738
|
+
const devCommand = async ({ dir, openBrowser, port: portValue }, dependencies = defaultDevCommandDependencies) => {
|
|
739
|
+
const cliLog = dependencies.getLog;
|
|
740
|
+
dependencies.getIntro(chalk.bold("blodemd dev"));
|
|
620
741
|
try {
|
|
621
|
-
const
|
|
622
|
-
const
|
|
623
|
-
await
|
|
624
|
-
|
|
742
|
+
const port = dependencies.parsePortValue(portValue);
|
|
743
|
+
const resolvedPort = await dependencies.resolveDevPortValue(port);
|
|
744
|
+
const root = await dependencies.resolveDocsRootValue(dir);
|
|
745
|
+
await dependencies.validateDocsRootValue(root);
|
|
746
|
+
const cliFilePath = dependencies.getCliFilePath();
|
|
747
|
+
const server = await dependencies.resolveServer(cliFilePath);
|
|
625
748
|
const localUrl = `http://localhost:${resolvedPort}`;
|
|
626
|
-
|
|
627
|
-
const child =
|
|
749
|
+
cliLog.info(`Docs root: ${chalk.cyan(root)}`);
|
|
750
|
+
const child = dependencies.spawnServer(server, {
|
|
628
751
|
port: resolvedPort,
|
|
629
752
|
root
|
|
630
753
|
});
|
|
@@ -637,21 +760,25 @@ const devCommand = async ({ dir, openBrowser, port: portValue }) => {
|
|
|
637
760
|
await watcher.close();
|
|
638
761
|
watcher = null;
|
|
639
762
|
}
|
|
640
|
-
await
|
|
763
|
+
await dependencies.shutdownChild(child);
|
|
764
|
+
if (server.mode === "standalone") await dependencies.removeDirectory(server.runtimeRoot, {
|
|
765
|
+
force: true,
|
|
766
|
+
recursive: true
|
|
767
|
+
});
|
|
641
768
|
};
|
|
642
769
|
process.once("SIGINT", closeAll);
|
|
643
770
|
process.once("SIGTERM", closeAll);
|
|
644
771
|
try {
|
|
645
|
-
await
|
|
772
|
+
await dependencies.waitForServerReady({
|
|
646
773
|
child,
|
|
647
774
|
port: resolvedPort
|
|
648
775
|
});
|
|
649
|
-
watcher = await
|
|
776
|
+
watcher = await dependencies.createWatcher({
|
|
650
777
|
port: resolvedPort,
|
|
651
778
|
root
|
|
652
779
|
});
|
|
653
|
-
|
|
654
|
-
if (openBrowser) await
|
|
780
|
+
cliLog.success(`Dev server running at ${chalk.cyan(localUrl)}`);
|
|
781
|
+
if (openBrowser) await dependencies.getOpen(localUrl);
|
|
655
782
|
const [code, signal] = await once(child, "exit");
|
|
656
783
|
if (shuttingDown || signal === "SIGINT" || signal === "SIGTERM") return;
|
|
657
784
|
if (code !== 0) throw new CliError(`The local dev server exited with code ${code ?? "unknown"}.`, EXIT_CODES.ERROR);
|
|
@@ -662,12 +789,434 @@ const devCommand = async ({ dir, openBrowser, port: portValue }) => {
|
|
|
662
789
|
}
|
|
663
790
|
} catch (error) {
|
|
664
791
|
const cliError = toCliError(error);
|
|
665
|
-
|
|
666
|
-
if (cliError.hint)
|
|
792
|
+
cliLog.error(cliError.message);
|
|
793
|
+
if (cliError.hint) cliLog.info(cliError.hint);
|
|
667
794
|
process.exitCode = cliError.exitCode;
|
|
668
795
|
}
|
|
669
796
|
};
|
|
670
797
|
//#endregion
|
|
798
|
+
//#region src/fs-utils.ts
|
|
799
|
+
const writeFileIfMissing = async (filePath, content) => {
|
|
800
|
+
try {
|
|
801
|
+
await fs.writeFile(filePath, content, { flag: "wx" });
|
|
802
|
+
} catch (error) {
|
|
803
|
+
const { code } = error;
|
|
804
|
+
if (code === "EEXIST") {
|
|
805
|
+
if ((await fs.stat(filePath).catch(() => null))?.isFile()) return;
|
|
806
|
+
}
|
|
807
|
+
throw error;
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
const writeSymlinkIfMissing = async (filePath, target, options) => {
|
|
811
|
+
const lstat = options?.lstat ?? fs.lstat;
|
|
812
|
+
const symlink = options?.symlink ?? fs.symlink;
|
|
813
|
+
try {
|
|
814
|
+
await symlink(target, filePath);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
const { code } = error;
|
|
817
|
+
if (code === "EEXIST") {
|
|
818
|
+
const existing = await lstat(filePath).catch(() => null);
|
|
819
|
+
if (existing?.isFile() || existing?.isSymbolicLink()) return;
|
|
820
|
+
}
|
|
821
|
+
if (options?.fallbackContent && (code === "EINVAL" || code === "ENOTSUP" || code === "EPERM" || code === "UNKNOWN")) {
|
|
822
|
+
await writeFileIfMissing(filePath, options.fallbackContent);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
const findExistingPaths = async (root, relativePaths) => {
|
|
829
|
+
return (await Promise.all(relativePaths.map(async (relativePath) => {
|
|
830
|
+
return await fs.lstat(path.join(root, relativePath)).catch(() => null) ? relativePath : null;
|
|
831
|
+
}))).filter((relativePath) => relativePath !== null).toSorted((left, right) => left.localeCompare(right));
|
|
832
|
+
};
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src/scaffold.ts
|
|
835
|
+
const SCAFFOLD_TEMPLATES = ["minimal", "starter"];
|
|
836
|
+
const DEFAULT_SCAFFOLD_DIRECTORY = "docs";
|
|
837
|
+
const stringifyJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
838
|
+
const isScaffoldTemplate = (value) => SCAFFOLD_TEMPLATES.includes(value);
|
|
839
|
+
const normalizeProjectSlug = (value) => slugify(value) || "my-project";
|
|
840
|
+
const resolveScaffoldDirectory = (directory) => directory?.trim() || "docs";
|
|
841
|
+
const deriveDefaultProjectSlug = (directory, cwd) => {
|
|
842
|
+
const resolvedDirectory = resolveScaffoldDirectory(directory);
|
|
843
|
+
if (resolvedDirectory === "." || resolvedDirectory === "docs") return normalizeProjectSlug(path.basename(cwd));
|
|
844
|
+
return normalizeProjectSlug(path.basename(path.resolve(cwd, resolvedDirectory)));
|
|
845
|
+
};
|
|
846
|
+
const validateProjectSlug = (value) => {
|
|
847
|
+
const trimmed = value?.trim();
|
|
848
|
+
if (!trimmed) return "Project slug is required.";
|
|
849
|
+
const normalized = slugify(trimmed);
|
|
850
|
+
if (!normalized) return "Use at least one letter or number.";
|
|
851
|
+
if (normalized !== trimmed) return `Use lowercase letters, numbers, and hyphens. Try "${normalized}".`;
|
|
852
|
+
};
|
|
853
|
+
const createMinimalDocsJson = (projectSlug) => ({
|
|
854
|
+
$schema: "https://blode.md/docs.json",
|
|
855
|
+
name: projectSlug,
|
|
856
|
+
navigation: { groups: [{
|
|
857
|
+
group: "Getting Started",
|
|
858
|
+
pages: ["index"]
|
|
859
|
+
}] }
|
|
860
|
+
});
|
|
861
|
+
const createStarterDocsJson = (projectSlug) => ({
|
|
862
|
+
$schema: "https://blode.md/docs.json",
|
|
863
|
+
appearance: { default: "system" },
|
|
864
|
+
contextual: { options: [
|
|
865
|
+
"copy",
|
|
866
|
+
"view",
|
|
867
|
+
"chatgpt",
|
|
868
|
+
"claude"
|
|
869
|
+
] },
|
|
870
|
+
description: "Ship documentation from your terminal.",
|
|
871
|
+
favicon: "/favicon.svg",
|
|
872
|
+
logo: {
|
|
873
|
+
alt: `${projectSlug} logo`,
|
|
874
|
+
dark: "/logo/dark.svg",
|
|
875
|
+
light: "/logo/light.svg"
|
|
876
|
+
},
|
|
877
|
+
metadata: { timestamp: true },
|
|
878
|
+
name: projectSlug,
|
|
879
|
+
navigation: { groups: [{
|
|
880
|
+
group: "Getting Started",
|
|
881
|
+
pages: [
|
|
882
|
+
"index",
|
|
883
|
+
"quickstart",
|
|
884
|
+
"development"
|
|
885
|
+
]
|
|
886
|
+
}] }
|
|
887
|
+
});
|
|
888
|
+
const claudeInstructions = [
|
|
889
|
+
"> **First-time setup**: Customize this file for your project. Prompt the user to update terminology, style preferences, and content boundaries before drafting large amounts of docs.",
|
|
890
|
+
"",
|
|
891
|
+
"# Documentation project instructions",
|
|
892
|
+
"",
|
|
893
|
+
"## About this project",
|
|
894
|
+
"",
|
|
895
|
+
"- This is a documentation site built on [Blode.md](https://blode.md)",
|
|
896
|
+
"- Pages are MDX files with YAML frontmatter",
|
|
897
|
+
"- Configuration lives in `docs.json`",
|
|
898
|
+
"- Run `blodemd dev` to preview locally",
|
|
899
|
+
"- Run `blodemd validate` before publishing",
|
|
900
|
+
"- Run `blodemd push` to deploy",
|
|
901
|
+
"",
|
|
902
|
+
"## Terminology",
|
|
903
|
+
"",
|
|
904
|
+
"{/* Add product-specific terms and preferred usage */}",
|
|
905
|
+
"{/* Example: Use \"workspace\" not \"project\", \"member\" not \"user\" */}",
|
|
906
|
+
"",
|
|
907
|
+
"## Style preferences",
|
|
908
|
+
"",
|
|
909
|
+
"{/* Add any project-specific style rules below */}",
|
|
910
|
+
"",
|
|
911
|
+
"- Use active voice and second person (\"you\")",
|
|
912
|
+
"- Keep sentences concise and task-oriented",
|
|
913
|
+
"- Use sentence case for headings",
|
|
914
|
+
"- Bold UI labels: Click **Settings**",
|
|
915
|
+
"- Use code formatting for file names, commands, paths, JSON fields, and code references",
|
|
916
|
+
"",
|
|
917
|
+
"## Content boundaries",
|
|
918
|
+
"",
|
|
919
|
+
"{/* Define what should and shouldn't be documented */}",
|
|
920
|
+
"{/* Example: Don't document internal admin features */}",
|
|
921
|
+
"",
|
|
922
|
+
"## Workflow reminders",
|
|
923
|
+
"",
|
|
924
|
+
"- Content lives in MDX files next to `docs.json`.",
|
|
925
|
+
"- Update `docs.json` when navigation or branding changes.",
|
|
926
|
+
"- Prefer concise, task-oriented documentation.",
|
|
927
|
+
"- Run `blodemd validate` before publishing.",
|
|
928
|
+
""
|
|
929
|
+
].join("\n");
|
|
930
|
+
const createMinimalFiles = (projectSlug) => [{
|
|
931
|
+
content: stringifyJson(createMinimalDocsJson(projectSlug)),
|
|
932
|
+
path: "docs.json"
|
|
933
|
+
}, {
|
|
934
|
+
content: "---\ntitle: Welcome\n---\n\nStart writing your docs here.\n",
|
|
935
|
+
path: "index.mdx"
|
|
936
|
+
}];
|
|
937
|
+
const createStarterFiles = (projectSlug) => [
|
|
938
|
+
{
|
|
939
|
+
content: stringifyJson(createStarterDocsJson(projectSlug)),
|
|
940
|
+
path: "docs.json"
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
content: [
|
|
944
|
+
"---",
|
|
945
|
+
"title: Welcome",
|
|
946
|
+
"description: Start here.",
|
|
947
|
+
"---",
|
|
948
|
+
"",
|
|
949
|
+
"# Welcome",
|
|
950
|
+
"",
|
|
951
|
+
"This starter gives you branded assets, repo helper files, and a small docs structure you can rewrite quickly.",
|
|
952
|
+
"",
|
|
953
|
+
"",
|
|
954
|
+
"",
|
|
955
|
+
"## What is included",
|
|
956
|
+
"",
|
|
957
|
+
"- A starter `docs.json` with branding, contextual actions, and navigation.",
|
|
958
|
+
"- Placeholder brand assets in `/logo` and `/images`.",
|
|
959
|
+
"- Repo helper files like `.gitignore`, `README.md`, `AGENTS.md`, and `CLAUDE.md`.",
|
|
960
|
+
"",
|
|
961
|
+
"## Next steps",
|
|
962
|
+
"",
|
|
963
|
+
"- Confirm the `name` field in `docs.json` matches your project slug.",
|
|
964
|
+
"- Set `description` in `docs.json` to explain your product.",
|
|
965
|
+
"- Replace the files in `/logo` and `/images` with your own brand assets.",
|
|
966
|
+
"- Rewrite `CLAUDE.md` with your terminology and writing standards.",
|
|
967
|
+
"- Update this page, then preview locally with `blodemd dev`.",
|
|
968
|
+
"",
|
|
969
|
+
"## Included pages",
|
|
970
|
+
"",
|
|
971
|
+
"- [Quickstart](quickstart)",
|
|
972
|
+
"- [Development](development)",
|
|
973
|
+
""
|
|
974
|
+
].join("\n"),
|
|
975
|
+
path: "index.mdx"
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
content: [
|
|
979
|
+
"---",
|
|
980
|
+
"title: Quickstart",
|
|
981
|
+
"description: Get your docs running fast.",
|
|
982
|
+
"---",
|
|
983
|
+
"",
|
|
984
|
+
"# Quickstart",
|
|
985
|
+
"",
|
|
986
|
+
"",
|
|
987
|
+
"",
|
|
988
|
+
"1. Confirm the `name` field in `docs.json`.",
|
|
989
|
+
"2. Update the `description` field to match your product.",
|
|
990
|
+
"3. Replace the assets in `/logo` and `/images`.",
|
|
991
|
+
"4. Run `blodemd dev` to preview locally.",
|
|
992
|
+
"5. Run `blodemd push` when you are ready to publish.",
|
|
993
|
+
""
|
|
994
|
+
].join("\n"),
|
|
995
|
+
path: "quickstart.mdx"
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
content: [
|
|
999
|
+
"---",
|
|
1000
|
+
"title: Development",
|
|
1001
|
+
"description: Work on your docs locally.",
|
|
1002
|
+
"---",
|
|
1003
|
+
"",
|
|
1004
|
+
"# Development",
|
|
1005
|
+
"",
|
|
1006
|
+
"",
|
|
1007
|
+
"",
|
|
1008
|
+
"Preview locally with:",
|
|
1009
|
+
"",
|
|
1010
|
+
"```bash",
|
|
1011
|
+
"blodemd dev",
|
|
1012
|
+
"```",
|
|
1013
|
+
"",
|
|
1014
|
+
"Validate your configuration with:",
|
|
1015
|
+
"",
|
|
1016
|
+
"```bash",
|
|
1017
|
+
"blodemd validate",
|
|
1018
|
+
"```",
|
|
1019
|
+
"",
|
|
1020
|
+
"Keep `CLAUDE.md` current as your product terminology and writing rules evolve.",
|
|
1021
|
+
""
|
|
1022
|
+
].join("\n"),
|
|
1023
|
+
path: "development.mdx"
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
content: [
|
|
1027
|
+
"# Documentation starter",
|
|
1028
|
+
"",
|
|
1029
|
+
"This directory was scaffolded with `blodemd new --template starter`.",
|
|
1030
|
+
"",
|
|
1031
|
+
"## What is included",
|
|
1032
|
+
"",
|
|
1033
|
+
"- `docs.json` with branding, contextual actions, and starter navigation",
|
|
1034
|
+
"- `index.mdx`, `quickstart.mdx`, and `development.mdx`",
|
|
1035
|
+
"- Placeholder brand assets in `/logo` and `/images`",
|
|
1036
|
+
"- Repo helper files: `.gitignore`, `README.md`, `AGENTS.md`, and `CLAUDE.md`",
|
|
1037
|
+
"",
|
|
1038
|
+
"## Commands",
|
|
1039
|
+
"",
|
|
1040
|
+
"```bash",
|
|
1041
|
+
"blodemd dev",
|
|
1042
|
+
"blodemd validate",
|
|
1043
|
+
"blodemd push",
|
|
1044
|
+
"```",
|
|
1045
|
+
"",
|
|
1046
|
+
"## Customize",
|
|
1047
|
+
"",
|
|
1048
|
+
"- Confirm the project slug and set the description in `docs.json`.",
|
|
1049
|
+
"- Replace the assets in `/logo` and `/images`.",
|
|
1050
|
+
"- Rewrite `CLAUDE.md` with project-specific terminology and writing rules.",
|
|
1051
|
+
"- Rewrite the starter pages to match your product.",
|
|
1052
|
+
"- Add a `LICENSE` file deliberately if this repo will be public.",
|
|
1053
|
+
""
|
|
1054
|
+
].join("\n"),
|
|
1055
|
+
path: "README.md"
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
fallbackContent: claudeInstructions,
|
|
1059
|
+
path: "AGENTS.md",
|
|
1060
|
+
target: "CLAUDE.md",
|
|
1061
|
+
type: "symlink"
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
content: claudeInstructions,
|
|
1065
|
+
path: "CLAUDE.md"
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
content: [
|
|
1069
|
+
"# dependencies",
|
|
1070
|
+
"node_modules/",
|
|
1071
|
+
"",
|
|
1072
|
+
"# local env files",
|
|
1073
|
+
".env*",
|
|
1074
|
+
"!.env.example",
|
|
1075
|
+
"",
|
|
1076
|
+
"# build and cache",
|
|
1077
|
+
".next/",
|
|
1078
|
+
".turbo/",
|
|
1079
|
+
"coverage/",
|
|
1080
|
+
"dist/",
|
|
1081
|
+
".vercel/",
|
|
1082
|
+
"*.tsbuildinfo",
|
|
1083
|
+
"",
|
|
1084
|
+
"# logs",
|
|
1085
|
+
"*.log",
|
|
1086
|
+
"",
|
|
1087
|
+
"# misc",
|
|
1088
|
+
".DS_Store",
|
|
1089
|
+
""
|
|
1090
|
+
].join("\n"),
|
|
1091
|
+
path: ".gitignore"
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
content: [
|
|
1095
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\" fill=\"none\">",
|
|
1096
|
+
" <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#0D9373\"/>",
|
|
1097
|
+
" <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#CFF6EE\"/>",
|
|
1098
|
+
" <path d=\"M28 26h6c5.523 0 10 4.477 10 10s-4.477 10-10 10h-6V26Z\" fill=\"#0C3A33\"/>",
|
|
1099
|
+
"</svg>",
|
|
1100
|
+
""
|
|
1101
|
+
].join("\n"),
|
|
1102
|
+
path: "favicon.svg"
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
content: [
|
|
1106
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 240 64\" fill=\"none\">",
|
|
1107
|
+
" <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#0C3A33\"/>",
|
|
1108
|
+
" <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#CFF6EE\"/>",
|
|
1109
|
+
` <text x="84" y="41" fill="#111827" font-family="Arial, sans-serif" font-size="28" font-weight="700">${projectSlug}</text>`,
|
|
1110
|
+
"</svg>",
|
|
1111
|
+
""
|
|
1112
|
+
].join("\n"),
|
|
1113
|
+
path: "logo/light.svg"
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
content: [
|
|
1117
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 240 64\" fill=\"none\">",
|
|
1118
|
+
" <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#CFF6EE\"/>",
|
|
1119
|
+
" <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#0C3A33\"/>",
|
|
1120
|
+
` <text x="84" y="41" fill="#F9FAFB" font-family="Arial, sans-serif" font-size="28" font-weight="700">${projectSlug}</text>`,
|
|
1121
|
+
"</svg>",
|
|
1122
|
+
""
|
|
1123
|
+
].join("\n"),
|
|
1124
|
+
path: "logo/dark.svg"
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
content: [
|
|
1128
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
|
|
1129
|
+
" <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#F4FBF8\"/>",
|
|
1130
|
+
" <rect x=\"48\" y=\"48\" width=\"260\" height=\"424\" rx=\"24\" fill=\"#E1F4EE\"/>",
|
|
1131
|
+
" <rect x=\"96\" y=\"120\" width=\"164\" height=\"20\" rx=\"10\" fill=\"#0D9373\" opacity=\".25\"/>",
|
|
1132
|
+
" <rect x=\"96\" y=\"164\" width=\"132\" height=\"16\" rx=\"8\" fill=\"#0D9373\" opacity=\".18\"/>",
|
|
1133
|
+
" <rect x=\"96\" y=\"204\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#0D9373\" opacity=\".18\"/>",
|
|
1134
|
+
" <rect x=\"356\" y=\"80\" width=\"556\" height=\"104\" rx=\"24\" fill=\"#0D9373\"/>",
|
|
1135
|
+
" <rect x=\"388\" y=\"116\" width=\"220\" height=\"18\" rx=\"9\" fill=\"#CFF6EE\"/>",
|
|
1136
|
+
" <rect x=\"388\" y=\"148\" width=\"156\" height=\"14\" rx=\"7\" fill=\"#CFF6EE\" opacity=\".7\"/>",
|
|
1137
|
+
" <rect x=\"356\" y=\"216\" width=\"268\" height=\"256\" rx=\"24\" fill=\"#FFFFFF\"/>",
|
|
1138
|
+
" <rect x=\"388\" y=\"260\" width=\"168\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".18\"/>",
|
|
1139
|
+
" <rect x=\"388\" y=\"292\" width=\"196\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".12\"/>",
|
|
1140
|
+
" <rect x=\"656\" y=\"216\" width=\"256\" height=\"256\" rx=\"24\" fill=\"#0C3A33\"/>",
|
|
1141
|
+
" <rect x=\"692\" y=\"260\" width=\"128\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".85\"/>",
|
|
1142
|
+
" <rect x=\"692\" y=\"292\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".45\"/>",
|
|
1143
|
+
" <circle cx=\"804\" cy=\"380\" r=\"52\" fill=\"#0D9373\"/>",
|
|
1144
|
+
"</svg>",
|
|
1145
|
+
""
|
|
1146
|
+
].join("\n"),
|
|
1147
|
+
path: "images/hero-light.svg"
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
content: [
|
|
1151
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
|
|
1152
|
+
" <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#071715\"/>",
|
|
1153
|
+
" <rect x=\"48\" y=\"48\" width=\"260\" height=\"424\" rx=\"24\" fill=\"#0F2E28\"/>",
|
|
1154
|
+
" <rect x=\"96\" y=\"120\" width=\"164\" height=\"20\" rx=\"10\" fill=\"#CFF6EE\" opacity=\".18\"/>",
|
|
1155
|
+
" <rect x=\"96\" y=\"164\" width=\"132\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".14\"/>",
|
|
1156
|
+
" <rect x=\"96\" y=\"204\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".14\"/>",
|
|
1157
|
+
" <rect x=\"356\" y=\"80\" width=\"556\" height=\"104\" rx=\"24\" fill=\"#0D9373\"/>",
|
|
1158
|
+
" <rect x=\"388\" y=\"116\" width=\"220\" height=\"18\" rx=\"9\" fill=\"#E8FFF9\"/>",
|
|
1159
|
+
" <rect x=\"388\" y=\"148\" width=\"156\" height=\"14\" rx=\"7\" fill=\"#E8FFF9\" opacity=\".6\"/>",
|
|
1160
|
+
" <rect x=\"356\" y=\"216\" width=\"268\" height=\"256\" rx=\"24\" fill=\"#0C3A33\"/>",
|
|
1161
|
+
" <rect x=\"388\" y=\"260\" width=\"168\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".22\"/>",
|
|
1162
|
+
" <rect x=\"388\" y=\"292\" width=\"196\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".16\"/>",
|
|
1163
|
+
" <rect x=\"656\" y=\"216\" width=\"256\" height=\"256\" rx=\"24\" fill=\"#E9FFF8\"/>",
|
|
1164
|
+
" <rect x=\"692\" y=\"260\" width=\"128\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".24\"/>",
|
|
1165
|
+
" <rect x=\"692\" y=\"292\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".12\"/>",
|
|
1166
|
+
" <circle cx=\"804\" cy=\"380\" r=\"52\" fill=\"#0D9373\"/>",
|
|
1167
|
+
"</svg>",
|
|
1168
|
+
""
|
|
1169
|
+
].join("\n"),
|
|
1170
|
+
path: "images/hero-dark.svg"
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
content: [
|
|
1174
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
|
|
1175
|
+
" <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#F8FCFA\"/>",
|
|
1176
|
+
" <rect x=\"60\" y=\"76\" width=\"840\" height=\"368\" rx=\"28\" fill=\"#FFFFFF\" stroke=\"#D7ECE6\" stroke-width=\"4\"/>",
|
|
1177
|
+
" <rect x=\"108\" y=\"124\" width=\"96\" height=\"96\" rx=\"24\" fill=\"#0D9373\"/>",
|
|
1178
|
+
" <path d=\"M136 172l18 18 38-48\" stroke=\"#CFF6EE\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"18\"/>",
|
|
1179
|
+
" <rect x=\"244\" y=\"132\" width=\"280\" height=\"24\" rx=\"12\" fill=\"#0C3A33\"/>",
|
|
1180
|
+
" <rect x=\"244\" y=\"176\" width=\"416\" height=\"18\" rx=\"9\" fill=\"#0C3A33\" opacity=\".16\"/>",
|
|
1181
|
+
" <rect x=\"244\" y=\"214\" width=\"340\" height=\"18\" rx=\"9\" fill=\"#0C3A33\" opacity=\".12\"/>",
|
|
1182
|
+
" <rect x=\"108\" y=\"280\" width=\"744\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
|
|
1183
|
+
" <rect x=\"108\" y=\"326\" width=\"520\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
|
|
1184
|
+
" <rect x=\"108\" y=\"372\" width=\"612\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
|
|
1185
|
+
"</svg>",
|
|
1186
|
+
""
|
|
1187
|
+
].join("\n"),
|
|
1188
|
+
path: "images/checks-passed.svg"
|
|
1189
|
+
}
|
|
1190
|
+
];
|
|
1191
|
+
const getScaffoldFiles = (template, options) => {
|
|
1192
|
+
const projectSlug = options?.projectSlug ?? "my-project";
|
|
1193
|
+
return template === "starter" ? createStarterFiles(projectSlug) : createMinimalFiles(projectSlug);
|
|
1194
|
+
};
|
|
1195
|
+
//#endregion
|
|
1196
|
+
//#region src/new-flow.ts
|
|
1197
|
+
const CREATE_IN_SUBDIRECTORY = "create-in-subdirectory";
|
|
1198
|
+
const SCAFFOLD_CURRENT_DIRECTORY = "scaffold-current-directory";
|
|
1199
|
+
const CANCEL_SCAFFOLD = "cancel";
|
|
1200
|
+
const resolveInitialDirectory = (options) => {
|
|
1201
|
+
if (options.directory) return {
|
|
1202
|
+
directory: options.directory,
|
|
1203
|
+
kind: "target"
|
|
1204
|
+
};
|
|
1205
|
+
if (!options.interactive) return {
|
|
1206
|
+
directory: DEFAULT_SCAFFOLD_DIRECTORY,
|
|
1207
|
+
kind: "target"
|
|
1208
|
+
};
|
|
1209
|
+
if (options.currentDirectoryEntries.length === 0) return {
|
|
1210
|
+
directory: ".",
|
|
1211
|
+
kind: "target"
|
|
1212
|
+
};
|
|
1213
|
+
return { kind: "prompt" };
|
|
1214
|
+
};
|
|
1215
|
+
const resolveDirectoryFromAction = (action, subdirectory) => {
|
|
1216
|
+
if (action === "scaffold-current-directory") return ".";
|
|
1217
|
+
if (action === "create-in-subdirectory") return subdirectory?.trim() || "docs";
|
|
1218
|
+
};
|
|
1219
|
+
//#endregion
|
|
671
1220
|
//#region src/oauth-callback.ts
|
|
672
1221
|
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>";
|
|
673
1222
|
const escapeHtml = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
@@ -753,8 +1302,7 @@ const MIN_SUPPORTED_NODE_VERSION = [
|
|
|
753
1302
|
17,
|
|
754
1303
|
0
|
|
755
1304
|
];
|
|
756
|
-
const
|
|
757
|
-
const SUPPORTED_NODE_RANGE = ">=20.17.0 <25";
|
|
1305
|
+
const SUPPORTED_NODE_RANGE = ">=20.17.0";
|
|
758
1306
|
const parseVersion = (input) => {
|
|
759
1307
|
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(input.trim());
|
|
760
1308
|
if (!match) return null;
|
|
@@ -779,7 +1327,6 @@ const isSupportedNodeVersion = (version) => {
|
|
|
779
1327
|
if (!parsed) return false;
|
|
780
1328
|
const [major, minor, patch] = parsed;
|
|
781
1329
|
const [minMajor, minMinor, minPatch] = MIN_SUPPORTED_NODE_VERSION;
|
|
782
|
-
if (major >= MAX_SUPPORTED_NODE_MAJOR) return false;
|
|
783
1330
|
if (major !== minMajor) return major > minMajor;
|
|
784
1331
|
if (minor !== minMinor) return minor > minMinor;
|
|
785
1332
|
return patch >= minPatch;
|
|
@@ -794,8 +1341,7 @@ const readCliVersion = (moduleUrl) => {
|
|
|
794
1341
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
795
1342
|
};
|
|
796
1343
|
//#endregion
|
|
797
|
-
//#region src/
|
|
798
|
-
const CONFIG_FILE = "docs.json";
|
|
1344
|
+
//#region src/upload.ts
|
|
799
1345
|
const TEXT_CONTENT_TYPES = {
|
|
800
1346
|
".css": "text/css; charset=utf-8",
|
|
801
1347
|
".html": "text/html; charset=utf-8",
|
|
@@ -808,11 +1354,37 @@ const TEXT_CONTENT_TYPES = {
|
|
|
808
1354
|
".yaml": "application/yaml; charset=utf-8",
|
|
809
1355
|
".yml": "application/yaml; charset=utf-8"
|
|
810
1356
|
};
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1357
|
+
const normalizeRelativePath = (root, filePath) => path.relative(root, filePath).split(path.sep).join("/");
|
|
1358
|
+
const getContentType = (filePath) => TEXT_CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
1359
|
+
const estimateUploadItemBytes = (item) => item.contentBase64.length + item.path.length + 64;
|
|
1360
|
+
const createUploadBatchItem = async (filePath, root, readFile) => {
|
|
1361
|
+
return {
|
|
1362
|
+
contentBase64: (await readFile(filePath)).toString("base64"),
|
|
1363
|
+
contentType: getContentType(filePath),
|
|
1364
|
+
path: normalizeRelativePath(root, filePath)
|
|
1365
|
+
};
|
|
815
1366
|
};
|
|
1367
|
+
const createUploadBatches = async function* createUploadBatchesGenerator({ files, maxBatchBytes, readFile = fs.readFile, root }) {
|
|
1368
|
+
let currentBatch = [];
|
|
1369
|
+
let currentBatchBytes = 0;
|
|
1370
|
+
for (const filePath of files) {
|
|
1371
|
+
const item = await createUploadBatchItem(filePath, root, readFile);
|
|
1372
|
+
if (shouldIgnoreRootDocsFile(item.path)) continue;
|
|
1373
|
+
const itemBytes = estimateUploadItemBytes(item);
|
|
1374
|
+
if (itemBytes > maxBatchBytes) throw new CliError(`File "${item.path}" is too large to upload in a single request.`, EXIT_CODES.VALIDATION, "Split the file into smaller pieces or raise the server request limit.");
|
|
1375
|
+
if (currentBatch.length > 0 && currentBatchBytes + itemBytes > maxBatchBytes) {
|
|
1376
|
+
yield currentBatch;
|
|
1377
|
+
currentBatch = [];
|
|
1378
|
+
currentBatchBytes = 0;
|
|
1379
|
+
}
|
|
1380
|
+
currentBatch.push(item);
|
|
1381
|
+
currentBatchBytes += itemBytes;
|
|
1382
|
+
}
|
|
1383
|
+
if (currentBatch.length > 0) yield currentBatch;
|
|
1384
|
+
};
|
|
1385
|
+
//#endregion
|
|
1386
|
+
//#region src/cli.ts
|
|
1387
|
+
const CONFIG_FILE = "docs.json";
|
|
816
1388
|
const readGitValue = (gitArgs) => {
|
|
817
1389
|
const result = spawnSync("git", gitArgs, {
|
|
818
1390
|
encoding: "utf8",
|
|
@@ -825,30 +1397,32 @@ const readGitValue = (gitArgs) => {
|
|
|
825
1397
|
if (result.status !== 0) return;
|
|
826
1398
|
return result.stdout.trim() || void 0;
|
|
827
1399
|
};
|
|
828
|
-
const normalizeRelativePath = (root, filePath) => path.relative(root, filePath).split(path.sep).join("/");
|
|
829
1400
|
const shouldSkipEntry = (name) => name.startsWith(".") || name === "node_modules";
|
|
830
|
-
const collectFiles = async (root) => {
|
|
831
|
-
const entries = await fs.readdir(
|
|
1401
|
+
const collectFiles = async (root, directory = root) => {
|
|
1402
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
832
1403
|
const files = [];
|
|
833
1404
|
for (const entry of entries) {
|
|
834
1405
|
if (shouldSkipEntry(entry.name)) continue;
|
|
835
|
-
const absolutePath = path.join(
|
|
1406
|
+
const absolutePath = path.join(directory, entry.name);
|
|
1407
|
+
const relativePath = path.relative(root, absolutePath).split(path.sep).join("/");
|
|
836
1408
|
if (entry.isDirectory()) {
|
|
837
|
-
files.push(...await collectFiles(absolutePath));
|
|
1409
|
+
files.push(...await collectFiles(root, absolutePath));
|
|
838
1410
|
continue;
|
|
839
1411
|
}
|
|
840
|
-
if (entry.isFile())
|
|
1412
|
+
if (entry.isFile()) {
|
|
1413
|
+
if (shouldIgnoreRootDocsFile(relativePath)) continue;
|
|
1414
|
+
files.push(absolutePath);
|
|
1415
|
+
}
|
|
841
1416
|
}
|
|
842
1417
|
return files.toSorted((left, right) => left.localeCompare(right));
|
|
843
1418
|
};
|
|
844
|
-
const getContentType = (filePath) => TEXT_CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
845
1419
|
const readJson = async (response) => {
|
|
846
|
-
const
|
|
847
|
-
if (!
|
|
1420
|
+
const responseText = await response.text();
|
|
1421
|
+
if (!responseText) return null;
|
|
848
1422
|
try {
|
|
849
|
-
return JSON.parse(
|
|
1423
|
+
return JSON.parse(responseText);
|
|
850
1424
|
} catch {
|
|
851
|
-
return
|
|
1425
|
+
return responseText;
|
|
852
1426
|
}
|
|
853
1427
|
};
|
|
854
1428
|
const requestJson = async (url, init, message) => {
|
|
@@ -860,11 +1434,6 @@ const requestJson = async (url, init, message) => {
|
|
|
860
1434
|
}
|
|
861
1435
|
return data;
|
|
862
1436
|
};
|
|
863
|
-
const parsePositiveInteger = (value, label) => {
|
|
864
|
-
const parsed = Number.parseInt(value, 10);
|
|
865
|
-
if (!Number.isInteger(parsed) || parsed <= 0) throw new CliError(`${label} must be a positive integer.`, EXIT_CODES.VALIDATION);
|
|
866
|
-
return parsed;
|
|
867
|
-
};
|
|
868
1437
|
const reportCommandError = (prefix, error) => {
|
|
869
1438
|
const cliError = toCliError(error);
|
|
870
1439
|
log.error(`${prefix}: ${cliError.message}`);
|
|
@@ -872,6 +1441,109 @@ const reportCommandError = (prefix, error) => {
|
|
|
872
1441
|
log.info("Failed");
|
|
873
1442
|
process.exitCode = cliError.exitCode;
|
|
874
1443
|
};
|
|
1444
|
+
const parseScaffoldTemplate = (value) => {
|
|
1445
|
+
if (isScaffoldTemplate(value)) return value;
|
|
1446
|
+
throw new InvalidArgumentError(`Expected one of: ${SCAFFOLD_TEMPLATES.join(", ")}.`);
|
|
1447
|
+
};
|
|
1448
|
+
const parseProjectSlug = (value) => {
|
|
1449
|
+
const validationError = validateProjectSlug(value);
|
|
1450
|
+
if (validationError) throw new InvalidArgumentError(validationError);
|
|
1451
|
+
return value.trim();
|
|
1452
|
+
};
|
|
1453
|
+
const isInteractiveTerminal = () => process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1454
|
+
const promptForNoArgDirectoryAction = async () => {
|
|
1455
|
+
const directoryAction = await select({
|
|
1456
|
+
initialValue: CREATE_IN_SUBDIRECTORY,
|
|
1457
|
+
message: "Directory . is not empty. What would you like to do?",
|
|
1458
|
+
options: [
|
|
1459
|
+
{
|
|
1460
|
+
hint: "recommended",
|
|
1461
|
+
label: "Create in a subdirectory",
|
|
1462
|
+
value: CREATE_IN_SUBDIRECTORY
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
hint: "scaffold here",
|
|
1466
|
+
label: "Scaffold current directory",
|
|
1467
|
+
value: SCAFFOLD_CURRENT_DIRECTORY
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
label: "Cancel",
|
|
1471
|
+
value: CANCEL_SCAFFOLD
|
|
1472
|
+
}
|
|
1473
|
+
]
|
|
1474
|
+
});
|
|
1475
|
+
if (isCancel(directoryAction)) return;
|
|
1476
|
+
return directoryAction;
|
|
1477
|
+
};
|
|
1478
|
+
const promptForSubdirectoryName = async () => {
|
|
1479
|
+
const subdirectory = await text({
|
|
1480
|
+
initialValue: DEFAULT_SCAFFOLD_DIRECTORY,
|
|
1481
|
+
message: "Subdirectory name",
|
|
1482
|
+
placeholder: DEFAULT_SCAFFOLD_DIRECTORY,
|
|
1483
|
+
validate: (value) => {
|
|
1484
|
+
if (!value?.trim()) return "Subdirectory name is required.";
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
if (isCancel(subdirectory)) return;
|
|
1488
|
+
return subdirectory.trim();
|
|
1489
|
+
};
|
|
1490
|
+
const promptForProjectSlug = async (initialValue) => {
|
|
1491
|
+
const projectSlug = await text({
|
|
1492
|
+
initialValue,
|
|
1493
|
+
message: "Project slug",
|
|
1494
|
+
placeholder: initialValue,
|
|
1495
|
+
validate: validateProjectSlug
|
|
1496
|
+
});
|
|
1497
|
+
if (isCancel(projectSlug)) return;
|
|
1498
|
+
return projectSlug.trim();
|
|
1499
|
+
};
|
|
1500
|
+
const resolveRequestedDirectory = async (directory, shouldPrompt) => {
|
|
1501
|
+
let currentDirectoryEntries = [];
|
|
1502
|
+
if (!directory && shouldPrompt) currentDirectoryEntries = await fs.readdir(process.cwd());
|
|
1503
|
+
const initialResolution = resolveInitialDirectory({
|
|
1504
|
+
currentDirectoryEntries,
|
|
1505
|
+
directory,
|
|
1506
|
+
interactive: shouldPrompt
|
|
1507
|
+
});
|
|
1508
|
+
if (initialResolution.kind === "target") return { directory: initialResolution.directory };
|
|
1509
|
+
const directoryAction = await promptForNoArgDirectoryAction();
|
|
1510
|
+
if (!directoryAction || directoryAction === "cancel") return;
|
|
1511
|
+
const resolvedDirectory = resolveDirectoryFromAction(directoryAction, directoryAction === "create-in-subdirectory" ? await promptForSubdirectoryName() : void 0);
|
|
1512
|
+
if (!resolvedDirectory) return;
|
|
1513
|
+
return {
|
|
1514
|
+
directory: resolvedDirectory,
|
|
1515
|
+
skipNonEmptyConfirmation: directoryAction === SCAFFOLD_CURRENT_DIRECTORY
|
|
1516
|
+
};
|
|
1517
|
+
};
|
|
1518
|
+
const confirmScaffoldTarget = async (root, template, shouldPrompt, options) => {
|
|
1519
|
+
const existingTarget = await fs.lstat(root).catch(() => null);
|
|
1520
|
+
if (existingTarget && !existingTarget.isDirectory()) throw new Error(`Target path already exists and is not a directory: ${root}`);
|
|
1521
|
+
if (!existingTarget?.isDirectory()) return true;
|
|
1522
|
+
const existingScaffoldPaths = await findExistingPaths(root, getScaffoldFiles(template).map((file) => file.path));
|
|
1523
|
+
if (existingScaffoldPaths.length > 0) throw new Error(`Target directory already contains scaffold files: ${existingScaffoldPaths.join(", ")}. Use a different directory or remove those files first.`);
|
|
1524
|
+
if ((await fs.readdir(root)).toSorted((left, right) => left.localeCompare(right)).length === 0) return true;
|
|
1525
|
+
if (options?.skipNonEmptyConfirmation) return true;
|
|
1526
|
+
if (!shouldPrompt) throw new Error(`Target directory is not empty: ${root}. Choose an empty directory or run this command in an interactive terminal to confirm scaffolding there.`);
|
|
1527
|
+
const shouldContinue = await confirm({ message: `Scaffold into the non-empty directory ${root}? Existing files will be left untouched.` });
|
|
1528
|
+
return !isCancel(shouldContinue) && shouldContinue;
|
|
1529
|
+
};
|
|
1530
|
+
const resolveProjectSlug = async (providedName, directory, shouldPrompt) => {
|
|
1531
|
+
const defaultProjectSlug = deriveDefaultProjectSlug(directory, process.cwd());
|
|
1532
|
+
if (providedName) return providedName;
|
|
1533
|
+
if (!shouldPrompt) return defaultProjectSlug;
|
|
1534
|
+
return await promptForProjectSlug(defaultProjectSlug);
|
|
1535
|
+
};
|
|
1536
|
+
const writeScaffoldFiles = async (root, template, projectSlug) => {
|
|
1537
|
+
for (const file of getScaffoldFiles(template, { projectSlug })) {
|
|
1538
|
+
const filePath = path.join(root, file.path);
|
|
1539
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1540
|
+
if (file.type === "symlink") {
|
|
1541
|
+
await writeSymlinkIfMissing(filePath, file.target, { fallbackContent: file.fallbackContent });
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
await writeFileIfMissing(filePath, file.content);
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
875
1547
|
const fetchUserEmail = async (apiUrl, token) => {
|
|
876
1548
|
try {
|
|
877
1549
|
return (await requestJson(`${apiUrl}/auth/me`, { headers: { Authorization: `Bearer ${token}` } }, "Failed to fetch user info")).email;
|
|
@@ -919,32 +1591,47 @@ const autoCreateProject = async (project, apiUrl, headers) => {
|
|
|
919
1591
|
log.info(`API key for CI: ${chalk.dim(createResult.token)}`);
|
|
920
1592
|
return true;
|
|
921
1593
|
};
|
|
1594
|
+
const scaffoldDocsSite = async (directory, options) => {
|
|
1595
|
+
intro(chalk.bold("blodemd new"));
|
|
1596
|
+
if (options?.deprecatedCommand) log.warn(`"${options.deprecatedCommand}" is deprecated. Use ${chalk.cyan("blodemd new")} instead.`);
|
|
1597
|
+
try {
|
|
1598
|
+
const template = options?.template ?? "minimal";
|
|
1599
|
+
const shouldPrompt = isInteractiveTerminal() && !options?.yes;
|
|
1600
|
+
const selectedDirectory = await resolveRequestedDirectory(directory, shouldPrompt);
|
|
1601
|
+
if (!selectedDirectory) {
|
|
1602
|
+
log.warn("Cancelled");
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const resolvedDirectory = resolveScaffoldDirectory(selectedDirectory.directory);
|
|
1606
|
+
const root = path.resolve(process.cwd(), resolvedDirectory);
|
|
1607
|
+
if (!await confirmScaffoldTarget(root, template, shouldPrompt, { skipNonEmptyConfirmation: selectedDirectory.skipNonEmptyConfirmation })) {
|
|
1608
|
+
log.warn("Cancelled");
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const projectSlug = await resolveProjectSlug(options?.name, resolvedDirectory, shouldPrompt);
|
|
1612
|
+
if (!projectSlug) {
|
|
1613
|
+
log.warn("Cancelled");
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
await fs.mkdir(root, { recursive: true });
|
|
1617
|
+
await writeScaffoldFiles(root, template, projectSlug);
|
|
1618
|
+
log.success(`Docs scaffolded in ${chalk.cyan(root)}`);
|
|
1619
|
+
if (template === "starter") log.info("Starter template includes brand assets and helper files.");
|
|
1620
|
+
log.info(`Project slug: ${chalk.cyan(projectSlug)}`);
|
|
1621
|
+
log.info("Done");
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
reportCommandError("New failed", error);
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
922
1626
|
const MAX_BATCH_BYTES = 4 * 1024 * 1024;
|
|
923
1627
|
const uploadFiles = async (files, root, apiPath, deploymentId, headers, s) => {
|
|
924
1628
|
s.start(`Uploading ${files.length} files`);
|
|
925
|
-
const items = await Promise.all(files.map(async (filePath) => {
|
|
926
|
-
return {
|
|
927
|
-
contentBase64: (await fs.readFile(filePath)).toString("base64"),
|
|
928
|
-
contentType: getContentType(filePath),
|
|
929
|
-
path: normalizeRelativePath(root, filePath)
|
|
930
|
-
};
|
|
931
|
-
}));
|
|
932
|
-
const batches = [];
|
|
933
|
-
let current = [];
|
|
934
|
-
let currentBytes = 0;
|
|
935
|
-
for (const item of items) {
|
|
936
|
-
const itemBytes = item.contentBase64.length + item.path.length + 64;
|
|
937
|
-
if (current.length > 0 && currentBytes + itemBytes > MAX_BATCH_BYTES) {
|
|
938
|
-
batches.push(current);
|
|
939
|
-
current = [];
|
|
940
|
-
currentBytes = 0;
|
|
941
|
-
}
|
|
942
|
-
current.push(item);
|
|
943
|
-
currentBytes += itemBytes;
|
|
944
|
-
}
|
|
945
|
-
if (current.length > 0) batches.push(current);
|
|
946
1629
|
let uploaded = 0;
|
|
947
|
-
for (const batch of
|
|
1630
|
+
for await (const batch of createUploadBatches({
|
|
1631
|
+
files,
|
|
1632
|
+
maxBatchBytes: MAX_BATCH_BYTES,
|
|
1633
|
+
root
|
|
1634
|
+
})) {
|
|
948
1635
|
await requestJson(apiPath(`/${deploymentId}/files/batch`), {
|
|
949
1636
|
body: JSON.stringify({ files: batch }),
|
|
950
1637
|
headers,
|
|
@@ -986,7 +1673,7 @@ program.command("login").description("Authenticate with Blode.md").option("--tok
|
|
|
986
1673
|
}
|
|
987
1674
|
const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
|
|
988
1675
|
const clientId = OAUTH_CLIENT_ID;
|
|
989
|
-
const port =
|
|
1676
|
+
const port = parsePort(options.port);
|
|
990
1677
|
const timeoutSeconds = parsePositiveInteger(options.timeout, "Timeout");
|
|
991
1678
|
const redirectUrl = new URL(`http://127.0.0.1:${port}${DEFAULT_OAUTH_CALLBACK_PATH}`);
|
|
992
1679
|
const state = createOAuthState();
|
|
@@ -1030,9 +1717,15 @@ program.command("login").description("Authenticate with Blode.md").option("--tok
|
|
|
1030
1717
|
program.command("logout").description("Remove stored credentials").action(async () => {
|
|
1031
1718
|
intro(chalk.bold("blodemd logout"));
|
|
1032
1719
|
try {
|
|
1033
|
-
|
|
1720
|
+
let existing = false;
|
|
1721
|
+
try {
|
|
1722
|
+
await fs.access(CREDENTIALS_FILE);
|
|
1723
|
+
existing = true;
|
|
1724
|
+
} catch {
|
|
1725
|
+
existing = false;
|
|
1726
|
+
}
|
|
1034
1727
|
await clearStoredCredentials();
|
|
1035
|
-
if (existing
|
|
1728
|
+
if (existing) log.success("Credentials removed.");
|
|
1036
1729
|
else log.info("No stored credentials found.");
|
|
1037
1730
|
log.info("Done");
|
|
1038
1731
|
} catch (error) {
|
|
@@ -1064,28 +1757,20 @@ program.command("whoami").description("Show current authentication").action(asyn
|
|
|
1064
1757
|
reportCommandError("Whoami failed", error);
|
|
1065
1758
|
}
|
|
1066
1759
|
});
|
|
1067
|
-
program.command("
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}, null, 2)}\n`);
|
|
1082
|
-
await ensureFile(path.join(root, "index.mdx"), "---\ntitle: Welcome\n---\n\nStart writing your docs here.\n");
|
|
1083
|
-
log.success(`Docs scaffolded in ${chalk.cyan(root)}`);
|
|
1084
|
-
log.info(`Set ${chalk.cyan("name")} in docs.json to your project slug.`);
|
|
1085
|
-
log.info("Done");
|
|
1086
|
-
} catch (error) {
|
|
1087
|
-
reportCommandError("Init failed", error);
|
|
1088
|
-
}
|
|
1760
|
+
program.command("new").description("Create a new blode.md documentation site").argument("[directory]", "target directory").option("--name <slug>", "project slug for docs.json", parseProjectSlug).option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
|
|
1761
|
+
await scaffoldDocsSite(directory, {
|
|
1762
|
+
name: options.name,
|
|
1763
|
+
template: options.template,
|
|
1764
|
+
yes: options.yes
|
|
1765
|
+
});
|
|
1766
|
+
});
|
|
1767
|
+
program.command("init", { hidden: true }).argument("[directory]", "target directory").option("--name <slug>", "project slug for docs.json", parseProjectSlug).option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
|
|
1768
|
+
await scaffoldDocsSite(directory, {
|
|
1769
|
+
deprecatedCommand: "blodemd init",
|
|
1770
|
+
name: options.name,
|
|
1771
|
+
template: options.template,
|
|
1772
|
+
yes: options.yes
|
|
1773
|
+
});
|
|
1089
1774
|
});
|
|
1090
1775
|
program.command("validate").description("Validate docs.json").argument("[dir]", "docs directory").action(async (dir) => {
|
|
1091
1776
|
intro(chalk.bold("blodemd validate"));
|