blodemd 0.0.12 → 0.0.14
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 +1 -1
- package/dev-server/lib/local-runtime.tsx +12 -15
- package/dev-server/next.config.js +19 -9
- package/dev-server/package.json +5 -1
- package/dev-server/tsconfig.json +0 -3
- package/dist/cli.mjs +1205 -969
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +1 -1
- package/docs/components/api/api-playground.tsx +2 -5
- package/docs/components/api/api-reference.tsx +29 -3
- package/docs/components/docs/contextual-menu.tsx +37 -33
- package/docs/components/docs/copy-page-menu.tsx +18 -0
- package/docs/components/docs/doc-header.tsx +4 -1
- package/docs/components/docs/doc-shell.tsx +48 -18
- package/docs/components/docs/doc-sidebar.tsx +131 -89
- package/docs/components/docs/doc-toc.tsx +1 -0
- package/docs/components/docs/mobile-nav.tsx +72 -68
- package/docs/components/docs/sidebar-active-highlight.tsx +34 -0
- package/docs/components/docs/sidebar-scroll-area.tsx +9 -4
- package/docs/components/git/repo-picker.tsx +5 -2
- package/docs/components/mdx/index.tsx +31 -21
- package/docs/components/posthog-provider.tsx +42 -0
- package/docs/components/tenant-analytics.tsx +49 -0
- package/docs/components/third-parties.tsx +4 -1
- package/docs/components/ui/copy-button.tsx +16 -2
- package/docs/components/ui/search-dialog.tsx +431 -0
- package/docs/components/ui/search.tsx +44 -427
- package/docs/components/web-mcp.tsx +115 -0
- package/docs/lib/agent-skills.ts +48 -0
- package/docs/lib/config.ts +4 -5
- package/docs/lib/docs-collection.ts +7 -22
- package/docs/lib/docs-runtime.tsx +160 -10
- package/docs/lib/llms-full.ts +82 -0
- package/docs/lib/marketing-markdown.ts +79 -0
- package/docs/lib/marketing-site.ts +31 -0
- package/docs/lib/mdx.ts +17 -6
- package/docs/lib/routes.ts +19 -3
- package/docs/lib/shiki.ts +4 -0
- package/docs/lib/tenancy.ts +15 -2
- package/docs/lib/tenant-headers.ts +29 -0
- package/docs/lib/tenant-static.ts +73 -99
- package/package.json +10 -5
- package/packages/@repo/common/package.json +3 -3
- package/packages/@repo/contracts/dist/analytics.d.ts +21 -0
- package/packages/@repo/contracts/dist/analytics.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/analytics.js +18 -0
- package/packages/@repo/contracts/dist/deployment.js +1 -1
- package/packages/@repo/contracts/dist/git.js +2 -2
- package/packages/@repo/contracts/dist/ids.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/ids.js +2 -1
- package/packages/@repo/contracts/dist/index.d.ts +1 -0
- package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/index.js +1 -0
- package/packages/@repo/contracts/dist/project.d.ts +18 -1
- package/packages/@repo/contracts/dist/project.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/project.js +3 -1
- package/packages/@repo/contracts/dist/tenant.d.ts +36 -0
- package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/tenant.js +2 -0
- package/packages/@repo/contracts/package.json +3 -3
- package/packages/@repo/contracts/src/analytics.ts +33 -0
- package/packages/@repo/contracts/src/deployment.ts +1 -1
- package/packages/@repo/contracts/src/git.ts +2 -2
- package/packages/@repo/contracts/src/ids.ts +2 -1
- package/packages/@repo/contracts/src/index.ts +1 -0
- package/packages/@repo/contracts/src/project.ts +3 -1
- package/packages/@repo/contracts/src/tenant.ts +2 -0
- package/packages/@repo/models/dist/docs-config.d.ts +0 -198
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +3 -2
- package/packages/@repo/models/dist/tenant.d.ts +12 -0
- package/packages/@repo/models/dist/tenant.d.ts.map +1 -1
- package/packages/@repo/models/package.json +3 -3
- package/packages/@repo/models/src/docs-config.ts +3 -3
- package/packages/@repo/models/src/tenant.ts +15 -0
- package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -1
- package/packages/@repo/prebuild/dist/openapi.js +32 -4
- package/packages/@repo/prebuild/package.json +3 -3
- package/packages/@repo/prebuild/src/openapi.ts +42 -4
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/blob-source.js +18 -5
- package/packages/@repo/previewing/dist/config-loader.d.ts +134 -0
- package/packages/@repo/previewing/dist/config-loader.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/config-loader.js +231 -0
- package/packages/@repo/previewing/dist/constants.d.ts +15 -0
- package/packages/@repo/previewing/dist/constants.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/constants.js +14 -0
- package/packages/@repo/previewing/dist/content-index.d.ts +7 -0
- package/packages/@repo/previewing/dist/content-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/content-index.js +291 -0
- package/packages/@repo/previewing/dist/index.d.ts +13 -121
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +11 -1003
- package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +3 -0
- package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/agent-markdown.js +176 -0
- package/packages/@repo/previewing/dist/markdown/format.d.ts +8 -0
- package/packages/@repo/previewing/dist/markdown/format.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/format.js +43 -0
- package/packages/@repo/previewing/dist/markdown/links.d.ts +3 -0
- package/packages/@repo/previewing/dist/markdown/links.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/links.js +31 -0
- package/packages/@repo/previewing/dist/openapi-pages.d.ts +7 -0
- package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/openapi-pages.js +200 -0
- package/packages/@repo/previewing/dist/search-index.d.ts +4 -0
- package/packages/@repo/previewing/dist/search-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/search-index.js +23 -0
- package/packages/@repo/previewing/dist/serialization.d.ts +13 -0
- package/packages/@repo/previewing/dist/serialization.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/serialization.js +108 -0
- package/packages/@repo/previewing/dist/toc-index.d.ts +5 -0
- package/packages/@repo/previewing/dist/toc-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/toc-index.js +53 -0
- package/packages/@repo/previewing/dist/types.d.ts +92 -0
- package/packages/@repo/previewing/dist/types.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/types.js +1 -0
- package/packages/@repo/previewing/dist/utility-index.d.ts +7 -0
- package/packages/@repo/previewing/dist/utility-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/utility-index.js +201 -0
- package/packages/@repo/previewing/package.json +3 -3
- package/packages/@repo/previewing/src/blob-source.ts +20 -5
- package/packages/@repo/previewing/src/config-loader.ts +316 -0
- package/packages/@repo/previewing/src/constants.ts +16 -0
- package/packages/@repo/previewing/src/content-index.ts +384 -0
- package/packages/@repo/previewing/src/index.ts +69 -1515
- package/packages/@repo/previewing/src/markdown/agent-markdown.ts +254 -0
- package/packages/@repo/previewing/src/markdown/format.ts +63 -0
- package/packages/@repo/previewing/src/markdown/links.ts +46 -0
- package/packages/@repo/previewing/src/openapi-pages.ts +319 -0
- package/packages/@repo/previewing/src/search-index.ts +38 -0
- package/packages/@repo/previewing/src/serialization.ts +191 -0
- package/packages/@repo/previewing/src/toc-index.ts +72 -0
- package/packages/@repo/previewing/src/types.ts +105 -0
- package/packages/@repo/previewing/src/utility-index.ts +304 -0
- package/packages/@repo/validation/package.json +3 -3
- package/scripts/repo-packages.mjs +8 -0
- package/docs/components/ui/site-footer.tsx +0 -41
package/dist/cli.mjs
CHANGED
|
@@ -1,45 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import {
|
|
4
|
-
import fs, { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
-
import path, { join } from "node:path";
|
|
3
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
6
4
|
import { confirm, intro, isCancel, log, select, spinner, text } from "@clack/prompts";
|
|
7
5
|
import chalk from "chalk";
|
|
8
|
-
import { Command, InvalidArgumentError } from "commander";
|
|
9
|
-
import open from "open";
|
|
10
6
|
import { homedir } from "node:os";
|
|
7
|
+
import path, { join } from "node:path";
|
|
8
|
+
import fs, { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import "yaml";
|
|
11
|
+
import open from "open";
|
|
12
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
13
|
+
import { createServer } from "node:http";
|
|
14
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
11
15
|
import { once } from "node:events";
|
|
12
|
-
import { createServer } from "node:net";
|
|
16
|
+
import { createServer as createServer$1 } from "node:net";
|
|
13
17
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
14
18
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { z } from "zod";
|
|
16
|
-
import "yaml";
|
|
17
19
|
import { watch } from "chokidar";
|
|
18
|
-
import { createServer as createServer$1 } from "node:http";
|
|
19
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
|
-
//#region ../../packages/common/dist/index.js
|
|
22
|
-
const BACKSLASH_TO_SLASH_REGEX = /\\/g;
|
|
23
|
-
const TRAILING_SLASHES_REGEX = /\/+$/g;
|
|
24
|
-
const LEADING_SLASHES_REGEX = /^\/+/;
|
|
25
|
-
const normalizePath = (value) => {
|
|
26
|
-
return value.replace(BACKSLASH_TO_SLASH_REGEX, "/").replace(TRAILING_SLASHES_REGEX, "").replace(LEADING_SLASHES_REGEX, "");
|
|
27
|
-
};
|
|
28
|
-
const slugify = (value) => value.toLowerCase().trim().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(^-|-$)+/g, "");
|
|
29
|
-
const IGNORED_ROOT_DOCS_FILES = new Set([
|
|
30
|
-
".gitignore",
|
|
31
|
-
"AGENTS.md",
|
|
32
|
-
"CLAUDE.md",
|
|
33
|
-
"LICENSE",
|
|
34
|
-
"LICENSE.md",
|
|
35
|
-
"README.md"
|
|
36
|
-
]);
|
|
37
|
-
const shouldIgnoreRootDocsFile = (value) => {
|
|
38
|
-
const normalized = normalizePath(value);
|
|
39
|
-
if (!normalized || normalized.includes("/")) return false;
|
|
40
|
-
return IGNORED_ROOT_DOCS_FILES.has(normalized);
|
|
41
|
-
};
|
|
42
|
-
//#endregion
|
|
43
21
|
//#region src/constants.ts
|
|
44
22
|
const CLI_NAME = "blodemd";
|
|
45
23
|
const BLODE_PROJECT_ENV = "BLODEMD_PROJECT";
|
|
@@ -300,24 +278,31 @@ const resolveTokenStatus = (token) => {
|
|
|
300
278
|
};
|
|
301
279
|
};
|
|
302
280
|
//#endregion
|
|
303
|
-
//#region
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new CliError(`${label} must be a positive integer.`, EXIT_CODES.VALIDATION);
|
|
310
|
-
return parsed;
|
|
281
|
+
//#region ../../packages/common/dist/index.js
|
|
282
|
+
const BACKSLASH_TO_SLASH_REGEX = /\\/g;
|
|
283
|
+
const TRAILING_SLASHES_REGEX = /\/+$/g;
|
|
284
|
+
const LEADING_SLASHES_REGEX = /^\/+/;
|
|
285
|
+
const normalizePath = (value) => {
|
|
286
|
+
return value.replace(BACKSLASH_TO_SLASH_REGEX, "/").replace(TRAILING_SLASHES_REGEX, "").replace(LEADING_SLASHES_REGEX, "");
|
|
311
287
|
};
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
288
|
+
const slugify = (value) => value.toLowerCase().trim().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(^-|-$)+/g, "");
|
|
289
|
+
const IGNORED_ROOT_DOCS_FILES = new Set([
|
|
290
|
+
".gitignore",
|
|
291
|
+
"AGENTS.md",
|
|
292
|
+
"CLAUDE.md",
|
|
293
|
+
"LICENSE",
|
|
294
|
+
"LICENSE.md",
|
|
295
|
+
"README.md"
|
|
296
|
+
]);
|
|
297
|
+
const shouldIgnoreRootDocsFile = (value) => {
|
|
298
|
+
const normalized = normalizePath(value);
|
|
299
|
+
if (!normalized || normalized.includes("/")) return false;
|
|
300
|
+
return IGNORED_ROOT_DOCS_FILES.has(normalized);
|
|
316
301
|
};
|
|
317
302
|
//#endregion
|
|
318
303
|
//#region ../../packages/models/dist/docs-config.js
|
|
319
304
|
const UrlOrPathSchema = z.string().min(1);
|
|
320
|
-
const SlugSchema = z.string().min(1).regex(/^[a-z0-9-]
|
|
305
|
+
const SlugSchema = z.string().min(1).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
|
|
321
306
|
const DocsColorsSchema = z.object({
|
|
322
307
|
background: z.string().optional(),
|
|
323
308
|
border: z.string().optional(),
|
|
@@ -740,69 +725,10 @@ const validateDocsConfig = (input) => {
|
|
|
740
725
|
};
|
|
741
726
|
};
|
|
742
727
|
//#endregion
|
|
743
|
-
//#region ../../packages/previewing/dist/
|
|
744
|
-
const IGNORED_DIRECTORIES = new Set([
|
|
745
|
-
"app",
|
|
746
|
-
"lib",
|
|
747
|
-
"node_modules",
|
|
748
|
-
"public"
|
|
749
|
-
]);
|
|
750
|
-
const isNotFoundError = (error) => Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
751
|
-
const isWithinRoot = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
752
|
-
const resolveWithinRoot = (root, relativePath) => {
|
|
753
|
-
const normalized = normalizePath(relativePath);
|
|
754
|
-
const absolutePath = path.resolve(root, normalized);
|
|
755
|
-
if (!isWithinRoot(root, absolutePath)) throw new Error(`Path "${relativePath}" escapes the content source root.`);
|
|
756
|
-
return absolutePath;
|
|
757
|
-
};
|
|
758
|
-
const walkFiles = async (directory, prefix) => {
|
|
759
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
760
|
-
const files = [];
|
|
761
|
-
for (const entry of entries) {
|
|
762
|
-
if (entry.name.startsWith(".")) continue;
|
|
763
|
-
const absolutePath = path.join(directory, entry.name);
|
|
764
|
-
const relativePath = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
765
|
-
if (entry.isDirectory()) {
|
|
766
|
-
if (IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
767
|
-
files.push(...await walkFiles(absolutePath, relativePath));
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
if (entry.isFile()) {
|
|
771
|
-
if (!prefix && shouldIgnoreRootDocsFile(entry.name)) continue;
|
|
772
|
-
files.push(normalizePath(relativePath));
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
return files;
|
|
776
|
-
};
|
|
777
|
-
var FsContentSource = class {
|
|
778
|
-
root;
|
|
779
|
-
constructor(root) {
|
|
780
|
-
this.root = path.resolve(root);
|
|
781
|
-
}
|
|
782
|
-
async readFile(relativePath) {
|
|
783
|
-
return await fs.readFile(resolveWithinRoot(this.root, relativePath), "utf8");
|
|
784
|
-
}
|
|
785
|
-
async listFiles(directory) {
|
|
786
|
-
return await walkFiles(resolveWithinRoot(this.root, directory), "");
|
|
787
|
-
}
|
|
788
|
-
async exists(relativePath) {
|
|
789
|
-
try {
|
|
790
|
-
await fs.access(resolveWithinRoot(this.root, relativePath));
|
|
791
|
-
return true;
|
|
792
|
-
} catch (error) {
|
|
793
|
-
if (isNotFoundError(error)) return false;
|
|
794
|
-
throw error;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
resolveUrl() {
|
|
798
|
-
return null;
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
const createFsSource = (root) => new FsContentSource(root);
|
|
802
|
-
//#endregion
|
|
803
|
-
//#region ../../packages/previewing/dist/index.js
|
|
728
|
+
//#region ../../packages/previewing/dist/constants.js
|
|
804
729
|
const LEGACY_PROJECT_NAME_FALLBACK_WARNING$1 = "docs.json.slug is recommended. Falling back to docs.json.name as the deployment slug is deprecated.";
|
|
805
|
-
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region ../../packages/previewing/dist/config-loader.js
|
|
806
732
|
const DOCS_CONFIG_FILE = "docs.json";
|
|
807
733
|
const defaultLinkLabel = (input) => {
|
|
808
734
|
if (input.label) return input.label;
|
|
@@ -950,6 +876,81 @@ const loadSiteConfig = async (source) => {
|
|
|
950
876
|
ok: false
|
|
951
877
|
};
|
|
952
878
|
};
|
|
879
|
+
new Set(PageModeSchema.options);
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region ../../packages/previewing/dist/fs-source.js
|
|
882
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
883
|
+
"app",
|
|
884
|
+
"lib",
|
|
885
|
+
"node_modules",
|
|
886
|
+
"public"
|
|
887
|
+
]);
|
|
888
|
+
const isNotFoundError = (error) => Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
889
|
+
const isWithinRoot = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
890
|
+
const resolveWithinRoot = (root, relativePath) => {
|
|
891
|
+
const normalized = normalizePath(relativePath);
|
|
892
|
+
const absolutePath = path.resolve(root, normalized);
|
|
893
|
+
if (!isWithinRoot(root, absolutePath)) throw new Error(`Path "${relativePath}" escapes the content source root.`);
|
|
894
|
+
return absolutePath;
|
|
895
|
+
};
|
|
896
|
+
const walkFiles = async (directory, prefix) => {
|
|
897
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
898
|
+
const files = [];
|
|
899
|
+
for (const entry of entries) {
|
|
900
|
+
if (entry.name.startsWith(".")) continue;
|
|
901
|
+
const absolutePath = path.join(directory, entry.name);
|
|
902
|
+
const relativePath = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
903
|
+
if (entry.isDirectory()) {
|
|
904
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
905
|
+
files.push(...await walkFiles(absolutePath, relativePath));
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (entry.isFile()) {
|
|
909
|
+
if (!prefix && shouldIgnoreRootDocsFile(entry.name)) continue;
|
|
910
|
+
files.push(normalizePath(relativePath));
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return files;
|
|
914
|
+
};
|
|
915
|
+
var FsContentSource = class {
|
|
916
|
+
root;
|
|
917
|
+
constructor(root) {
|
|
918
|
+
this.root = path.resolve(root);
|
|
919
|
+
}
|
|
920
|
+
async readFile(relativePath) {
|
|
921
|
+
return await fs.readFile(resolveWithinRoot(this.root, relativePath), "utf8");
|
|
922
|
+
}
|
|
923
|
+
async listFiles(directory) {
|
|
924
|
+
return await walkFiles(resolveWithinRoot(this.root, directory), "");
|
|
925
|
+
}
|
|
926
|
+
async exists(relativePath) {
|
|
927
|
+
try {
|
|
928
|
+
await fs.access(resolveWithinRoot(this.root, relativePath));
|
|
929
|
+
return true;
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (isNotFoundError(error)) return false;
|
|
932
|
+
throw error;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
resolveUrl() {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
const createFsSource = (root) => new FsContentSource(root);
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region ../../packages/previewing/dist/markdown/links.js
|
|
942
|
+
const PLACEHOLDER_URL_PATTERN = [
|
|
943
|
+
"https?://(?:[a-z0-9-]+\\.)*example\\.(?:com|org|net)\\b[^\\s)\\]\"'<>]*",
|
|
944
|
+
"https?://discord\\.gg/example\\b[^\\s)\\]\"'<>]*",
|
|
945
|
+
"https?://(?:[a-z0-9-]+\\.)+(?:test|invalid)(?:[/?#:][^\\s)\\]\"'<>]*)?",
|
|
946
|
+
"https?://localhost(?::\\d+)?[^\\s)\\]\"'<>]*",
|
|
947
|
+
"https?://(?:[a-z0-9-]+\\.)*your[-_]?domain\\.[a-z]+\\b[^\\s)\\]\"'<>]*",
|
|
948
|
+
"https?://acme\\.blode\\.md\\b[^\\s)\\]\"'<>]*",
|
|
949
|
+
"https?://github\\.com/example/[^\\s)\\]\"'<>]*",
|
|
950
|
+
"https?://(?:us|eu)\\.i\\.posthog\\.com\\b[^\\s)\\]\"'<>]*"
|
|
951
|
+
].join("|");
|
|
952
|
+
new RegExp(PLACEHOLDER_URL_PATTERN, "gi");
|
|
953
|
+
new RegExp(`\\[([^\\]]+)\\]\\((?:${PLACEHOLDER_URL_PATTERN})\\)`, "gi");
|
|
953
954
|
//#endregion
|
|
954
955
|
//#region src/site-config.ts
|
|
955
956
|
const CONFIG_FILE$2 = "docs.json";
|
|
@@ -1007,497 +1008,231 @@ const resolveDocsRoot = async (dir, cwd = process.cwd()) => {
|
|
|
1007
1008
|
};
|
|
1008
1009
|
const validateDocsRoot = loadValidatedSiteConfig;
|
|
1009
1010
|
//#endregion
|
|
1010
|
-
//#region src/
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
}),
|
|
1041
|
-
headers: { "Content-Type": "application/json" },
|
|
1042
|
-
method: "POST"
|
|
1043
|
-
});
|
|
1044
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1045
|
-
} catch (error) {
|
|
1046
|
-
log.error(`Failed to invalidate preview cache: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
1047
|
-
}
|
|
1011
|
+
//#region src/project-config.ts
|
|
1012
|
+
const LEGACY_PROJECT_NAME_FALLBACK_WARNING = "docs.json.slug is recommended. Falling back to docs.json.name as the deployment slug is deprecated.";
|
|
1013
|
+
const validateProjectSlug = (value) => {
|
|
1014
|
+
const trimmed = value?.trim();
|
|
1015
|
+
if (!trimmed) return "Project slug is required.";
|
|
1016
|
+
const normalized = slugify(trimmed);
|
|
1017
|
+
if (!normalized) return "Use at least one letter or number.";
|
|
1018
|
+
if (normalized !== trimmed) return `Use lowercase letters, numbers, and hyphens. Try "${normalized}".`;
|
|
1019
|
+
};
|
|
1020
|
+
const deriveDisplayNameFromProjectSlug = (projectSlug) => projectSlug.split("-").filter(Boolean).map((segment) => segment[0]?.toUpperCase() + segment.slice(1)).join(" ");
|
|
1021
|
+
const resolveProjectTarget = (options) => {
|
|
1022
|
+
if (options.cliProject) return {
|
|
1023
|
+
project: options.cliProject,
|
|
1024
|
+
usedLegacyNameFallback: false
|
|
1025
|
+
};
|
|
1026
|
+
if (options.envProject) return {
|
|
1027
|
+
project: options.envProject,
|
|
1028
|
+
usedLegacyNameFallback: false
|
|
1029
|
+
};
|
|
1030
|
+
if (options.config.slug) return {
|
|
1031
|
+
project: options.config.slug,
|
|
1032
|
+
usedLegacyNameFallback: false
|
|
1033
|
+
};
|
|
1034
|
+
if (options.config.name) return {
|
|
1035
|
+
project: options.config.name,
|
|
1036
|
+
usedLegacyNameFallback: true
|
|
1037
|
+
};
|
|
1038
|
+
return {
|
|
1039
|
+
project: void 0,
|
|
1040
|
+
usedLegacyNameFallback: false
|
|
1048
1041
|
};
|
|
1049
|
-
watcher.on("all", (event, changedPath) => {
|
|
1050
|
-
if (isDirectoryEvent(event)) return;
|
|
1051
|
-
const relativePath = normalizeRelativePath$1(root, changedPath);
|
|
1052
|
-
pendingPaths.add(relativePath);
|
|
1053
|
-
if (path.basename(changedPath) === "docs.json") pendingKind = "config";
|
|
1054
|
-
if (flushTimer) clearTimeout(flushTimer);
|
|
1055
|
-
flushTimer = setTimeout(() => {
|
|
1056
|
-
flush();
|
|
1057
|
-
}, WATCH_DEBOUNCE_MS);
|
|
1058
|
-
});
|
|
1059
|
-
return { async close() {
|
|
1060
|
-
if (flushTimer) {
|
|
1061
|
-
clearTimeout(flushTimer);
|
|
1062
|
-
await flush();
|
|
1063
|
-
}
|
|
1064
|
-
await watcher.close();
|
|
1065
|
-
} };
|
|
1066
1042
|
};
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const DEV_READY_TIMEOUT_MS = 45e3;
|
|
1071
|
-
const DEV_PORT_SCAN_LIMIT = 10;
|
|
1072
|
-
const DEV_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
1073
|
-
const LOCALHOST = "127.0.0.1";
|
|
1074
|
-
const RUNTIME_EXCLUDE_DIRS = new Set([
|
|
1075
|
-
".next",
|
|
1076
|
-
".turbo",
|
|
1077
|
-
"node_modules"
|
|
1078
|
-
]);
|
|
1079
|
-
const STANDALONE_RUNTIME_MAX_AGE_MS = 1440 * 60 * 1e3;
|
|
1080
|
-
const STANDALONE_RUNTIME_PREFIX = "standalone-runtime-";
|
|
1081
|
-
const TURBOPACK_ARGS = ["dev", "--turbopack"];
|
|
1082
|
-
const fileExists = async (filePath) => {
|
|
1083
|
-
try {
|
|
1084
|
-
await fs.access(filePath);
|
|
1085
|
-
return true;
|
|
1086
|
-
} catch {
|
|
1087
|
-
return false;
|
|
1088
|
-
}
|
|
1043
|
+
const getProjectSlugError = (project) => {
|
|
1044
|
+
if (!project) return;
|
|
1045
|
+
return validateProjectSlug(project);
|
|
1089
1046
|
};
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
if (!restPaths.every((candidatePath) => {
|
|
1099
|
-
const candidate = path.resolve(candidatePath);
|
|
1100
|
-
if (path.parse(candidate).root !== root) return false;
|
|
1101
|
-
return candidate.slice(root.length).split(path.sep).filter(Boolean)[index] === segment;
|
|
1102
|
-
})) break;
|
|
1103
|
-
sharedSegments.push(segment);
|
|
1104
|
-
}
|
|
1105
|
-
return path.join(root, ...sharedSegments);
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region src/analytics/validators.ts
|
|
1049
|
+
const GA4_MEASUREMENT_ID_REGEX = /^G-[A-Z0-9]{4,20}$/;
|
|
1050
|
+
const POSTHOG_PROJECT_KEY_REGEX = /^phc_[A-Za-z0-9]{20,}$/;
|
|
1051
|
+
const parseGa4MeasurementId = (value) => {
|
|
1052
|
+
const trimmed = value.trim();
|
|
1053
|
+
if (!GA4_MEASUREMENT_ID_REGEX.test(trimmed)) throw new InvalidArgumentError("GA4 measurement IDs look like G-XXXXXXXXXX.");
|
|
1054
|
+
return trimmed;
|
|
1106
1055
|
};
|
|
1107
|
-
const
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
if ((await fs.stat(entryPath)).mtimeMs >= cutoff) return;
|
|
1113
|
-
await fs.rm(entryPath, {
|
|
1114
|
-
force: true,
|
|
1115
|
-
recursive: true
|
|
1116
|
-
});
|
|
1117
|
-
}));
|
|
1056
|
+
const parsePosthogProjectKey = (value) => {
|
|
1057
|
+
const trimmed = value.trim();
|
|
1058
|
+
if (trimmed.startsWith("phx_")) throw new InvalidArgumentError("Personal API keys (phx_) are not supported. Use the project API key (phc_).");
|
|
1059
|
+
if (!POSTHOG_PROJECT_KEY_REGEX.test(trimmed)) throw new InvalidArgumentError("PostHog project keys start with phc_ followed by 20+ characters.");
|
|
1060
|
+
return trimmed;
|
|
1118
1061
|
};
|
|
1119
|
-
const
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
const [error] = await once(server, "error");
|
|
1127
|
-
return {
|
|
1128
|
-
error,
|
|
1129
|
-
kind: "error"
|
|
1130
|
-
};
|
|
1131
|
-
})();
|
|
1132
|
-
server.listen({
|
|
1133
|
-
exclusive: true,
|
|
1134
|
-
host: LOCALHOST,
|
|
1135
|
-
port
|
|
1136
|
-
});
|
|
1137
|
-
const outcome = await Promise.race([listening, errored]);
|
|
1138
|
-
if (outcome.kind === "error") {
|
|
1139
|
-
if (outcome.error.code === "EADDRINUSE" || outcome.error.code === "EACCES") return false;
|
|
1140
|
-
throw outcome.error;
|
|
1062
|
+
const parsePosthogHost = (value) => {
|
|
1063
|
+
const trimmed = value.trim();
|
|
1064
|
+
try {
|
|
1065
|
+
if (new URL(trimmed).protocol !== "https:") throw new InvalidArgumentError("PostHog host must use https://");
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
if (error instanceof InvalidArgumentError) throw error;
|
|
1068
|
+
throw new InvalidArgumentError("PostHog host must be a valid URL.");
|
|
1141
1069
|
}
|
|
1142
|
-
|
|
1143
|
-
await once(server, "close");
|
|
1144
|
-
return true;
|
|
1070
|
+
return trimmed;
|
|
1145
1071
|
};
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
if (await probePort(candidate)) return candidate;
|
|
1151
|
-
}
|
|
1152
|
-
throw new CliError(`No available port found within ${DEV_PORT_SCAN_LIMIT} attempts starting at ${requestedPort}.`, EXIT_CODES.ERROR, "Close the process using the port or pass a different --port value.");
|
|
1072
|
+
const parseProvider = (value) => {
|
|
1073
|
+
const normalized = value.trim().toLowerCase();
|
|
1074
|
+
if (normalized === "ga4" || normalized === "posthog") return normalized;
|
|
1075
|
+
throw new InvalidArgumentError(`Unknown provider "${value}". Expected "ga4" or "posthog".`);
|
|
1153
1076
|
};
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/analytics/command.ts
|
|
1079
|
+
const apiBase = (options) => options.apiUrl ?? process.env["BLODEMD_API_URL"] ?? "https://api.blode.md";
|
|
1080
|
+
const resolveAuthorization = async () => {
|
|
1081
|
+
const resolved = await resolveAuthToken();
|
|
1082
|
+
if (!resolved?.token) throw new CliError("Not logged in. Run \"blodemd login\" to authenticate.", EXIT_CODES.AUTH_REQUIRED);
|
|
1083
|
+
return `Bearer ${resolved.token}`;
|
|
1084
|
+
};
|
|
1085
|
+
const tryLoadDocsSlug = async () => {
|
|
1160
1086
|
try {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
throw error;
|
|
1087
|
+
const { config } = await loadValidatedSiteConfig(await resolveDocsRoot());
|
|
1088
|
+
return config.slug ?? config.name;
|
|
1089
|
+
} catch {
|
|
1090
|
+
return;
|
|
1166
1091
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1092
|
+
};
|
|
1093
|
+
const resolveSlug = async (options) => {
|
|
1094
|
+
if (options.project) return options.project;
|
|
1095
|
+
const envSlug = process.env[BLODE_PROJECT_ENV];
|
|
1096
|
+
if (envSlug) return envSlug;
|
|
1097
|
+
const { project } = resolveProjectTarget({
|
|
1098
|
+
cliProject: void 0,
|
|
1099
|
+
config: { slug: await tryLoadDocsSlug() },
|
|
1100
|
+
envProject: void 0
|
|
1169
1101
|
});
|
|
1102
|
+
if (!project) throw new CliError("Could not resolve project. Pass --project <slug>, set BLODEMD_PROJECT, or run from a directory with docs.json.", EXIT_CODES.VALIDATION);
|
|
1103
|
+
return project;
|
|
1170
1104
|
};
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1105
|
+
const getProjectBySlug = async (apiUrl, authorization, slug) => {
|
|
1106
|
+
const response = await fetch(`${apiUrl}/projects/by-slug/${slug}`, { headers: { Authorization: authorization } });
|
|
1107
|
+
if (response.status === 404) throw new CliError(`Project "${slug}" not found or not accessible.`, EXIT_CODES.ERROR);
|
|
1108
|
+
if (!response.ok) throw new CliError(`Failed to fetch project: ${response.status} ${await response.text()}`, EXIT_CODES.ERROR);
|
|
1109
|
+
return await response.json();
|
|
1110
|
+
};
|
|
1111
|
+
const patchAnalytics = async (apiUrl, authorization, projectId, analytics) => {
|
|
1112
|
+
const response = await fetch(`${apiUrl}/projects/${projectId}`, {
|
|
1113
|
+
body: JSON.stringify({ analytics }),
|
|
1114
|
+
headers: {
|
|
1115
|
+
Authorization: authorization,
|
|
1116
|
+
"Content-Type": "application/json"
|
|
1183
1117
|
},
|
|
1184
|
-
|
|
1118
|
+
method: "PATCH"
|
|
1185
1119
|
});
|
|
1120
|
+
if (!response.ok) throw new CliError(`Failed to update analytics: ${response.status} ${await response.text()}`, EXIT_CODES.ERROR);
|
|
1121
|
+
return await response.json();
|
|
1186
1122
|
};
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1123
|
+
const normalizeAnalytics = (analytics) => {
|
|
1124
|
+
if (!analytics) return null;
|
|
1125
|
+
const next = {};
|
|
1126
|
+
if (analytics.ga4?.measurementId) next.ga4 = { measurementId: analytics.ga4.measurementId };
|
|
1127
|
+
if (analytics.posthog?.projectKey) next.posthog = {
|
|
1128
|
+
projectKey: analytics.posthog.projectKey,
|
|
1129
|
+
...analytics.posthog.host ? { host: analytics.posthog.host } : {}
|
|
1130
|
+
};
|
|
1131
|
+
return next.ga4 || next.posthog ? next : null;
|
|
1132
|
+
};
|
|
1133
|
+
const printAnalytics = (project, format) => {
|
|
1134
|
+
const analytics = normalizeAnalytics(project.analytics);
|
|
1135
|
+
if (format === "json") {
|
|
1136
|
+
process.stdout.write(`${JSON.stringify({
|
|
1137
|
+
analytics,
|
|
1138
|
+
project: project.slug
|
|
1139
|
+
}, null, 2)}\n`);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
log.info(`Project: ${chalk.cyan(project.slug)}`);
|
|
1143
|
+
if (!analytics) {
|
|
1144
|
+
log.info(" No analytics configured.");
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (analytics.ga4) log.info(` GA4: ${chalk.cyan(analytics.ga4.measurementId)}`);
|
|
1148
|
+
if (analytics.posthog) {
|
|
1149
|
+
log.info(` PostHog: ${chalk.cyan(analytics.posthog.projectKey)}`);
|
|
1150
|
+
if (analytics.posthog.host) log.info(` host: ${analytics.posthog.host}`);
|
|
1192
1151
|
}
|
|
1193
1152
|
};
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
await
|
|
1197
|
-
|
|
1153
|
+
const runGet = async (options) => {
|
|
1154
|
+
const authorization = await resolveAuthorization();
|
|
1155
|
+
printAnalytics(await getProjectBySlug(apiBase(options), authorization, await resolveSlug(options)), options.json ? "json" : "text");
|
|
1156
|
+
};
|
|
1157
|
+
const runSetGa4 = async (measurementId, options) => {
|
|
1158
|
+
const authorization = await resolveAuthorization();
|
|
1159
|
+
const apiUrl = apiBase(options);
|
|
1160
|
+
const project = await getProjectBySlug(apiUrl, authorization, await resolveSlug(options));
|
|
1161
|
+
const next = {
|
|
1162
|
+
...project.analytics,
|
|
1163
|
+
ga4: { measurementId }
|
|
1164
|
+
};
|
|
1165
|
+
const updated = await patchAnalytics(apiUrl, authorization, project.id, next);
|
|
1166
|
+
log.success(`Updated GA4 for ${chalk.cyan(updated.slug)}.`);
|
|
1167
|
+
printAnalytics(updated, "text");
|
|
1168
|
+
};
|
|
1169
|
+
const runSetPosthog = async (projectKey, options) => {
|
|
1170
|
+
const authorization = await resolveAuthorization();
|
|
1171
|
+
const apiUrl = apiBase(options);
|
|
1172
|
+
const project = await getProjectBySlug(apiUrl, authorization, await resolveSlug(options));
|
|
1173
|
+
const next = {
|
|
1174
|
+
...project.analytics,
|
|
1175
|
+
posthog: {
|
|
1176
|
+
projectKey,
|
|
1177
|
+
...options.host ? { host: options.host } : {}
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
const updated = await patchAnalytics(apiUrl, authorization, project.id, next);
|
|
1181
|
+
log.success(`Updated PostHog for ${chalk.cyan(updated.slug)}.`);
|
|
1182
|
+
printAnalytics(updated, "text");
|
|
1183
|
+
};
|
|
1184
|
+
const runUnset = async (provider, options) => {
|
|
1185
|
+
const authorization = await resolveAuthorization();
|
|
1186
|
+
const apiUrl = apiBase(options);
|
|
1187
|
+
const project = await getProjectBySlug(apiUrl, authorization, await resolveSlug(options));
|
|
1188
|
+
const next = normalizeAnalytics({
|
|
1189
|
+
...project.analytics,
|
|
1190
|
+
[provider]: void 0
|
|
1191
|
+
});
|
|
1192
|
+
const updated = await patchAnalytics(apiUrl, authorization, project.id, next);
|
|
1193
|
+
log.success(`Removed ${provider.toUpperCase()} for ${chalk.cyan(updated.slug)}.`);
|
|
1194
|
+
printAnalytics(updated, "text");
|
|
1198
1195
|
};
|
|
1199
|
-
const
|
|
1200
|
-
const runtimeRoot = await createStandaloneRuntimeRoot();
|
|
1196
|
+
const runAction = async (label, action) => {
|
|
1201
1197
|
try {
|
|
1202
|
-
|
|
1203
|
-
"dev-server",
|
|
1204
|
-
"docs",
|
|
1205
|
-
"packages"
|
|
1206
|
-
]) await copyStandaloneTree(path.join(cliPackageRoot, dir), path.join(runtimeRoot, dir));
|
|
1207
|
-
await fs.symlink(path.join(cliPackageRoot, "node_modules"), path.join(runtimeRoot, "node_modules"), process.platform === "win32" ? "junction" : "dir");
|
|
1208
|
-
await fs.mkdir(path.join(runtimeRoot, "dev-server", "node_modules"), { recursive: true });
|
|
1209
|
-
await fs.symlink(path.join(runtimeRoot, "packages", "@repo"), path.join(runtimeRoot, "dev-server", "node_modules", "@repo"), process.platform === "win32" ? "junction" : "dir");
|
|
1210
|
-
await fs.writeFile(path.join(runtimeRoot, "dev-server", "package.json"), `${JSON.stringify({
|
|
1211
|
-
dependencies: {
|
|
1212
|
-
next: "16.2.1",
|
|
1213
|
-
react: "^19.2.0",
|
|
1214
|
-
"react-dom": "^19.2.0"
|
|
1215
|
-
},
|
|
1216
|
-
devDependencies: {
|
|
1217
|
-
"@types/node": "^22.19.15",
|
|
1218
|
-
"@types/react": "19.2.14",
|
|
1219
|
-
"@types/react-dom": "19.2.3",
|
|
1220
|
-
typescript: "6.0.2"
|
|
1221
|
-
},
|
|
1222
|
-
name: "blodemd-dev-server",
|
|
1223
|
-
private: true,
|
|
1224
|
-
type: "module"
|
|
1225
|
-
}, null, 2)}\n`);
|
|
1226
|
-
return {
|
|
1227
|
-
devServerDir: path.join(runtimeRoot, "dev-server"),
|
|
1228
|
-
runtimeRoot
|
|
1229
|
-
};
|
|
1198
|
+
await action();
|
|
1230
1199
|
} catch (error) {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
throw error;
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1238
|
-
/**
|
|
1239
|
-
* Check if a shipped dev-server exists alongside an installed CLI package.
|
|
1240
|
-
* We only use standalone mode when the package root lives under `node_modules`.
|
|
1241
|
-
*/
|
|
1242
|
-
const findStandaloneDevServer = async (cliPackageRoot) => {
|
|
1243
|
-
const devServerDir = path.join(cliPackageRoot, "dev-server");
|
|
1244
|
-
if (!await fileExists(path.join(devServerDir, "next.config.js"))) return null;
|
|
1245
|
-
if (!await isStandaloneCliInstall(cliPackageRoot)) return null;
|
|
1246
|
-
try {
|
|
1247
|
-
createRequire(path.join(cliPackageRoot, "package.json")).resolve("next/package.json");
|
|
1248
|
-
} catch {
|
|
1249
|
-
return null;
|
|
1200
|
+
const cliError = toCliError(error);
|
|
1201
|
+
log.error(`${label}: ${cliError.message}`);
|
|
1202
|
+
if (cliError.hint) log.info(cliError.hint);
|
|
1203
|
+
process.exitCode = cliError.exitCode;
|
|
1250
1204
|
}
|
|
1251
|
-
const runtime = await materializeStandaloneRuntime(cliPackageRoot);
|
|
1252
|
-
return {
|
|
1253
|
-
devServerDir: runtime.devServerDir,
|
|
1254
|
-
mode: "standalone",
|
|
1255
|
-
nextPackageRoot: cliPackageRoot,
|
|
1256
|
-
runtimeRoot: runtime.runtimeRoot
|
|
1257
|
-
};
|
|
1258
1205
|
};
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1206
|
+
const registerAnalyticsCommand = (program) => {
|
|
1207
|
+
const analytics = program.command("analytics").description("Manage tenant analytics integrations (GA4, PostHog)");
|
|
1208
|
+
analytics.command("get").description("Show the analytics config for a project").option("--project <slug>", "project slug (env: BLODEMD_PROJECT)").option("--api-url <url>", "API URL (env: BLODEMD_API_URL)").option("--json", "print as JSON").action(async (options) => {
|
|
1209
|
+
await runAction("Analytics get failed", () => runGet(options));
|
|
1210
|
+
});
|
|
1211
|
+
const set = analytics.command("set").description("Set an analytics integration");
|
|
1212
|
+
set.command("ga4").description("Set the Google Analytics 4 measurement ID").argument("<measurementId>", "GA4 measurement ID (G-XXXXXXXXXX)", parseGa4MeasurementId).option("--project <slug>", "project slug (env: BLODEMD_PROJECT)").option("--api-url <url>", "API URL (env: BLODEMD_API_URL)").action(async (measurementId, options) => {
|
|
1213
|
+
await runAction("Set GA4 failed", () => runSetGa4(measurementId, options));
|
|
1214
|
+
});
|
|
1215
|
+
set.command("posthog").description("Set the PostHog project key").argument("<projectKey>", "PostHog project key (phc_...)", parsePosthogProjectKey).option("--host <url>", "PostHog host (default: https://us.i.posthog.com)", parsePosthogHost).option("--project <slug>", "project slug (env: BLODEMD_PROJECT)").option("--api-url <url>", "API URL (env: BLODEMD_API_URL)").action(async (projectKey, options) => {
|
|
1216
|
+
await runAction("Set PostHog failed", () => runSetPosthog(projectKey, options));
|
|
1217
|
+
});
|
|
1218
|
+
analytics.command("unset").description("Remove an analytics integration").argument("<provider>", "provider to remove (ga4 | posthog)", parseProvider).option("--project <slug>", "project slug (env: BLODEMD_PROJECT)").option("--api-url <url>", "API URL (env: BLODEMD_API_URL)").action(async (provider, options) => {
|
|
1219
|
+
await runAction("Unset failed", () => runUnset(provider, options));
|
|
1220
|
+
});
|
|
1265
1221
|
};
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
};
|
|
1281
|
-
const resolveDevServer = async (cliFilePath) => {
|
|
1282
|
-
const standalone = await findStandaloneDevServer(resolveCliPackageRoot(cliFilePath));
|
|
1283
|
-
if (standalone) return standalone;
|
|
1284
|
-
return {
|
|
1285
|
-
mode: "monorepo",
|
|
1286
|
-
repoRoot: await findMonorepoRoot(path.dirname(cliFilePath))
|
|
1287
|
-
};
|
|
1288
|
-
};
|
|
1289
|
-
const buildDevServerLaunch = (server, { root, port }) => {
|
|
1290
|
-
if (server.mode === "standalone") return {
|
|
1291
|
-
args: [resolveNextBin(server.nextPackageRoot), ...TURBOPACK_ARGS],
|
|
1292
|
-
command: process.execPath,
|
|
1293
|
-
cwd: server.devServerDir,
|
|
1294
|
-
env: {
|
|
1295
|
-
...process.env,
|
|
1296
|
-
BLODEMD_PACKAGES_DIR: path.join(server.runtimeRoot, "packages"),
|
|
1297
|
-
BLODEMD_TURBOPACK_ROOT: resolveCommonAncestor([server.nextPackageRoot, server.runtimeRoot]),
|
|
1298
|
-
DOCS_ROOT: root,
|
|
1299
|
-
PORT: String(port)
|
|
1300
|
-
}
|
|
1301
|
-
};
|
|
1302
|
-
return {
|
|
1303
|
-
args: [
|
|
1304
|
-
"run",
|
|
1305
|
-
"dev",
|
|
1306
|
-
"--workspace=dev-server"
|
|
1307
|
-
],
|
|
1308
|
-
command: process.platform === "win32" ? "npm.cmd" : "npm",
|
|
1309
|
-
cwd: server.repoRoot,
|
|
1310
|
-
env: {
|
|
1311
|
-
...process.env,
|
|
1312
|
-
DOCS_ROOT: root,
|
|
1313
|
-
PORT: String(port)
|
|
1314
|
-
}
|
|
1315
|
-
};
|
|
1316
|
-
};
|
|
1317
|
-
const spawnDevServer = (server, options) => {
|
|
1318
|
-
const launch = buildDevServerLaunch(server, options);
|
|
1319
|
-
return spawn(launch.command, launch.args, {
|
|
1320
|
-
cwd: launch.cwd,
|
|
1321
|
-
env: launch.env,
|
|
1322
|
-
stdio: "inherit"
|
|
1323
|
-
});
|
|
1324
|
-
};
|
|
1325
|
-
const waitForServer = async ({ child, port }) => {
|
|
1326
|
-
const url = `http://localhost:${port}${DEV_READY_ENDPOINT}`;
|
|
1327
|
-
const startedAt = Date.now();
|
|
1328
|
-
while (Date.now() - startedAt < DEV_READY_TIMEOUT_MS) {
|
|
1329
|
-
if (child.exitCode !== null) throw new CliError("The local dev server exited before it became ready.", EXIT_CODES.ERROR);
|
|
1330
|
-
try {
|
|
1331
|
-
if ((await fetch(url, {
|
|
1332
|
-
cache: "no-store",
|
|
1333
|
-
headers: { accept: "application/json" }
|
|
1334
|
-
})).ok) return;
|
|
1335
|
-
} catch {}
|
|
1336
|
-
await setTimeout$1(500);
|
|
1337
|
-
}
|
|
1338
|
-
throw new CliError("Timed out waiting for the local dev server to start.", EXIT_CODES.ERROR);
|
|
1339
|
-
};
|
|
1340
|
-
const defaultDevCommandDependencies = {
|
|
1341
|
-
createWatcher: createDevWatcher,
|
|
1342
|
-
getCliFilePath: () => fileURLToPath(import.meta.url),
|
|
1343
|
-
getIntro: intro,
|
|
1344
|
-
getLog: log,
|
|
1345
|
-
getOpen: open,
|
|
1346
|
-
parsePortValue: parsePort,
|
|
1347
|
-
removeDirectory: fs.rm,
|
|
1348
|
-
resolveDevPortValue: resolveDevPort,
|
|
1349
|
-
resolveDocsRootValue: resolveDocsRoot,
|
|
1350
|
-
resolveServer: resolveDevServer,
|
|
1351
|
-
shutdownChild: shutdownChildProcess,
|
|
1352
|
-
spawnServer: spawnDevServer,
|
|
1353
|
-
validateDocsRootValue: validateDocsRoot,
|
|
1354
|
-
waitForServerReady: waitForServer
|
|
1355
|
-
};
|
|
1356
|
-
const devCommand = async ({ dir, openBrowser, port: portValue }, dependencies = defaultDevCommandDependencies) => {
|
|
1357
|
-
const cliLog = dependencies.getLog;
|
|
1358
|
-
dependencies.getIntro(chalk.bold("blodemd dev"));
|
|
1359
|
-
try {
|
|
1360
|
-
const port = dependencies.parsePortValue(portValue);
|
|
1361
|
-
const resolvedPort = await dependencies.resolveDevPortValue(port);
|
|
1362
|
-
const root = await dependencies.resolveDocsRootValue(dir);
|
|
1363
|
-
await dependencies.validateDocsRootValue(root);
|
|
1364
|
-
const cliFilePath = dependencies.getCliFilePath();
|
|
1365
|
-
const server = await dependencies.resolveServer(cliFilePath);
|
|
1366
|
-
const localUrl = `http://localhost:${resolvedPort}`;
|
|
1367
|
-
cliLog.info(`Docs root: ${chalk.cyan(root)}`);
|
|
1368
|
-
const child = dependencies.spawnServer(server, {
|
|
1369
|
-
port: resolvedPort,
|
|
1370
|
-
root
|
|
1371
|
-
});
|
|
1372
|
-
let watcher = null;
|
|
1373
|
-
let shuttingDown = false;
|
|
1374
|
-
const closeAll = async () => {
|
|
1375
|
-
if (shuttingDown) return;
|
|
1376
|
-
shuttingDown = true;
|
|
1377
|
-
if (watcher) {
|
|
1378
|
-
await watcher.close();
|
|
1379
|
-
watcher = null;
|
|
1380
|
-
}
|
|
1381
|
-
await dependencies.shutdownChild(child);
|
|
1382
|
-
if (server.mode === "standalone") await dependencies.removeDirectory(server.runtimeRoot, {
|
|
1383
|
-
force: true,
|
|
1384
|
-
recursive: true
|
|
1385
|
-
});
|
|
1386
|
-
};
|
|
1387
|
-
process.once("SIGINT", closeAll);
|
|
1388
|
-
process.once("SIGTERM", closeAll);
|
|
1389
|
-
try {
|
|
1390
|
-
await dependencies.waitForServerReady({
|
|
1391
|
-
child,
|
|
1392
|
-
port: resolvedPort
|
|
1393
|
-
});
|
|
1394
|
-
watcher = await dependencies.createWatcher({
|
|
1395
|
-
port: resolvedPort,
|
|
1396
|
-
root
|
|
1397
|
-
});
|
|
1398
|
-
cliLog.success(`Dev server running at ${chalk.cyan(localUrl)}`);
|
|
1399
|
-
if (openBrowser) await dependencies.getOpen(localUrl);
|
|
1400
|
-
const [code, signal] = await once(child, "exit");
|
|
1401
|
-
if (shuttingDown || signal === "SIGINT" || signal === "SIGTERM") return;
|
|
1402
|
-
if (code !== 0) throw new CliError(`The local dev server exited with code ${code ?? "unknown"}.`, EXIT_CODES.ERROR);
|
|
1403
|
-
} finally {
|
|
1404
|
-
await closeAll();
|
|
1405
|
-
process.removeListener("SIGINT", closeAll);
|
|
1406
|
-
process.removeListener("SIGTERM", closeAll);
|
|
1407
|
-
}
|
|
1408
|
-
} catch (error) {
|
|
1409
|
-
const cliError = toCliError(error);
|
|
1410
|
-
cliLog.error(cliError.message);
|
|
1411
|
-
if (cliError.hint) cliLog.info(cliError.hint);
|
|
1412
|
-
process.exitCode = cliError.exitCode;
|
|
1413
|
-
}
|
|
1414
|
-
};
|
|
1415
|
-
//#endregion
|
|
1416
|
-
//#region src/fs-utils.ts
|
|
1417
|
-
const writeFileIfMissing = async (filePath, content) => {
|
|
1418
|
-
try {
|
|
1419
|
-
await fs.writeFile(filePath, content, { flag: "wx" });
|
|
1420
|
-
} catch (error) {
|
|
1421
|
-
const { code } = error;
|
|
1422
|
-
if (code === "EEXIST") {
|
|
1423
|
-
if ((await fs.stat(filePath).catch(() => null))?.isFile()) return;
|
|
1424
|
-
}
|
|
1425
|
-
throw error;
|
|
1426
|
-
}
|
|
1427
|
-
};
|
|
1428
|
-
const writeSymlinkIfMissing = async (filePath, target, options) => {
|
|
1429
|
-
const lstat = options?.lstat ?? fs.lstat;
|
|
1430
|
-
const symlink = options?.symlink ?? fs.symlink;
|
|
1431
|
-
try {
|
|
1432
|
-
await symlink(target, filePath);
|
|
1433
|
-
} catch (error) {
|
|
1434
|
-
const { code } = error;
|
|
1435
|
-
if (code === "EEXIST") {
|
|
1436
|
-
const existing = await lstat(filePath).catch(() => null);
|
|
1437
|
-
if (existing?.isFile() || existing?.isSymbolicLink()) return;
|
|
1438
|
-
}
|
|
1439
|
-
if (options?.fallbackContent && (code === "EINVAL" || code === "ENOTSUP" || code === "EPERM" || code === "UNKNOWN")) {
|
|
1440
|
-
await writeFileIfMissing(filePath, options.fallbackContent);
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
throw error;
|
|
1444
|
-
}
|
|
1445
|
-
};
|
|
1446
|
-
const findExistingPaths = async (root, relativePaths) => {
|
|
1447
|
-
return (await Promise.all(relativePaths.map(async (relativePath) => {
|
|
1448
|
-
return await fs.lstat(path.join(root, relativePath)).catch(() => null) ? relativePath : null;
|
|
1449
|
-
}))).filter((relativePath) => relativePath !== null).toSorted((left, right) => left.localeCompare(right));
|
|
1450
|
-
};
|
|
1451
|
-
//#endregion
|
|
1452
|
-
//#region src/project-config.ts
|
|
1453
|
-
const LEGACY_PROJECT_NAME_FALLBACK_WARNING = "docs.json.slug is recommended. Falling back to docs.json.name as the deployment slug is deprecated.";
|
|
1454
|
-
const validateProjectSlug = (value) => {
|
|
1455
|
-
const trimmed = value?.trim();
|
|
1456
|
-
if (!trimmed) return "Project slug is required.";
|
|
1457
|
-
const normalized = slugify(trimmed);
|
|
1458
|
-
if (!normalized) return "Use at least one letter or number.";
|
|
1459
|
-
if (normalized !== trimmed) return `Use lowercase letters, numbers, and hyphens. Try "${normalized}".`;
|
|
1460
|
-
};
|
|
1461
|
-
const deriveDisplayNameFromProjectSlug = (projectSlug) => projectSlug.split("-").filter(Boolean).map((segment) => segment[0]?.toUpperCase() + segment.slice(1)).join(" ");
|
|
1462
|
-
const resolveProjectTarget = (options) => {
|
|
1463
|
-
if (options.cliProject) return {
|
|
1464
|
-
project: options.cliProject,
|
|
1465
|
-
usedLegacyNameFallback: false
|
|
1466
|
-
};
|
|
1467
|
-
if (options.envProject) return {
|
|
1468
|
-
project: options.envProject,
|
|
1469
|
-
usedLegacyNameFallback: false
|
|
1470
|
-
};
|
|
1471
|
-
if (options.config.slug) return {
|
|
1472
|
-
project: options.config.slug,
|
|
1473
|
-
usedLegacyNameFallback: false
|
|
1474
|
-
};
|
|
1475
|
-
if (options.config.name) return {
|
|
1476
|
-
project: options.config.name,
|
|
1477
|
-
usedLegacyNameFallback: true
|
|
1478
|
-
};
|
|
1479
|
-
return {
|
|
1480
|
-
project: void 0,
|
|
1481
|
-
usedLegacyNameFallback: false
|
|
1482
|
-
};
|
|
1483
|
-
};
|
|
1484
|
-
const getProjectSlugError = (project) => {
|
|
1485
|
-
if (!project) return;
|
|
1486
|
-
return validateProjectSlug(project);
|
|
1487
|
-
};
|
|
1488
|
-
//#endregion
|
|
1489
|
-
//#region src/scaffold.ts
|
|
1490
|
-
const SCAFFOLD_TEMPLATES = ["minimal", "starter"];
|
|
1491
|
-
const DEFAULT_SCAFFOLD_DIRECTORY = "docs";
|
|
1492
|
-
const stringifyJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
1493
|
-
const escapeXmlText = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
1494
|
-
const isScaffoldTemplate = (value) => SCAFFOLD_TEMPLATES.includes(value);
|
|
1495
|
-
const normalizeProjectSlug = (value) => slugify(value) || "my-project";
|
|
1496
|
-
const resolveScaffoldDirectory = (directory) => directory?.trim() || "docs";
|
|
1497
|
-
const deriveDefaultProjectSlug = (directory, cwd) => {
|
|
1498
|
-
const resolvedDirectory = resolveScaffoldDirectory(directory);
|
|
1499
|
-
if (resolvedDirectory === "." || resolvedDirectory === "docs") return normalizeProjectSlug(path.basename(cwd));
|
|
1500
|
-
return normalizeProjectSlug(path.basename(path.resolve(cwd, resolvedDirectory)));
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/scaffold.ts
|
|
1224
|
+
const SCAFFOLD_TEMPLATES = ["minimal", "starter"];
|
|
1225
|
+
const DEFAULT_SCAFFOLD_DIRECTORY = "docs";
|
|
1226
|
+
const DEFAULT_PROJECT_SLUG = "my-project";
|
|
1227
|
+
const stringifyJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
1228
|
+
const escapeXmlText = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
1229
|
+
const isScaffoldTemplate = (value) => SCAFFOLD_TEMPLATES.includes(value);
|
|
1230
|
+
const normalizeProjectSlug = (value) => slugify(value) || DEFAULT_PROJECT_SLUG;
|
|
1231
|
+
const resolveScaffoldDirectory = (directory) => directory?.trim() || "docs";
|
|
1232
|
+
const deriveDefaultProjectSlug = (directory, cwd) => {
|
|
1233
|
+
const resolvedDirectory = resolveScaffoldDirectory(directory);
|
|
1234
|
+
if (resolvedDirectory === "." || resolvedDirectory === "docs") return normalizeProjectSlug(path.basename(cwd));
|
|
1235
|
+
return normalizeProjectSlug(path.basename(path.resolve(cwd, resolvedDirectory)));
|
|
1501
1236
|
};
|
|
1502
1237
|
const createMinimalDocsJson = (projectSlug, displayName) => ({
|
|
1503
1238
|
$schema: "https://blode.md/docs.json",
|
|
@@ -1845,42 +1580,87 @@ const createStarterFiles = (projectSlug, displayName) => {
|
|
|
1845
1580
|
];
|
|
1846
1581
|
};
|
|
1847
1582
|
const getScaffoldFiles = (template, options) => {
|
|
1848
|
-
const projectSlug = options?.projectSlug ??
|
|
1583
|
+
const projectSlug = options?.projectSlug ?? DEFAULT_PROJECT_SLUG;
|
|
1849
1584
|
const displayName = options?.displayName ?? deriveDisplayNameFromProjectSlug(projectSlug);
|
|
1850
1585
|
return template === "starter" ? createStarterFiles(projectSlug, displayName) : createMinimalFiles(projectSlug, displayName);
|
|
1851
1586
|
};
|
|
1852
1587
|
//#endregion
|
|
1853
|
-
//#region src/
|
|
1854
|
-
const
|
|
1855
|
-
const
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
};
|
|
1866
|
-
if (options.currentDirectoryEntries.length === 0) return {
|
|
1867
|
-
directory: ".",
|
|
1868
|
-
kind: "target"
|
|
1869
|
-
};
|
|
1870
|
-
return { kind: "prompt" };
|
|
1871
|
-
};
|
|
1872
|
-
const resolveDirectoryFromAction = (action, subdirectory) => {
|
|
1873
|
-
if (action === "scaffold-current-directory") return ".";
|
|
1874
|
-
if (action === "create-in-subdirectory") return subdirectory?.trim() || "docs";
|
|
1588
|
+
//#region src/command-utils.ts
|
|
1589
|
+
const readGitValue = (gitArgs) => {
|
|
1590
|
+
const result = spawnSync("git", gitArgs, {
|
|
1591
|
+
encoding: "utf8",
|
|
1592
|
+
stdio: [
|
|
1593
|
+
"ignore",
|
|
1594
|
+
"pipe",
|
|
1595
|
+
"ignore"
|
|
1596
|
+
]
|
|
1597
|
+
});
|
|
1598
|
+
if (result.status !== 0) return;
|
|
1599
|
+
return result.stdout.trim() || void 0;
|
|
1875
1600
|
};
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
const
|
|
1879
|
-
const
|
|
1880
|
-
const
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1601
|
+
const shouldSkipEntry = (name) => name.startsWith(".") || name === "node_modules";
|
|
1602
|
+
const collectFiles = async (root, directory = root) => {
|
|
1603
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
1604
|
+
const files = [];
|
|
1605
|
+
for (const entry of entries) {
|
|
1606
|
+
if (shouldSkipEntry(entry.name)) continue;
|
|
1607
|
+
const absolutePath = path.join(directory, entry.name);
|
|
1608
|
+
const relativePath = path.relative(root, absolutePath).split(path.sep).join("/");
|
|
1609
|
+
if (entry.isDirectory()) {
|
|
1610
|
+
files.push(...await collectFiles(root, absolutePath));
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
if (entry.isFile()) {
|
|
1614
|
+
if (shouldIgnoreRootDocsFile(relativePath)) continue;
|
|
1615
|
+
files.push(absolutePath);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return files.toSorted((left, right) => left.localeCompare(right));
|
|
1619
|
+
};
|
|
1620
|
+
const reportCommandError = (prefix, error) => {
|
|
1621
|
+
const cliError = toCliError(error);
|
|
1622
|
+
log.error(`${prefix}: ${cliError.message}`);
|
|
1623
|
+
if (cliError.hint) log.info(cliError.hint);
|
|
1624
|
+
log.info("Failed");
|
|
1625
|
+
process.exitCode = cliError.exitCode;
|
|
1626
|
+
};
|
|
1627
|
+
const parseScaffoldTemplate = (value) => {
|
|
1628
|
+
if (isScaffoldTemplate(value)) return value;
|
|
1629
|
+
throw new InvalidArgumentError(`Expected one of: ${SCAFFOLD_TEMPLATES.join(", ")}.`);
|
|
1630
|
+
};
|
|
1631
|
+
const parseProjectSlug = (value) => {
|
|
1632
|
+
const validationError = validateProjectSlug(value);
|
|
1633
|
+
if (validationError) throw new InvalidArgumentError(validationError);
|
|
1634
|
+
return value.trim();
|
|
1635
|
+
};
|
|
1636
|
+
//#endregion
|
|
1637
|
+
//#region src/http.ts
|
|
1638
|
+
const readJson = async (response) => {
|
|
1639
|
+
const responseText = await response.text();
|
|
1640
|
+
if (!responseText) return null;
|
|
1641
|
+
try {
|
|
1642
|
+
return JSON.parse(responseText);
|
|
1643
|
+
} catch {
|
|
1644
|
+
return responseText;
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
const requestJson = async (url, init, message) => {
|
|
1648
|
+
const response = await fetch(url, init);
|
|
1649
|
+
const data = await readJson(response);
|
|
1650
|
+
if (!response.ok) {
|
|
1651
|
+
const detail = typeof data === "string" ? data : JSON.stringify(data ?? {}, null, 2);
|
|
1652
|
+
throw new Error(`${message}: ${response.status} ${detail}`);
|
|
1653
|
+
}
|
|
1654
|
+
return data;
|
|
1655
|
+
};
|
|
1656
|
+
//#endregion
|
|
1657
|
+
//#region src/oauth-callback.ts
|
|
1658
|
+
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>";
|
|
1659
|
+
const escapeHtml = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
1660
|
+
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>`;
|
|
1661
|
+
const waitForOAuthCode = (options) => {
|
|
1662
|
+
const host = options.redirectUrl.hostname;
|
|
1663
|
+
const port = Number(options.redirectUrl.port);
|
|
1884
1664
|
const { pathname } = options.redirectUrl;
|
|
1885
1665
|
if (!Number.isInteger(port) || port <= 0) return Promise.reject(new CliError("OAuth redirect URL requires an explicit port", EXIT_CODES.ERROR));
|
|
1886
1666
|
return new Promise((resolve, reject) => {
|
|
@@ -1896,7 +1676,7 @@ const waitForOAuthCode = (options) => {
|
|
|
1896
1676
|
});
|
|
1897
1677
|
for (const socket of sockets) socket.destroy();
|
|
1898
1678
|
};
|
|
1899
|
-
const httpServer = createServer
|
|
1679
|
+
const httpServer = createServer((request, response) => {
|
|
1900
1680
|
if (!request.url) {
|
|
1901
1681
|
response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
1902
1682
|
response.end(errorHtml("Missing request URL"));
|
|
@@ -1953,160 +1733,610 @@ const createOAuthState = () => randomBytes(24).toString("hex");
|
|
|
1953
1733
|
const createCodeVerifier = () => randomBytes(64).toString("base64url");
|
|
1954
1734
|
const createCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest().toString("base64url");
|
|
1955
1735
|
//#endregion
|
|
1956
|
-
//#region src/
|
|
1957
|
-
const
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(input.trim());
|
|
1965
|
-
if (!match) return null;
|
|
1966
|
-
const [, majorText = "", minorText = "", patchText = ""] = match;
|
|
1967
|
-
if (!majorText || !minorText || !patchText) return null;
|
|
1968
|
-
const major = Number.parseInt(majorText, 10);
|
|
1969
|
-
const minor = Number.parseInt(minorText, 10);
|
|
1970
|
-
const patch = Number.parseInt(patchText, 10);
|
|
1971
|
-
if ([
|
|
1972
|
-
major,
|
|
1973
|
-
minor,
|
|
1974
|
-
patch
|
|
1975
|
-
].some((value) => Number.isNaN(value))) return null;
|
|
1976
|
-
return [
|
|
1977
|
-
major,
|
|
1978
|
-
minor,
|
|
1979
|
-
patch
|
|
1980
|
-
];
|
|
1981
|
-
};
|
|
1982
|
-
const isSupportedNodeVersion = (version) => {
|
|
1983
|
-
const parsed = parseVersion(version);
|
|
1984
|
-
if (!parsed) return false;
|
|
1985
|
-
const [major, minor, patch] = parsed;
|
|
1986
|
-
const [minMajor, minMinor, minPatch] = MIN_SUPPORTED_NODE_VERSION;
|
|
1987
|
-
if (major !== minMajor) return major > minMajor;
|
|
1988
|
-
if (minor !== minMinor) return minor > minMinor;
|
|
1989
|
-
return patch >= minPatch;
|
|
1990
|
-
};
|
|
1991
|
-
const assertSupportedNodeVersion = (version = process.versions.node) => {
|
|
1992
|
-
if (isSupportedNodeVersion(version)) return;
|
|
1993
|
-
throw new CliError(`blodemd requires Node.js ${SUPPORTED_NODE_RANGE}. Current version: ${version}.`, EXIT_CODES.VALIDATION, "Install a supported Node.js version and try again.");
|
|
1736
|
+
//#region src/validation.ts
|
|
1737
|
+
const MAX_PORT = 65535;
|
|
1738
|
+
const parsePositiveInteger = (value, label) => {
|
|
1739
|
+
const trimmed = value.trim();
|
|
1740
|
+
if (!/^\d+$/u.test(trimmed)) throw new CliError(`${label} must be a positive integer.`, EXIT_CODES.VALIDATION);
|
|
1741
|
+
const parsed = Number(trimmed);
|
|
1742
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new CliError(`${label} must be a positive integer.`, EXIT_CODES.VALIDATION);
|
|
1743
|
+
return parsed;
|
|
1994
1744
|
};
|
|
1995
|
-
const
|
|
1996
|
-
const
|
|
1997
|
-
|
|
1998
|
-
return
|
|
1745
|
+
const parsePort = (value, label = "Port") => {
|
|
1746
|
+
const parsed = parsePositiveInteger(value, label);
|
|
1747
|
+
if (parsed > MAX_PORT) throw new CliError(`${label} must be between 1 and ${MAX_PORT}.`, EXIT_CODES.VALIDATION);
|
|
1748
|
+
return parsed;
|
|
1999
1749
|
};
|
|
2000
1750
|
//#endregion
|
|
2001
|
-
//#region src/
|
|
2002
|
-
const
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
".mdx": "text/markdown; charset=utf-8",
|
|
2009
|
-
".svg": "image/svg+xml",
|
|
2010
|
-
".txt": "text/plain; charset=utf-8",
|
|
2011
|
-
".yaml": "application/yaml; charset=utf-8",
|
|
2012
|
-
".yml": "application/yaml; charset=utf-8"
|
|
2013
|
-
};
|
|
2014
|
-
const normalizeRelativePath = (root, filePath) => path.relative(root, filePath).split(path.sep).join("/");
|
|
2015
|
-
const getContentType = (filePath) => TEXT_CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
2016
|
-
const estimateUploadItemBytes = (item) => item.contentBase64.length + item.path.length + 64;
|
|
2017
|
-
const createUploadBatchItem = async (filePath, root, readFile) => {
|
|
2018
|
-
return {
|
|
2019
|
-
contentBase64: (await readFile(filePath)).toString("base64"),
|
|
2020
|
-
contentType: getContentType(filePath),
|
|
2021
|
-
path: normalizeRelativePath(root, filePath)
|
|
2022
|
-
};
|
|
1751
|
+
//#region src/commands/auth.ts
|
|
1752
|
+
const fetchUserEmail = async (apiUrl, token) => {
|
|
1753
|
+
try {
|
|
1754
|
+
return (await requestJson(`${apiUrl}/auth/me`, { headers: { Authorization: `Bearer ${token}` } }, "Failed to fetch user info")).email;
|
|
1755
|
+
} catch {
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
2023
1758
|
};
|
|
2024
|
-
const
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
1759
|
+
const registerAuthCommands = (program) => {
|
|
1760
|
+
program.command("login").description("Authenticate with Blode.md via GitHub in your browser").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) => {
|
|
1761
|
+
intro(chalk.bold("blodemd login"));
|
|
1762
|
+
try {
|
|
1763
|
+
const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
|
|
1764
|
+
const clientId = OAUTH_CLIENT_ID;
|
|
1765
|
+
const port = parsePort(options.port);
|
|
1766
|
+
const timeoutSeconds = parsePositiveInteger(options.timeout, "Timeout");
|
|
1767
|
+
const redirectUrl = new URL(`http://127.0.0.1:${port}${DEFAULT_OAUTH_CALLBACK_PATH}`);
|
|
1768
|
+
const state = createOAuthState();
|
|
1769
|
+
const codeVerifier = createCodeVerifier();
|
|
1770
|
+
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
1771
|
+
const authUrl = new URL(authorizeUrl);
|
|
1772
|
+
authUrl.searchParams.set("response_type", "code");
|
|
1773
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
1774
|
+
authUrl.searchParams.set("redirect_uri", redirectUrl.toString());
|
|
1775
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
1776
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
1777
|
+
authUrl.searchParams.set("state", state);
|
|
1778
|
+
authUrl.searchParams.set("scope", "openid email profile");
|
|
1779
|
+
authUrl.searchParams.set("provider", "github");
|
|
1780
|
+
const callbackPromise = waitForOAuthCode({
|
|
1781
|
+
expectedState: state,
|
|
1782
|
+
redirectUrl,
|
|
1783
|
+
timeoutMs: timeoutSeconds * 1e3
|
|
1784
|
+
});
|
|
1785
|
+
if (options.open) {
|
|
1786
|
+
log.info("Opening browser for authentication...");
|
|
1787
|
+
log.info(`If the browser doesn't open, visit: ${chalk.cyan(authUrl.toString())}`);
|
|
1788
|
+
await open(authUrl.toString());
|
|
1789
|
+
} else {
|
|
1790
|
+
log.info("Open this URL to continue authentication:");
|
|
1791
|
+
log.info(chalk.cyan(authUrl.toString()));
|
|
1792
|
+
}
|
|
1793
|
+
const code = await callbackPromise;
|
|
1794
|
+
const storedSession = tokenResponseToStoredSession(await exchangeAuthorizationCode({
|
|
1795
|
+
clientId,
|
|
1796
|
+
tokenUrl
|
|
1797
|
+
}, code, codeVerifier, redirectUrl.toString()));
|
|
1798
|
+
await writeStoredAuthSession(storedSession);
|
|
1799
|
+
const email = storedSession.user?.email ?? await fetchUserEmail(process.env["BLODEMD_API_URL"] ?? "https://api.blode.md", storedSession.accessToken);
|
|
1800
|
+
if (email) log.success(`Logged in as ${chalk.cyan(email)}`);
|
|
1801
|
+
else log.success("Logged in successfully.");
|
|
1802
|
+
log.info("Done");
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
reportCommandError("Login failed", error);
|
|
2036
1805
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
1806
|
+
});
|
|
1807
|
+
program.command("logout").description("Remove stored credentials").action(async () => {
|
|
1808
|
+
intro(chalk.bold("blodemd logout"));
|
|
1809
|
+
try {
|
|
1810
|
+
let existing = false;
|
|
1811
|
+
try {
|
|
1812
|
+
await fs.access(CREDENTIALS_FILE);
|
|
1813
|
+
existing = true;
|
|
1814
|
+
} catch {
|
|
1815
|
+
existing = false;
|
|
1816
|
+
}
|
|
1817
|
+
await clearStoredCredentials();
|
|
1818
|
+
if (existing) log.success("Credentials removed.");
|
|
1819
|
+
else log.info("No stored credentials found.");
|
|
1820
|
+
log.info("Done");
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
reportCommandError("Logout failed", error);
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
program.command("whoami").description("Show current authentication").action(async () => {
|
|
1826
|
+
try {
|
|
1827
|
+
const resolved = await resolveAuthToken();
|
|
1828
|
+
if (!resolved) {
|
|
1829
|
+
log.warn("Not logged in. Run \"blodemd login\" to authenticate.");
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
const status = resolveTokenStatus(resolved);
|
|
1833
|
+
const email = resolved.user?.email ?? await fetchUserEmail(process.env["BLODEMD_API_URL"] ?? "https://api.blode.md", resolved.token);
|
|
1834
|
+
if (email) log.info(`Logged in as ${chalk.cyan(email)}`);
|
|
1835
|
+
else log.info("Logged in (could not fetch user details).");
|
|
1836
|
+
if (resolved.expiresAt && status.expired) log.warn("Session has expired. Run \"blodemd login\" to re-authenticate.");
|
|
1837
|
+
} catch (error) {
|
|
1838
|
+
reportCommandError("Whoami failed", error);
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
2041
1841
|
};
|
|
2042
1842
|
//#endregion
|
|
2043
|
-
//#region src/
|
|
2044
|
-
const
|
|
2045
|
-
const
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
1843
|
+
//#region src/dev/watcher.ts
|
|
1844
|
+
const INVALIDATE_ENDPOINT = "/blodemd-dev/invalidate";
|
|
1845
|
+
const WATCH_DEBOUNCE_MS = 100;
|
|
1846
|
+
const normalizeRelativePath$1 = (root, filePath) => path.relative(root, filePath).split(path.sep).join("/");
|
|
1847
|
+
const isDirectoryEvent = (event) => event === "addDir" || event === "unlinkDir";
|
|
1848
|
+
const createDevWatcher = ({ port, root }) => {
|
|
1849
|
+
const watcher = watch(root, {
|
|
1850
|
+
ignoreInitial: true,
|
|
1851
|
+
ignored: [
|
|
1852
|
+
"**/.git/**",
|
|
1853
|
+
"**/.next/**",
|
|
1854
|
+
"**/dist/**",
|
|
1855
|
+
"**/node_modules/**"
|
|
2052
1856
|
]
|
|
2053
1857
|
});
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
1858
|
+
let flushTimer = null;
|
|
1859
|
+
let pendingKind = "content";
|
|
1860
|
+
const pendingPaths = /* @__PURE__ */ new Set();
|
|
1861
|
+
const flush = async () => {
|
|
1862
|
+
flushTimer = null;
|
|
1863
|
+
const paths = [...pendingPaths];
|
|
1864
|
+
const kind = pendingKind;
|
|
1865
|
+
pendingPaths.clear();
|
|
1866
|
+
pendingKind = "content";
|
|
1867
|
+
if (!paths.length) return;
|
|
1868
|
+
try {
|
|
1869
|
+
const response = await fetch(`http://127.0.0.1:${port}${INVALIDATE_ENDPOINT}`, {
|
|
1870
|
+
body: JSON.stringify({
|
|
1871
|
+
kind,
|
|
1872
|
+
paths
|
|
1873
|
+
}),
|
|
1874
|
+
headers: { "Content-Type": "application/json" },
|
|
1875
|
+
method: "POST"
|
|
1876
|
+
});
|
|
1877
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
log.error(`Failed to invalidate preview cache: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
2068
1880
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
1881
|
+
};
|
|
1882
|
+
watcher.on("all", (event, changedPath) => {
|
|
1883
|
+
if (isDirectoryEvent(event)) return;
|
|
1884
|
+
const relativePath = normalizeRelativePath$1(root, changedPath);
|
|
1885
|
+
pendingPaths.add(relativePath);
|
|
1886
|
+
if (path.basename(changedPath) === "docs.json") pendingKind = "config";
|
|
1887
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
1888
|
+
flushTimer = setTimeout(() => {
|
|
1889
|
+
flush();
|
|
1890
|
+
}, WATCH_DEBOUNCE_MS);
|
|
1891
|
+
});
|
|
1892
|
+
return { async close() {
|
|
1893
|
+
if (flushTimer) {
|
|
1894
|
+
clearTimeout(flushTimer);
|
|
1895
|
+
await flush();
|
|
2072
1896
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
};
|
|
2076
|
-
const readJson = async (response) => {
|
|
2077
|
-
const responseText = await response.text();
|
|
2078
|
-
if (!responseText) return null;
|
|
2079
|
-
try {
|
|
2080
|
-
return JSON.parse(responseText);
|
|
2081
|
-
} catch {
|
|
2082
|
-
return responseText;
|
|
2083
|
-
}
|
|
1897
|
+
await watcher.close();
|
|
1898
|
+
} };
|
|
2084
1899
|
};
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
1900
|
+
//#endregion
|
|
1901
|
+
//#region src/dev/command.ts
|
|
1902
|
+
const DEV_READY_ENDPOINT = "/blodemd-dev/version";
|
|
1903
|
+
const DEV_READY_TIMEOUT_MS = 45e3;
|
|
1904
|
+
const DEV_PORT_SCAN_LIMIT = 10;
|
|
1905
|
+
const DEV_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
1906
|
+
const LOCALHOST = "127.0.0.1";
|
|
1907
|
+
const RUNTIME_EXCLUDE_DIRS = new Set([
|
|
1908
|
+
".next",
|
|
1909
|
+
".turbo",
|
|
1910
|
+
"node_modules"
|
|
1911
|
+
]);
|
|
1912
|
+
const STANDALONE_RUNTIME_MAX_AGE_MS = 1440 * 60 * 1e3;
|
|
1913
|
+
const STANDALONE_RUNTIME_PREFIX = "standalone-runtime-";
|
|
1914
|
+
const TURBOPACK_ARGS = ["dev", "--turbopack"];
|
|
1915
|
+
const fileExists = async (filePath) => {
|
|
1916
|
+
try {
|
|
1917
|
+
await fs.access(filePath);
|
|
1918
|
+
return true;
|
|
1919
|
+
} catch {
|
|
1920
|
+
return false;
|
|
2091
1921
|
}
|
|
2092
|
-
return data;
|
|
2093
1922
|
};
|
|
2094
|
-
const
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
1923
|
+
const resolveCommonAncestor = (pathsToCompare) => {
|
|
1924
|
+
const [firstPath, ...restPaths] = pathsToCompare;
|
|
1925
|
+
if (!firstPath) return path.sep;
|
|
1926
|
+
const first = path.resolve(firstPath);
|
|
1927
|
+
const { root } = path.parse(first);
|
|
1928
|
+
const firstSegments = first.slice(root.length).split(path.sep).filter(Boolean);
|
|
1929
|
+
const sharedSegments = [];
|
|
1930
|
+
for (const [index, segment] of firstSegments.entries()) {
|
|
1931
|
+
if (!restPaths.every((candidatePath) => {
|
|
1932
|
+
const candidate = path.resolve(candidatePath);
|
|
1933
|
+
if (path.parse(candidate).root !== root) return false;
|
|
1934
|
+
return candidate.slice(root.length).split(path.sep).filter(Boolean)[index] === segment;
|
|
1935
|
+
})) break;
|
|
1936
|
+
sharedSegments.push(segment);
|
|
1937
|
+
}
|
|
1938
|
+
return path.join(root, ...sharedSegments);
|
|
2100
1939
|
};
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2103
|
-
|
|
1940
|
+
const cleanupStandaloneRuntimeRoots = async (configDir, maxAgeMs = STANDALONE_RUNTIME_MAX_AGE_MS) => {
|
|
1941
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
1942
|
+
const entries = await fs.readdir(configDir, { withFileTypes: true });
|
|
1943
|
+
await Promise.all(entries.filter((entry) => entry.isDirectory() && entry.name.startsWith(STANDALONE_RUNTIME_PREFIX)).map(async (entry) => {
|
|
1944
|
+
const entryPath = path.join(configDir, entry.name);
|
|
1945
|
+
if ((await fs.stat(entryPath)).mtimeMs >= cutoff) return;
|
|
1946
|
+
await fs.rm(entryPath, {
|
|
1947
|
+
force: true,
|
|
1948
|
+
recursive: true
|
|
1949
|
+
});
|
|
1950
|
+
}));
|
|
1951
|
+
};
|
|
1952
|
+
const probePortAvailability = async (port) => {
|
|
1953
|
+
const server = createServer$1();
|
|
1954
|
+
const listening = (async () => {
|
|
1955
|
+
await once(server, "listening");
|
|
1956
|
+
return { kind: "listening" };
|
|
1957
|
+
})();
|
|
1958
|
+
const errored = (async () => {
|
|
1959
|
+
const [error] = await once(server, "error");
|
|
1960
|
+
return {
|
|
1961
|
+
error,
|
|
1962
|
+
kind: "error"
|
|
1963
|
+
};
|
|
1964
|
+
})();
|
|
1965
|
+
server.listen({
|
|
1966
|
+
exclusive: true,
|
|
1967
|
+
host: LOCALHOST,
|
|
1968
|
+
port
|
|
1969
|
+
});
|
|
1970
|
+
const outcome = await Promise.race([listening, errored]);
|
|
1971
|
+
if (outcome.kind === "error") {
|
|
1972
|
+
if (outcome.error.code === "EADDRINUSE" || outcome.error.code === "EACCES") return false;
|
|
1973
|
+
throw outcome.error;
|
|
1974
|
+
}
|
|
1975
|
+
server.close();
|
|
1976
|
+
await once(server, "close");
|
|
1977
|
+
return true;
|
|
1978
|
+
};
|
|
1979
|
+
const resolveDevPort = async (requestedPort, probePort = probePortAvailability) => {
|
|
1980
|
+
for (let offset = 0; offset < DEV_PORT_SCAN_LIMIT; offset += 1) {
|
|
1981
|
+
const candidate = requestedPort + offset;
|
|
1982
|
+
if (candidate > 65535) break;
|
|
1983
|
+
if (await probePort(candidate)) return candidate;
|
|
1984
|
+
}
|
|
1985
|
+
throw new CliError(`No available port found within ${DEV_PORT_SCAN_LIMIT} attempts starting at ${requestedPort}.`, EXIT_CODES.ERROR, "Close the process using the port or pass a different --port value.");
|
|
1986
|
+
};
|
|
1987
|
+
const shutdownChildProcess = async (child, timeoutMs = DEV_SHUTDOWN_TIMEOUT_MS) => {
|
|
1988
|
+
if (child.exitCode !== null) return;
|
|
1989
|
+
const timer = setTimeout(() => {
|
|
1990
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
1991
|
+
}, timeoutMs);
|
|
1992
|
+
const exitPromise = once(child, "exit");
|
|
1993
|
+
try {
|
|
1994
|
+
child.kill("SIGTERM");
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
clearTimeout(timer);
|
|
1997
|
+
if (error.code === "ESRCH") return;
|
|
1998
|
+
throw error;
|
|
1999
|
+
}
|
|
2000
|
+
await exitPromise.finally(() => {
|
|
2001
|
+
clearTimeout(timer);
|
|
2002
|
+
});
|
|
2003
|
+
};
|
|
2004
|
+
/**
|
|
2005
|
+
* Derive the CLI npm package root from the running script path.
|
|
2006
|
+
* The CLI entry point is at `<pkg-root>/dist/cli.mjs`.
|
|
2007
|
+
*/
|
|
2008
|
+
const resolveCliPackageRoot = (cliFilePath) => path.dirname(path.dirname(cliFilePath));
|
|
2009
|
+
const copyStandaloneTree = async (sourceDir, targetDir) => {
|
|
2010
|
+
await fs.cp(sourceDir, targetDir, {
|
|
2011
|
+
filter: (source) => {
|
|
2012
|
+
const relative = path.relative(sourceDir, source);
|
|
2013
|
+
if (!relative) return true;
|
|
2014
|
+
const topSegment = relative.split(path.sep)[0] ?? "";
|
|
2015
|
+
return !RUNTIME_EXCLUDE_DIRS.has(topSegment);
|
|
2016
|
+
},
|
|
2017
|
+
recursive: true
|
|
2018
|
+
});
|
|
2019
|
+
};
|
|
2020
|
+
const isStandaloneCliInstall = async (cliPackageRoot) => {
|
|
2021
|
+
try {
|
|
2022
|
+
return (await fs.realpath(cliPackageRoot)).split(path.sep).includes("node_modules");
|
|
2023
|
+
} catch {
|
|
2024
|
+
return cliPackageRoot.split(path.sep).includes("node_modules");
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
const createStandaloneRuntimeRoot = async (configDir = CONFIG_DIR) => {
|
|
2028
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
2029
|
+
await cleanupStandaloneRuntimeRoots(configDir);
|
|
2030
|
+
return await fs.mkdtemp(path.join(configDir, STANDALONE_RUNTIME_PREFIX));
|
|
2031
|
+
};
|
|
2032
|
+
/**
|
|
2033
|
+
* Locate the `node_modules` directory that actually contains the CLI's
|
|
2034
|
+
* transitive dependencies. Package managers like npm/yarn-classic (and
|
|
2035
|
+
* `npx` caches) hoist shared deps above the package directory, so
|
|
2036
|
+
* `<cliPackageRoot>/node_modules` may not exist or may not contain `next`.
|
|
2037
|
+
* Resolve `next/package.json` and use the directory that owns it.
|
|
2038
|
+
*/
|
|
2039
|
+
const resolveRuntimeNodeModules = async (cliPackageRoot) => {
|
|
2040
|
+
const localNodeModules = path.join(cliPackageRoot, "node_modules");
|
|
2041
|
+
if (await fileExists(path.join(localNodeModules, "next", "package.json"))) return localNodeModules;
|
|
2042
|
+
const nextPkgPath = createRequire(path.join(cliPackageRoot, "package.json")).resolve("next/package.json");
|
|
2043
|
+
return path.dirname(path.dirname(nextPkgPath));
|
|
2044
|
+
};
|
|
2045
|
+
const materializeStandaloneRuntime = async (cliPackageRoot) => {
|
|
2046
|
+
const runtimeRoot = await createStandaloneRuntimeRoot();
|
|
2047
|
+
try {
|
|
2048
|
+
for (const dir of [
|
|
2049
|
+
"dev-server",
|
|
2050
|
+
"docs",
|
|
2051
|
+
"packages"
|
|
2052
|
+
]) await copyStandaloneTree(path.join(cliPackageRoot, dir), path.join(runtimeRoot, dir));
|
|
2053
|
+
const runtimeNodeModules = await resolveRuntimeNodeModules(cliPackageRoot);
|
|
2054
|
+
await fs.symlink(runtimeNodeModules, path.join(runtimeRoot, "node_modules"), process.platform === "win32" ? "junction" : "dir");
|
|
2055
|
+
const linkTarget = path.join(runtimeRoot, "packages", "@repo");
|
|
2056
|
+
for (const consumer of [
|
|
2057
|
+
"dev-server",
|
|
2058
|
+
"docs",
|
|
2059
|
+
"packages"
|
|
2060
|
+
]) {
|
|
2061
|
+
await fs.mkdir(path.join(runtimeRoot, consumer, "node_modules"), { recursive: true });
|
|
2062
|
+
await fs.symlink(linkTarget, path.join(runtimeRoot, consumer, "node_modules", "@repo"), process.platform === "win32" ? "junction" : "dir");
|
|
2063
|
+
}
|
|
2064
|
+
await fs.writeFile(path.join(runtimeRoot, "dev-server", "package.json"), `${JSON.stringify({
|
|
2065
|
+
dependencies: {
|
|
2066
|
+
next: "16.2.1",
|
|
2067
|
+
react: "^19.2.0",
|
|
2068
|
+
"react-dom": "^19.2.0"
|
|
2069
|
+
},
|
|
2070
|
+
devDependencies: {
|
|
2071
|
+
"@types/node": "^24.12.2",
|
|
2072
|
+
"@types/react": "19.2.14",
|
|
2073
|
+
"@types/react-dom": "19.2.3",
|
|
2074
|
+
typescript: "6.0.2"
|
|
2075
|
+
},
|
|
2076
|
+
name: "blodemd-dev-server",
|
|
2077
|
+
private: true,
|
|
2078
|
+
type: "module"
|
|
2079
|
+
}, null, 2)}\n`);
|
|
2080
|
+
return {
|
|
2081
|
+
devServerDir: path.join(runtimeRoot, "dev-server"),
|
|
2082
|
+
runtimeRoot
|
|
2083
|
+
};
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
await fs.rm(runtimeRoot, {
|
|
2086
|
+
force: true,
|
|
2087
|
+
recursive: true
|
|
2088
|
+
});
|
|
2089
|
+
throw error;
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
/**
|
|
2093
|
+
* Check if a shipped dev-server exists alongside an installed CLI package.
|
|
2094
|
+
* We only use standalone mode when the package root lives under `node_modules`.
|
|
2095
|
+
*/
|
|
2096
|
+
const findStandaloneDevServer = async (cliPackageRoot) => {
|
|
2097
|
+
const devServerDir = path.join(cliPackageRoot, "dev-server");
|
|
2098
|
+
if (!await fileExists(path.join(devServerDir, "next.config.js"))) return null;
|
|
2099
|
+
if (!await isStandaloneCliInstall(cliPackageRoot)) return null;
|
|
2100
|
+
try {
|
|
2101
|
+
createRequire(path.join(cliPackageRoot, "package.json")).resolve("next/package.json");
|
|
2102
|
+
} catch {
|
|
2103
|
+
return null;
|
|
2104
|
+
}
|
|
2105
|
+
const runtime = await materializeStandaloneRuntime(cliPackageRoot);
|
|
2106
|
+
return {
|
|
2107
|
+
devServerDir: runtime.devServerDir,
|
|
2108
|
+
mode: "standalone",
|
|
2109
|
+
nextPackageRoot: cliPackageRoot,
|
|
2110
|
+
runtimeRoot: runtime.runtimeRoot
|
|
2111
|
+
};
|
|
2112
|
+
};
|
|
2113
|
+
/**
|
|
2114
|
+
* Resolve the `next` CLI binary from the blodemd package's own dependencies.
|
|
2115
|
+
*/
|
|
2116
|
+
const resolveNextBin = (cliPackageRoot) => {
|
|
2117
|
+
const nextPkgPath = createRequire(path.join(cliPackageRoot, "package.json")).resolve("next/package.json");
|
|
2118
|
+
return path.join(path.dirname(nextPkgPath), "dist", "bin", "next");
|
|
2119
|
+
};
|
|
2120
|
+
const findMonorepoRoot = async (start) => {
|
|
2121
|
+
let current = start;
|
|
2122
|
+
while (true) {
|
|
2123
|
+
const packageJsonPath = path.join(current, "package.json");
|
|
2124
|
+
if (await fileExists(packageJsonPath)) {
|
|
2125
|
+
const raw = await fs.readFile(packageJsonPath, "utf8");
|
|
2126
|
+
const workspaces = JSON.parse(raw).workspaces ?? [];
|
|
2127
|
+
if (workspaces.includes("apps/*") && workspaces.includes("packages/*")) return current;
|
|
2128
|
+
}
|
|
2129
|
+
const parent = path.dirname(current);
|
|
2130
|
+
if (parent === current) break;
|
|
2131
|
+
current = parent;
|
|
2132
|
+
}
|
|
2133
|
+
throw new CliError("Could not locate the blodemd dev server.", EXIT_CODES.ERROR, "Make sure blodemd is installed correctly (npm i blodemd).");
|
|
2134
|
+
};
|
|
2135
|
+
const resolveDevServer = async (cliFilePath) => {
|
|
2136
|
+
const standalone = await findStandaloneDevServer(resolveCliPackageRoot(cliFilePath));
|
|
2137
|
+
if (standalone) return standalone;
|
|
2138
|
+
return {
|
|
2139
|
+
mode: "monorepo",
|
|
2140
|
+
repoRoot: await findMonorepoRoot(path.dirname(cliFilePath))
|
|
2141
|
+
};
|
|
2142
|
+
};
|
|
2143
|
+
const buildDevServerLaunch = (server, { root, port }) => {
|
|
2144
|
+
if (server.mode === "standalone") return {
|
|
2145
|
+
args: [resolveNextBin(server.nextPackageRoot), ...TURBOPACK_ARGS],
|
|
2146
|
+
command: process.execPath,
|
|
2147
|
+
cwd: server.devServerDir,
|
|
2148
|
+
env: {
|
|
2149
|
+
...process.env,
|
|
2150
|
+
BLODEMD_PACKAGES_DIR: path.join(server.runtimeRoot, "packages"),
|
|
2151
|
+
BLODEMD_TURBOPACK_ROOT: resolveCommonAncestor([server.nextPackageRoot, server.runtimeRoot]),
|
|
2152
|
+
DOCS_ROOT: root,
|
|
2153
|
+
PORT: String(port)
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
2156
|
+
return {
|
|
2157
|
+
args: [
|
|
2158
|
+
"run",
|
|
2159
|
+
"dev",
|
|
2160
|
+
"--workspace=dev-server"
|
|
2161
|
+
],
|
|
2162
|
+
command: process.platform === "win32" ? "npm.cmd" : "npm",
|
|
2163
|
+
cwd: server.repoRoot,
|
|
2164
|
+
env: {
|
|
2165
|
+
...process.env,
|
|
2166
|
+
DOCS_ROOT: root,
|
|
2167
|
+
PORT: String(port)
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
};
|
|
2171
|
+
const spawnDevServer = (server, options) => {
|
|
2172
|
+
const launch = buildDevServerLaunch(server, options);
|
|
2173
|
+
return spawn(launch.command, launch.args, {
|
|
2174
|
+
cwd: launch.cwd,
|
|
2175
|
+
env: launch.env,
|
|
2176
|
+
stdio: "inherit"
|
|
2177
|
+
});
|
|
2178
|
+
};
|
|
2179
|
+
const waitForServer = async ({ child, port }) => {
|
|
2180
|
+
const url = `http://localhost:${port}${DEV_READY_ENDPOINT}`;
|
|
2181
|
+
const startedAt = Date.now();
|
|
2182
|
+
while (Date.now() - startedAt < DEV_READY_TIMEOUT_MS) {
|
|
2183
|
+
if (child.exitCode !== null) throw new CliError("The local dev server exited before it became ready.", EXIT_CODES.ERROR);
|
|
2184
|
+
try {
|
|
2185
|
+
if ((await fetch(url, {
|
|
2186
|
+
cache: "no-store",
|
|
2187
|
+
headers: { accept: "application/json" }
|
|
2188
|
+
})).ok) return;
|
|
2189
|
+
} catch {}
|
|
2190
|
+
await setTimeout$1(500);
|
|
2191
|
+
}
|
|
2192
|
+
throw new CliError("Timed out waiting for the local dev server to start.", EXIT_CODES.ERROR);
|
|
2193
|
+
};
|
|
2194
|
+
const defaultDevCommandDependencies = {
|
|
2195
|
+
createWatcher: createDevWatcher,
|
|
2196
|
+
getCliFilePath: () => fileURLToPath(import.meta.url),
|
|
2197
|
+
getIntro: intro,
|
|
2198
|
+
getLog: log,
|
|
2199
|
+
getOpen: open,
|
|
2200
|
+
parsePortValue: parsePort,
|
|
2201
|
+
removeDirectory: fs.rm,
|
|
2202
|
+
resolveDevPortValue: resolveDevPort,
|
|
2203
|
+
resolveDocsRootValue: resolveDocsRoot,
|
|
2204
|
+
resolveServer: resolveDevServer,
|
|
2205
|
+
shutdownChild: shutdownChildProcess,
|
|
2206
|
+
spawnServer: spawnDevServer,
|
|
2207
|
+
validateDocsRootValue: validateDocsRoot,
|
|
2208
|
+
waitForServerReady: waitForServer
|
|
2209
|
+
};
|
|
2210
|
+
const devCommand = async ({ dir, openBrowser, port: portValue }, dependencies = defaultDevCommandDependencies) => {
|
|
2211
|
+
const cliLog = dependencies.getLog;
|
|
2212
|
+
dependencies.getIntro(chalk.bold("blodemd dev"));
|
|
2213
|
+
try {
|
|
2214
|
+
const port = dependencies.parsePortValue(portValue);
|
|
2215
|
+
const resolvedPort = await dependencies.resolveDevPortValue(port);
|
|
2216
|
+
const root = await dependencies.resolveDocsRootValue(dir);
|
|
2217
|
+
await dependencies.validateDocsRootValue(root);
|
|
2218
|
+
const cliFilePath = dependencies.getCliFilePath();
|
|
2219
|
+
const server = await dependencies.resolveServer(cliFilePath);
|
|
2220
|
+
const localUrl = `http://localhost:${resolvedPort}`;
|
|
2221
|
+
cliLog.info(`Docs root: ${chalk.cyan(root)}`);
|
|
2222
|
+
const child = dependencies.spawnServer(server, {
|
|
2223
|
+
port: resolvedPort,
|
|
2224
|
+
root
|
|
2225
|
+
});
|
|
2226
|
+
let watcher = null;
|
|
2227
|
+
let shuttingDown = false;
|
|
2228
|
+
const closeAll = async () => {
|
|
2229
|
+
if (shuttingDown) return;
|
|
2230
|
+
shuttingDown = true;
|
|
2231
|
+
if (watcher) {
|
|
2232
|
+
await watcher.close();
|
|
2233
|
+
watcher = null;
|
|
2234
|
+
}
|
|
2235
|
+
await dependencies.shutdownChild(child);
|
|
2236
|
+
if (server.mode === "standalone") await dependencies.removeDirectory(server.runtimeRoot, {
|
|
2237
|
+
force: true,
|
|
2238
|
+
recursive: true
|
|
2239
|
+
});
|
|
2240
|
+
};
|
|
2241
|
+
process.once("SIGINT", closeAll);
|
|
2242
|
+
process.once("SIGTERM", closeAll);
|
|
2243
|
+
try {
|
|
2244
|
+
await dependencies.waitForServerReady({
|
|
2245
|
+
child,
|
|
2246
|
+
port: resolvedPort
|
|
2247
|
+
});
|
|
2248
|
+
watcher = await dependencies.createWatcher({
|
|
2249
|
+
port: resolvedPort,
|
|
2250
|
+
root
|
|
2251
|
+
});
|
|
2252
|
+
cliLog.success(`Dev server running at ${chalk.cyan(localUrl)}`);
|
|
2253
|
+
if (openBrowser) await dependencies.getOpen(localUrl);
|
|
2254
|
+
const [code, signal] = await once(child, "exit");
|
|
2255
|
+
if (shuttingDown || signal === "SIGINT" || signal === "SIGTERM") return;
|
|
2256
|
+
if (code !== 0) throw new CliError(`The local dev server exited with code ${code ?? "unknown"}.`, EXIT_CODES.ERROR);
|
|
2257
|
+
} finally {
|
|
2258
|
+
await closeAll();
|
|
2259
|
+
process.removeListener("SIGINT", closeAll);
|
|
2260
|
+
process.removeListener("SIGTERM", closeAll);
|
|
2261
|
+
}
|
|
2262
|
+
} catch (error) {
|
|
2263
|
+
const cliError = toCliError(error);
|
|
2264
|
+
cliLog.error(cliError.message);
|
|
2265
|
+
if (cliError.hint) cliLog.info(cliError.hint);
|
|
2266
|
+
process.exitCode = cliError.exitCode;
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
//#endregion
|
|
2270
|
+
//#region src/commands/dev.ts
|
|
2271
|
+
const registerDevCommand = (program) => {
|
|
2272
|
+
program.command("dev").description("Start the local docs dev server").option("-p, --port <port>", "Port number", "3030").option("-d, --dir <dir>", "Docs directory").option("--no-open", "Don't open browser").action(async (options) => await devCommand({
|
|
2273
|
+
dir: options.dir,
|
|
2274
|
+
openBrowser: options.open ?? true,
|
|
2275
|
+
port: options.port
|
|
2276
|
+
}));
|
|
2277
|
+
};
|
|
2278
|
+
//#endregion
|
|
2279
|
+
//#region src/fs-utils.ts
|
|
2280
|
+
const writeFileIfMissing = async (filePath, content) => {
|
|
2281
|
+
try {
|
|
2282
|
+
await fs.writeFile(filePath, content, { flag: "wx" });
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
const { code } = error;
|
|
2285
|
+
if (code === "EEXIST") {
|
|
2286
|
+
if ((await fs.stat(filePath).catch(() => null))?.isFile()) return;
|
|
2287
|
+
}
|
|
2288
|
+
throw error;
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
const writeSymlinkIfMissing = async (filePath, target, options) => {
|
|
2292
|
+
const lstat = options?.lstat ?? fs.lstat;
|
|
2293
|
+
const symlink = options?.symlink ?? fs.symlink;
|
|
2294
|
+
try {
|
|
2295
|
+
await symlink(target, filePath);
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
const { code } = error;
|
|
2298
|
+
if (code === "EEXIST") {
|
|
2299
|
+
const existing = await lstat(filePath).catch(() => null);
|
|
2300
|
+
if (existing?.isFile() || existing?.isSymbolicLink()) return;
|
|
2301
|
+
}
|
|
2302
|
+
if (options?.fallbackContent && (code === "EINVAL" || code === "ENOTSUP" || code === "EPERM" || code === "UNKNOWN")) {
|
|
2303
|
+
await writeFileIfMissing(filePath, options.fallbackContent);
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
throw error;
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
const findExistingPaths = async (root, relativePaths) => {
|
|
2310
|
+
return (await Promise.all(relativePaths.map(async (relativePath) => {
|
|
2311
|
+
return await fs.lstat(path.join(root, relativePath)).catch(() => null) ? relativePath : null;
|
|
2312
|
+
}))).filter((relativePath) => relativePath !== null).toSorted((left, right) => left.localeCompare(right));
|
|
2313
|
+
};
|
|
2314
|
+
//#endregion
|
|
2315
|
+
//#region src/new-flow.ts
|
|
2316
|
+
const CREATE_IN_SUBDIRECTORY = "create-in-subdirectory";
|
|
2317
|
+
const SCAFFOLD_CURRENT_DIRECTORY = "scaffold-current-directory";
|
|
2318
|
+
const CANCEL_SCAFFOLD = "cancel";
|
|
2319
|
+
const resolveInitialDirectory = (options) => {
|
|
2320
|
+
if (options.directory) return {
|
|
2321
|
+
directory: options.directory,
|
|
2322
|
+
kind: "target"
|
|
2323
|
+
};
|
|
2324
|
+
if (!options.interactive) return {
|
|
2325
|
+
directory: DEFAULT_SCAFFOLD_DIRECTORY,
|
|
2326
|
+
kind: "target"
|
|
2327
|
+
};
|
|
2328
|
+
if (options.currentDirectoryEntries.length === 0) return {
|
|
2329
|
+
directory: ".",
|
|
2330
|
+
kind: "target"
|
|
2331
|
+
};
|
|
2332
|
+
return { kind: "prompt" };
|
|
2104
2333
|
};
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
if (
|
|
2108
|
-
return value.trim();
|
|
2334
|
+
const resolveDirectoryFromAction = (action, subdirectory) => {
|
|
2335
|
+
if (action === "scaffold-current-directory") return ".";
|
|
2336
|
+
if (action === "create-in-subdirectory") return subdirectory?.trim() || "docs";
|
|
2109
2337
|
};
|
|
2338
|
+
//#endregion
|
|
2339
|
+
//#region src/commands/new.ts
|
|
2110
2340
|
const isInteractiveTerminal = () => process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2111
2341
|
const promptForNoArgDirectoryAction = async () => {
|
|
2112
2342
|
const directoryAction = await select({
|
|
@@ -2219,13 +2449,113 @@ const writeScaffoldFiles = async (root, template, options) => {
|
|
|
2219
2449
|
await writeFileIfMissing(filePath, file.content);
|
|
2220
2450
|
}
|
|
2221
2451
|
};
|
|
2222
|
-
const
|
|
2452
|
+
const scaffoldDocsSite = async (directory, options) => {
|
|
2453
|
+
intro(chalk.bold("blodemd new"));
|
|
2454
|
+
if (options?.deprecatedCommand) log.warn(`"${options.deprecatedCommand}" is deprecated. Use ${chalk.cyan("blodemd new")} instead.`);
|
|
2455
|
+
if (options?.name && !options.slug) log.warn(`"${chalk.cyan("--name")}" is deprecated. Use ${chalk.cyan("--slug")} instead.`);
|
|
2223
2456
|
try {
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2457
|
+
const template = options?.template ?? "minimal";
|
|
2458
|
+
const shouldPrompt = isInteractiveTerminal() && !options?.yes;
|
|
2459
|
+
const selectedDirectory = await resolveRequestedDirectory(directory, shouldPrompt);
|
|
2460
|
+
if (!selectedDirectory) {
|
|
2461
|
+
log.warn("Cancelled");
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
const resolvedDirectory = resolveScaffoldDirectory(selectedDirectory.directory);
|
|
2465
|
+
const root = path.resolve(process.cwd(), resolvedDirectory);
|
|
2466
|
+
if (!await confirmScaffoldTarget(root, template, shouldPrompt, { skipNonEmptyConfirmation: selectedDirectory.skipNonEmptyConfirmation })) {
|
|
2467
|
+
log.warn("Cancelled");
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
const projectSlug = await resolveProjectSlug(options?.slug ?? options?.name, resolvedDirectory, shouldPrompt);
|
|
2471
|
+
if (!projectSlug) {
|
|
2472
|
+
log.warn("Cancelled");
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
const displayName = await resolveDisplayName(options?.displayName, projectSlug, shouldPrompt);
|
|
2476
|
+
if (!displayName) {
|
|
2477
|
+
log.warn("Cancelled");
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
await fs.mkdir(root, { recursive: true });
|
|
2481
|
+
await writeScaffoldFiles(root, template, {
|
|
2482
|
+
displayName,
|
|
2483
|
+
projectSlug
|
|
2484
|
+
});
|
|
2485
|
+
log.success(`Docs scaffolded in ${chalk.cyan(root)}`);
|
|
2486
|
+
if (template === "starter") log.info("Starter template includes brand assets and helper files.");
|
|
2487
|
+
log.info(`Display name: ${chalk.cyan(displayName)}`);
|
|
2488
|
+
log.info(`Project slug: ${chalk.cyan(projectSlug)}`);
|
|
2489
|
+
log.info("Done");
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
reportCommandError("New failed", error);
|
|
2227
2492
|
}
|
|
2228
2493
|
};
|
|
2494
|
+
const registerNewCommand = (program) => {
|
|
2495
|
+
program.command("new").description("Create a new blode.md documentation site").argument("[directory]", "target directory").option("--slug <slug>", "project slug for docs.json", parseProjectSlug).option("--name <slug>", "deprecated alias for --slug", parseProjectSlug).option("--display-name <name>", "display name for docs.json").option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
|
|
2496
|
+
await scaffoldDocsSite(directory, {
|
|
2497
|
+
displayName: options.displayName,
|
|
2498
|
+
name: options.name,
|
|
2499
|
+
slug: options.slug ?? options.name,
|
|
2500
|
+
template: options.template,
|
|
2501
|
+
yes: options.yes
|
|
2502
|
+
});
|
|
2503
|
+
});
|
|
2504
|
+
program.command("init", { hidden: true }).argument("[directory]", "target directory").option("--slug <slug>", "project slug for docs.json", parseProjectSlug).option("--name <slug>", "deprecated alias for --slug", parseProjectSlug).option("--display-name <name>", "display name for docs.json").option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
|
|
2505
|
+
await scaffoldDocsSite(directory, {
|
|
2506
|
+
deprecatedCommand: "blodemd init",
|
|
2507
|
+
displayName: options.displayName,
|
|
2508
|
+
name: options.name,
|
|
2509
|
+
slug: options.slug ?? options.name,
|
|
2510
|
+
template: options.template,
|
|
2511
|
+
yes: options.yes
|
|
2512
|
+
});
|
|
2513
|
+
});
|
|
2514
|
+
};
|
|
2515
|
+
//#endregion
|
|
2516
|
+
//#region src/upload.ts
|
|
2517
|
+
const TEXT_CONTENT_TYPES = {
|
|
2518
|
+
".css": "text/css; charset=utf-8",
|
|
2519
|
+
".html": "text/html; charset=utf-8",
|
|
2520
|
+
".js": "text/javascript; charset=utf-8",
|
|
2521
|
+
".json": "application/json; charset=utf-8",
|
|
2522
|
+
".md": "text/markdown; charset=utf-8",
|
|
2523
|
+
".mdx": "text/markdown; charset=utf-8",
|
|
2524
|
+
".svg": "image/svg+xml",
|
|
2525
|
+
".txt": "text/plain; charset=utf-8",
|
|
2526
|
+
".yaml": "application/yaml; charset=utf-8",
|
|
2527
|
+
".yml": "application/yaml; charset=utf-8"
|
|
2528
|
+
};
|
|
2529
|
+
const normalizeRelativePath = (root, filePath) => path.relative(root, filePath).split(path.sep).join("/");
|
|
2530
|
+
const getContentType = (filePath) => TEXT_CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
2531
|
+
const estimateUploadItemBytes = (item) => item.contentBase64.length + item.path.length + 64;
|
|
2532
|
+
const createUploadBatchItem = async (filePath, root, readFile) => {
|
|
2533
|
+
return {
|
|
2534
|
+
contentBase64: (await readFile(filePath)).toString("base64"),
|
|
2535
|
+
contentType: getContentType(filePath),
|
|
2536
|
+
path: normalizeRelativePath(root, filePath)
|
|
2537
|
+
};
|
|
2538
|
+
};
|
|
2539
|
+
const createUploadBatches = async function* createUploadBatchesGenerator({ files, maxBatchBytes, readFile = fs.readFile, root }) {
|
|
2540
|
+
let currentBatch = [];
|
|
2541
|
+
let currentBatchBytes = 0;
|
|
2542
|
+
for (const filePath of files) {
|
|
2543
|
+
const item = await createUploadBatchItem(filePath, root, readFile);
|
|
2544
|
+
if (shouldIgnoreRootDocsFile(item.path)) continue;
|
|
2545
|
+
const itemBytes = estimateUploadItemBytes(item);
|
|
2546
|
+
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.");
|
|
2547
|
+
if (currentBatch.length > 0 && currentBatchBytes + itemBytes > maxBatchBytes) {
|
|
2548
|
+
yield currentBatch;
|
|
2549
|
+
currentBatch = [];
|
|
2550
|
+
currentBatchBytes = 0;
|
|
2551
|
+
}
|
|
2552
|
+
currentBatch.push(item);
|
|
2553
|
+
currentBatchBytes += itemBytes;
|
|
2554
|
+
}
|
|
2555
|
+
if (currentBatch.length > 0) yield currentBatch;
|
|
2556
|
+
};
|
|
2557
|
+
//#endregion
|
|
2558
|
+
//#region src/commands/push.ts
|
|
2229
2559
|
const resolvePushConfig = async (config, options) => {
|
|
2230
2560
|
const { project, usedLegacyNameFallback } = resolveProjectTarget({
|
|
2231
2561
|
cliProject: options.project,
|
|
@@ -2276,48 +2606,6 @@ const autoCreateProject = async (project, projectDisplayName, apiUrl, headers) =
|
|
|
2276
2606
|
log.success(`Project ${chalk.cyan(createResult.slug)} created`);
|
|
2277
2607
|
return true;
|
|
2278
2608
|
};
|
|
2279
|
-
const scaffoldDocsSite = async (directory, options) => {
|
|
2280
|
-
intro(chalk.bold("blodemd new"));
|
|
2281
|
-
if (options?.deprecatedCommand) log.warn(`"${options.deprecatedCommand}" is deprecated. Use ${chalk.cyan("blodemd new")} instead.`);
|
|
2282
|
-
if (options?.name && !options.slug) log.warn(`"${chalk.cyan("--name")}" is deprecated. Use ${chalk.cyan("--slug")} instead.`);
|
|
2283
|
-
try {
|
|
2284
|
-
const template = options?.template ?? "minimal";
|
|
2285
|
-
const shouldPrompt = isInteractiveTerminal() && !options?.yes;
|
|
2286
|
-
const selectedDirectory = await resolveRequestedDirectory(directory, shouldPrompt);
|
|
2287
|
-
if (!selectedDirectory) {
|
|
2288
|
-
log.warn("Cancelled");
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
const resolvedDirectory = resolveScaffoldDirectory(selectedDirectory.directory);
|
|
2292
|
-
const root = path.resolve(process.cwd(), resolvedDirectory);
|
|
2293
|
-
if (!await confirmScaffoldTarget(root, template, shouldPrompt, { skipNonEmptyConfirmation: selectedDirectory.skipNonEmptyConfirmation })) {
|
|
2294
|
-
log.warn("Cancelled");
|
|
2295
|
-
return;
|
|
2296
|
-
}
|
|
2297
|
-
const projectSlug = await resolveProjectSlug(options?.slug ?? options?.name, resolvedDirectory, shouldPrompt);
|
|
2298
|
-
if (!projectSlug) {
|
|
2299
|
-
log.warn("Cancelled");
|
|
2300
|
-
return;
|
|
2301
|
-
}
|
|
2302
|
-
const displayName = await resolveDisplayName(options?.displayName, projectSlug, shouldPrompt);
|
|
2303
|
-
if (!displayName) {
|
|
2304
|
-
log.warn("Cancelled");
|
|
2305
|
-
return;
|
|
2306
|
-
}
|
|
2307
|
-
await fs.mkdir(root, { recursive: true });
|
|
2308
|
-
await writeScaffoldFiles(root, template, {
|
|
2309
|
-
displayName,
|
|
2310
|
-
projectSlug
|
|
2311
|
-
});
|
|
2312
|
-
log.success(`Docs scaffolded in ${chalk.cyan(root)}`);
|
|
2313
|
-
if (template === "starter") log.info("Starter template includes brand assets and helper files.");
|
|
2314
|
-
log.info(`Display name: ${chalk.cyan(displayName)}`);
|
|
2315
|
-
log.info(`Project slug: ${chalk.cyan(projectSlug)}`);
|
|
2316
|
-
log.info("Done");
|
|
2317
|
-
} catch (error) {
|
|
2318
|
-
reportCommandError("New failed", error);
|
|
2319
|
-
}
|
|
2320
|
-
};
|
|
2321
2609
|
const MAX_BATCH_BYTES = 4 * 1024 * 1024;
|
|
2322
2610
|
const uploadFiles = async (files, root, apiPath, deploymentId, headers, s) => {
|
|
2323
2611
|
s.start(`Uploading ${files.length} files`);
|
|
@@ -2337,192 +2625,140 @@ const uploadFiles = async (files, root, apiPath, deploymentId, headers, s) => {
|
|
|
2337
2625
|
}
|
|
2338
2626
|
s.stop(`Uploaded ${chalk.cyan(String(files.length))} files`);
|
|
2339
2627
|
};
|
|
2340
|
-
const
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
assertSupportedNodeVersion();
|
|
2345
|
-
});
|
|
2346
|
-
program.command("login").description("Authenticate with Blode.md via GitHub in your browser").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) => {
|
|
2347
|
-
intro(chalk.bold("blodemd login"));
|
|
2348
|
-
try {
|
|
2349
|
-
const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
|
|
2350
|
-
const clientId = OAUTH_CLIENT_ID;
|
|
2351
|
-
const port = parsePort(options.port);
|
|
2352
|
-
const timeoutSeconds = parsePositiveInteger(options.timeout, "Timeout");
|
|
2353
|
-
const redirectUrl = new URL(`http://127.0.0.1:${port}${DEFAULT_OAUTH_CALLBACK_PATH}`);
|
|
2354
|
-
const state = createOAuthState();
|
|
2355
|
-
const codeVerifier = createCodeVerifier();
|
|
2356
|
-
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
2357
|
-
const authUrl = new URL(authorizeUrl);
|
|
2358
|
-
authUrl.searchParams.set("response_type", "code");
|
|
2359
|
-
authUrl.searchParams.set("client_id", clientId);
|
|
2360
|
-
authUrl.searchParams.set("redirect_uri", redirectUrl.toString());
|
|
2361
|
-
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
2362
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
2363
|
-
authUrl.searchParams.set("state", state);
|
|
2364
|
-
authUrl.searchParams.set("scope", "openid email profile");
|
|
2365
|
-
authUrl.searchParams.set("provider", "github");
|
|
2366
|
-
const callbackPromise = waitForOAuthCode({
|
|
2367
|
-
expectedState: state,
|
|
2368
|
-
redirectUrl,
|
|
2369
|
-
timeoutMs: timeoutSeconds * 1e3
|
|
2370
|
-
});
|
|
2371
|
-
if (options.open) {
|
|
2372
|
-
log.info("Opening browser for authentication...");
|
|
2373
|
-
log.info(`If the browser doesn't open, visit: ${chalk.cyan(authUrl.toString())}`);
|
|
2374
|
-
await open(authUrl.toString());
|
|
2375
|
-
} else {
|
|
2376
|
-
log.info("Open this URL to continue authentication:");
|
|
2377
|
-
log.info(chalk.cyan(authUrl.toString()));
|
|
2378
|
-
}
|
|
2379
|
-
const code = await callbackPromise;
|
|
2380
|
-
const storedSession = tokenResponseToStoredSession(await exchangeAuthorizationCode({
|
|
2381
|
-
clientId,
|
|
2382
|
-
tokenUrl
|
|
2383
|
-
}, code, codeVerifier, redirectUrl.toString()));
|
|
2384
|
-
await writeStoredAuthSession(storedSession);
|
|
2385
|
-
const email = storedSession.user?.email ?? await fetchUserEmail(process.env["BLODEMD_API_URL"] ?? "https://api.blode.md", storedSession.accessToken);
|
|
2386
|
-
if (email) log.success(`Logged in as ${chalk.cyan(email)}`);
|
|
2387
|
-
else log.success("Logged in successfully.");
|
|
2388
|
-
log.info("Done");
|
|
2389
|
-
} catch (error) {
|
|
2390
|
-
reportCommandError("Login failed", error);
|
|
2391
|
-
}
|
|
2392
|
-
});
|
|
2393
|
-
program.command("logout").description("Remove stored credentials").action(async () => {
|
|
2394
|
-
intro(chalk.bold("blodemd logout"));
|
|
2395
|
-
try {
|
|
2396
|
-
let existing = false;
|
|
2628
|
+
const registerPushCommand = (program) => {
|
|
2629
|
+
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("--branch <name>", "git branch (env: BLODEMD_BRANCH)").option("--message <msg>", "deploy message (env: BLODEMD_COMMIT_MESSAGE)").action(async (dir, options) => {
|
|
2630
|
+
intro(chalk.bold("blodemd push"));
|
|
2631
|
+
const s = spinner();
|
|
2397
2632
|
try {
|
|
2398
|
-
await
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2633
|
+
const root = await resolveDocsRoot(dir);
|
|
2634
|
+
s.start("Validating configuration");
|
|
2635
|
+
const { config, warnings } = await loadValidatedSiteConfig(root);
|
|
2636
|
+
s.stop("Configuration valid");
|
|
2637
|
+
for (const warning of warnings) log.warn(warning);
|
|
2638
|
+
const { project, projectDisplayName, apiUrl, authToken, branch, commitMessage, usedLegacyNameFallback } = await resolvePushConfig(config, options);
|
|
2639
|
+
if (usedLegacyNameFallback) log.warn(LEGACY_PROJECT_NAME_FALLBACK_WARNING);
|
|
2640
|
+
s.start("Collecting files");
|
|
2641
|
+
const files = await collectFiles(root);
|
|
2642
|
+
if (files.length === 0) throw new Error("No files found to deploy.");
|
|
2643
|
+
s.stop(`Found ${chalk.cyan(String(files.length))} files`);
|
|
2644
|
+
const headers = {
|
|
2645
|
+
Authorization: `Bearer ${authToken}`,
|
|
2646
|
+
"Content-Type": "application/json"
|
|
2647
|
+
};
|
|
2648
|
+
const apiPath = (suffix) => new URL(`/projects/slug/${project}/deployments${suffix}`, apiUrl).toString();
|
|
2649
|
+
const createDeploymentBody = JSON.stringify({
|
|
2650
|
+
branch,
|
|
2651
|
+
commitMessage
|
|
2652
|
+
});
|
|
2653
|
+
s.start("Creating deployment");
|
|
2654
|
+
let deployment;
|
|
2655
|
+
try {
|
|
2656
|
+
deployment = await requestJson(apiPath(""), {
|
|
2657
|
+
body: createDeploymentBody,
|
|
2658
|
+
headers,
|
|
2659
|
+
method: "POST"
|
|
2660
|
+
}, "Failed to create deployment");
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
if (!(error instanceof Error ? error.message : "").includes("404")) throw error;
|
|
2663
|
+
s.stop("Project not found");
|
|
2664
|
+
if (!await autoCreateProject(project, projectDisplayName, apiUrl, headers)) {
|
|
2665
|
+
log.info("Cancelled");
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
s.start("Creating deployment");
|
|
2669
|
+
deployment = await requestJson(apiPath(""), {
|
|
2670
|
+
body: createDeploymentBody,
|
|
2671
|
+
headers,
|
|
2672
|
+
method: "POST"
|
|
2673
|
+
}, "Failed to create deployment");
|
|
2674
|
+
}
|
|
2675
|
+
s.stop(`Deployment ${chalk.cyan(deployment.id)} created`);
|
|
2676
|
+
await uploadFiles(files, root, apiPath, deployment.id, headers, s);
|
|
2677
|
+
s.start("Finalizing deployment");
|
|
2678
|
+
const finalized = await requestJson(apiPath(`/${deployment.id}/finalize`), {
|
|
2679
|
+
body: JSON.stringify({ promote: true }),
|
|
2680
|
+
headers,
|
|
2681
|
+
method: "POST"
|
|
2682
|
+
}, "Failed to finalize deployment");
|
|
2683
|
+
s.stop("Deployment finalized");
|
|
2684
|
+
log.success(`Published ${chalk.cyan(finalized.id)}`);
|
|
2685
|
+
if (finalized.manifestUrl) log.info(`Manifest: ${finalized.manifestUrl}`);
|
|
2686
|
+
if (typeof finalized.fileCount === "number") log.info(`Files: ${finalized.fileCount}`);
|
|
2687
|
+
log.info("Done");
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
s.stop("Failed");
|
|
2690
|
+
reportCommandError("Push failed", error);
|
|
2417
2691
|
}
|
|
2418
|
-
const status = resolveTokenStatus(resolved);
|
|
2419
|
-
const email = resolved.user?.email ?? await fetchUserEmail(process.env["BLODEMD_API_URL"] ?? "https://api.blode.md", resolved.token);
|
|
2420
|
-
if (email) log.info(`Logged in as ${chalk.cyan(email)}`);
|
|
2421
|
-
else log.info("Logged in (could not fetch user details).");
|
|
2422
|
-
if (resolved.expiresAt && status.expired) log.warn("Session has expired. Run \"blodemd login\" to re-authenticate.");
|
|
2423
|
-
} catch (error) {
|
|
2424
|
-
reportCommandError("Whoami failed", error);
|
|
2425
|
-
}
|
|
2426
|
-
});
|
|
2427
|
-
program.command("new").description("Create a new blode.md documentation site").argument("[directory]", "target directory").option("--slug <slug>", "project slug for docs.json", parseProjectSlug).option("--name <slug>", "deprecated alias for --slug", parseProjectSlug).option("--display-name <name>", "display name for docs.json").option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
|
|
2428
|
-
await scaffoldDocsSite(directory, {
|
|
2429
|
-
displayName: options.displayName,
|
|
2430
|
-
name: options.name,
|
|
2431
|
-
slug: options.slug ?? options.name,
|
|
2432
|
-
template: options.template,
|
|
2433
|
-
yes: options.yes
|
|
2434
|
-
});
|
|
2435
|
-
});
|
|
2436
|
-
program.command("init", { hidden: true }).argument("[directory]", "target directory").option("--slug <slug>", "project slug for docs.json", parseProjectSlug).option("--name <slug>", "deprecated alias for --slug", parseProjectSlug).option("--display-name <name>", "display name for docs.json").option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
|
|
2437
|
-
await scaffoldDocsSite(directory, {
|
|
2438
|
-
deprecatedCommand: "blodemd init",
|
|
2439
|
-
displayName: options.displayName,
|
|
2440
|
-
name: options.name,
|
|
2441
|
-
slug: options.slug ?? options.name,
|
|
2442
|
-
template: options.template,
|
|
2443
|
-
yes: options.yes
|
|
2444
2692
|
});
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
log.info("Done");
|
|
2453
|
-
} catch (error) {
|
|
2454
|
-
reportCommandError("Validation failed", error);
|
|
2455
|
-
}
|
|
2456
|
-
});
|
|
2457
|
-
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("--branch <name>", "git branch (env: BLODEMD_BRANCH)").option("--message <msg>", "deploy message (env: BLODEMD_COMMIT_MESSAGE)").action(async (dir, options) => {
|
|
2458
|
-
intro(chalk.bold("blodemd push"));
|
|
2459
|
-
const s = spinner();
|
|
2460
|
-
try {
|
|
2461
|
-
const root = await resolveDocsRoot(dir);
|
|
2462
|
-
s.start("Validating configuration");
|
|
2463
|
-
const { config, warnings } = await loadValidatedSiteConfig(root);
|
|
2464
|
-
s.stop("Configuration valid");
|
|
2465
|
-
for (const warning of warnings) log.warn(warning);
|
|
2466
|
-
const { project, projectDisplayName, apiUrl, authToken, branch, commitMessage, usedLegacyNameFallback } = await resolvePushConfig(config, options);
|
|
2467
|
-
if (usedLegacyNameFallback) log.warn(LEGACY_PROJECT_NAME_FALLBACK_WARNING);
|
|
2468
|
-
s.start("Collecting files");
|
|
2469
|
-
const files = await collectFiles(root);
|
|
2470
|
-
if (files.length === 0) throw new Error("No files found to deploy.");
|
|
2471
|
-
s.stop(`Found ${chalk.cyan(String(files.length))} files`);
|
|
2472
|
-
const headers = {
|
|
2473
|
-
Authorization: `Bearer ${authToken}`,
|
|
2474
|
-
"Content-Type": "application/json"
|
|
2475
|
-
};
|
|
2476
|
-
const apiPath = (suffix) => new URL(`/projects/slug/${project}/deployments${suffix}`, apiUrl).toString();
|
|
2477
|
-
const createDeploymentBody = JSON.stringify({
|
|
2478
|
-
branch,
|
|
2479
|
-
commitMessage
|
|
2480
|
-
});
|
|
2481
|
-
s.start("Creating deployment");
|
|
2482
|
-
let deployment;
|
|
2693
|
+
};
|
|
2694
|
+
//#endregion
|
|
2695
|
+
//#region src/commands/validate.ts
|
|
2696
|
+
const CONFIG_FILE = "docs.json";
|
|
2697
|
+
const registerValidateCommand = (program) => {
|
|
2698
|
+
program.command("validate").description("Validate docs.json").argument("[dir]", "docs directory").action(async (dir) => {
|
|
2699
|
+
intro(chalk.bold("blodemd validate"));
|
|
2483
2700
|
try {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
}, "Failed to create deployment");
|
|
2701
|
+
const { warnings } = await loadValidatedSiteConfig(await resolveDocsRoot(dir));
|
|
2702
|
+
for (const warning of warnings) log.warn(warning);
|
|
2703
|
+
log.success(`${chalk.cyan(CONFIG_FILE)} is valid.`);
|
|
2704
|
+
log.info("Done");
|
|
2489
2705
|
} catch (error) {
|
|
2490
|
-
|
|
2491
|
-
s.stop("Project not found");
|
|
2492
|
-
if (!await autoCreateProject(project, projectDisplayName, apiUrl, headers)) {
|
|
2493
|
-
log.info("Cancelled");
|
|
2494
|
-
return;
|
|
2495
|
-
}
|
|
2496
|
-
s.start("Creating deployment");
|
|
2497
|
-
deployment = await requestJson(apiPath(""), {
|
|
2498
|
-
body: createDeploymentBody,
|
|
2499
|
-
headers,
|
|
2500
|
-
method: "POST"
|
|
2501
|
-
}, "Failed to create deployment");
|
|
2706
|
+
reportCommandError("Validation failed", error);
|
|
2502
2707
|
}
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2708
|
+
});
|
|
2709
|
+
};
|
|
2710
|
+
//#endregion
|
|
2711
|
+
//#region src/runtime.ts
|
|
2712
|
+
const SUPPORTED_NODE_MAJOR = 24;
|
|
2713
|
+
const SUPPORTED_NODE_RANGE = "24.x";
|
|
2714
|
+
const parseVersion = (input) => {
|
|
2715
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(input.trim());
|
|
2716
|
+
if (!match) return null;
|
|
2717
|
+
const [, majorText = "", minorText = "", patchText = ""] = match;
|
|
2718
|
+
if (!majorText || !minorText || !patchText) return null;
|
|
2719
|
+
const major = Number.parseInt(majorText, 10);
|
|
2720
|
+
const minor = Number.parseInt(minorText, 10);
|
|
2721
|
+
const patch = Number.parseInt(patchText, 10);
|
|
2722
|
+
if ([
|
|
2723
|
+
major,
|
|
2724
|
+
minor,
|
|
2725
|
+
patch
|
|
2726
|
+
].some((value) => Number.isNaN(value))) return null;
|
|
2727
|
+
return [
|
|
2728
|
+
major,
|
|
2729
|
+
minor,
|
|
2730
|
+
patch
|
|
2731
|
+
];
|
|
2732
|
+
};
|
|
2733
|
+
const isSupportedNodeVersion = (version) => {
|
|
2734
|
+
const parsed = parseVersion(version);
|
|
2735
|
+
if (!parsed) return false;
|
|
2736
|
+
const [major] = parsed;
|
|
2737
|
+
return major === SUPPORTED_NODE_MAJOR;
|
|
2738
|
+
};
|
|
2739
|
+
const assertSupportedNodeVersion = (version = process.versions.node) => {
|
|
2740
|
+
if (isSupportedNodeVersion(version)) return;
|
|
2741
|
+
throw new CliError(`blodemd requires Node.js ${SUPPORTED_NODE_RANGE}. Current version: ${version}.`, EXIT_CODES.VALIDATION, "Install a supported Node.js version and try again.");
|
|
2742
|
+
};
|
|
2743
|
+
const readCliVersion = (moduleUrl) => {
|
|
2744
|
+
const moduleDir = path.dirname(fileURLToPath(moduleUrl));
|
|
2745
|
+
const raw = readFileSync(path.resolve(moduleDir, "..", "package.json"), "utf8");
|
|
2746
|
+
return JSON.parse(raw).version ?? "0.0.0";
|
|
2747
|
+
};
|
|
2748
|
+
//#endregion
|
|
2749
|
+
//#region src/cli.ts
|
|
2750
|
+
const program = new Command();
|
|
2751
|
+
const cliVersion = readCliVersion(import.meta.url);
|
|
2752
|
+
program.name("blodemd").description("Blode.md CLI").version(cliVersion);
|
|
2753
|
+
program.hook("preAction", () => {
|
|
2754
|
+
assertSupportedNodeVersion();
|
|
2520
2755
|
});
|
|
2521
|
-
program
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2756
|
+
registerAuthCommands(program);
|
|
2757
|
+
registerNewCommand(program);
|
|
2758
|
+
registerValidateCommand(program);
|
|
2759
|
+
registerPushCommand(program);
|
|
2760
|
+
registerDevCommand(program);
|
|
2761
|
+
registerAnalyticsCommand(program);
|
|
2526
2762
|
program.parse();
|
|
2527
2763
|
//#endregion
|
|
2528
2764
|
export {};
|