blodemd 0.0.10 → 0.0.11

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/dist/cli.mjs CHANGED
@@ -20,6 +20,7 @@ import { createHash, randomBytes } from "node:crypto";
20
20
  import { readFileSync } from "node:fs";
21
21
  //#region src/constants.ts
22
22
  const CLI_NAME = "blodemd";
23
+ const BLODE_PROJECT_ENV = "BLODEMD_PROJECT";
23
24
  const OAUTH_CLIENT_ID = "6b5f9860-fe96-4a83-b1ad-266260523c91";
24
25
  const DEFAULT_OAUTH_CALLBACK_PORT = 8787;
25
26
  const DEFAULT_OAUTH_CALLBACK_PATH = "/auth/callback";
@@ -831,10 +832,48 @@ const findExistingPaths = async (root, relativePaths) => {
831
832
  }))).filter((relativePath) => relativePath !== null).toSorted((left, right) => left.localeCompare(right));
832
833
  };
833
834
  //#endregion
835
+ //#region src/project-config.ts
836
+ const LEGACY_PROJECT_NAME_FALLBACK_WARNING = "docs.json.slug is recommended. Falling back to docs.json.name as the deployment slug is deprecated.";
837
+ const validateProjectSlug = (value) => {
838
+ const trimmed = value?.trim();
839
+ if (!trimmed) return "Project slug is required.";
840
+ const normalized = slugify(trimmed);
841
+ if (!normalized) return "Use at least one letter or number.";
842
+ if (normalized !== trimmed) return `Use lowercase letters, numbers, and hyphens. Try "${normalized}".`;
843
+ };
844
+ const deriveDisplayNameFromProjectSlug = (projectSlug) => projectSlug.split("-").filter(Boolean).map((segment) => segment[0]?.toUpperCase() + segment.slice(1)).join(" ");
845
+ const resolveProjectTarget = (options) => {
846
+ if (options.cliProject) return {
847
+ project: options.cliProject,
848
+ usedLegacyNameFallback: false
849
+ };
850
+ if (options.envProject) return {
851
+ project: options.envProject,
852
+ usedLegacyNameFallback: false
853
+ };
854
+ if (options.config.slug) return {
855
+ project: options.config.slug,
856
+ usedLegacyNameFallback: false
857
+ };
858
+ if (options.config.name) return {
859
+ project: options.config.name,
860
+ usedLegacyNameFallback: true
861
+ };
862
+ return {
863
+ project: void 0,
864
+ usedLegacyNameFallback: false
865
+ };
866
+ };
867
+ const getProjectSlugError = (project) => {
868
+ if (!project) return;
869
+ return validateProjectSlug(project);
870
+ };
871
+ //#endregion
834
872
  //#region src/scaffold.ts
835
873
  const SCAFFOLD_TEMPLATES = ["minimal", "starter"];
836
874
  const DEFAULT_SCAFFOLD_DIRECTORY = "docs";
837
875
  const stringifyJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
876
+ const escapeXmlText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
838
877
  const isScaffoldTemplate = (value) => SCAFFOLD_TEMPLATES.includes(value);
839
878
  const normalizeProjectSlug = (value) => slugify(value) || "my-project";
840
879
  const resolveScaffoldDirectory = (directory) => directory?.trim() || "docs";
@@ -843,22 +882,16 @@ const deriveDefaultProjectSlug = (directory, cwd) => {
843
882
  if (resolvedDirectory === "." || resolvedDirectory === "docs") return normalizeProjectSlug(path.basename(cwd));
844
883
  return normalizeProjectSlug(path.basename(path.resolve(cwd, resolvedDirectory)));
845
884
  };
846
- const validateProjectSlug = (value) => {
847
- const trimmed = value?.trim();
848
- if (!trimmed) return "Project slug is required.";
849
- const normalized = slugify(trimmed);
850
- if (!normalized) return "Use at least one letter or number.";
851
- if (normalized !== trimmed) return `Use lowercase letters, numbers, and hyphens. Try "${normalized}".`;
852
- };
853
- const createMinimalDocsJson = (projectSlug) => ({
885
+ const createMinimalDocsJson = (projectSlug, displayName) => ({
854
886
  $schema: "https://blode.md/docs.json",
855
- name: projectSlug,
887
+ name: displayName,
856
888
  navigation: { groups: [{
857
889
  group: "Getting Started",
858
890
  pages: ["index"]
859
- }] }
891
+ }] },
892
+ slug: projectSlug
860
893
  });
861
- const createStarterDocsJson = (projectSlug) => ({
894
+ const createStarterDocsJson = (projectSlug, displayName) => ({
862
895
  $schema: "https://blode.md/docs.json",
863
896
  appearance: { default: "system" },
864
897
  contextual: { options: [
@@ -870,12 +903,12 @@ const createStarterDocsJson = (projectSlug) => ({
870
903
  description: "Ship documentation from your terminal.",
871
904
  favicon: "/favicon.svg",
872
905
  logo: {
873
- alt: `${projectSlug} logo`,
906
+ alt: `${displayName} logo`,
874
907
  dark: "/logo/dark.svg",
875
908
  light: "/logo/light.svg"
876
909
  },
877
910
  metadata: { timestamp: true },
878
- name: projectSlug,
911
+ name: displayName,
879
912
  navigation: { groups: [{
880
913
  group: "Getting Started",
881
914
  pages: [
@@ -883,7 +916,8 @@ const createStarterDocsJson = (projectSlug) => ({
883
916
  "quickstart",
884
917
  "development"
885
918
  ]
886
- }] }
919
+ }] },
920
+ slug: projectSlug
887
921
  });
888
922
  const claudeInstructions = [
889
923
  "> **First-time setup**: Customize this file for your project. Prompt the user to update terminology, style preferences, and content boundaries before drafting large amounts of docs.",
@@ -927,270 +961,276 @@ const claudeInstructions = [
927
961
  "- Run `blodemd validate` before publishing.",
928
962
  ""
929
963
  ].join("\n");
930
- const createMinimalFiles = (projectSlug) => [{
931
- content: stringifyJson(createMinimalDocsJson(projectSlug)),
964
+ const createMinimalFiles = (projectSlug, displayName) => [{
965
+ content: stringifyJson(createMinimalDocsJson(projectSlug, displayName)),
932
966
  path: "docs.json"
933
967
  }, {
934
968
  content: "---\ntitle: Welcome\n---\n\nStart writing your docs here.\n",
935
969
  path: "index.mdx"
936
970
  }];
937
- const createStarterFiles = (projectSlug) => [
938
- {
939
- content: stringifyJson(createStarterDocsJson(projectSlug)),
940
- path: "docs.json"
941
- },
942
- {
943
- content: [
944
- "---",
945
- "title: Welcome",
946
- "description: Start here.",
947
- "---",
948
- "",
949
- "# Welcome",
950
- "",
951
- "This starter gives you branded assets, repo helper files, and a small docs structure you can rewrite quickly.",
952
- "",
953
- "![Starter illustration](images/hero-light.svg)",
954
- "",
955
- "## What is included",
956
- "",
957
- "- A starter `docs.json` with branding, contextual actions, and navigation.",
958
- "- Placeholder brand assets in `/logo` and `/images`.",
959
- "- Repo helper files like `.gitignore`, `README.md`, `AGENTS.md`, and `CLAUDE.md`.",
960
- "",
961
- "## Next steps",
962
- "",
963
- "- Confirm the `name` field in `docs.json` matches your project slug.",
964
- "- Set `description` in `docs.json` to explain your product.",
965
- "- Replace the files in `/logo` and `/images` with your own brand assets.",
966
- "- Rewrite `CLAUDE.md` with your terminology and writing standards.",
967
- "- Update this page, then preview locally with `blodemd dev`.",
968
- "",
969
- "## Included pages",
970
- "",
971
- "- [Quickstart](quickstart)",
972
- "- [Development](development)",
973
- ""
974
- ].join("\n"),
975
- path: "index.mdx"
976
- },
977
- {
978
- content: [
979
- "---",
980
- "title: Quickstart",
981
- "description: Get your docs running fast.",
982
- "---",
983
- "",
984
- "# Quickstart",
985
- "",
986
- "![Setup checklist](images/checks-passed.svg)",
987
- "",
988
- "1. Confirm the `name` field in `docs.json`.",
989
- "2. Update the `description` field to match your product.",
990
- "3. Replace the assets in `/logo` and `/images`.",
991
- "4. Run `blodemd dev` to preview locally.",
992
- "5. Run `blodemd push` when you are ready to publish.",
993
- ""
994
- ].join("\n"),
995
- path: "quickstart.mdx"
996
- },
997
- {
998
- content: [
999
- "---",
1000
- "title: Development",
1001
- "description: Work on your docs locally.",
1002
- "---",
1003
- "",
1004
- "# Development",
1005
- "",
1006
- "![Dark preview illustration](images/hero-dark.svg)",
1007
- "",
1008
- "Preview locally with:",
1009
- "",
1010
- "```bash",
1011
- "blodemd dev",
1012
- "```",
1013
- "",
1014
- "Validate your configuration with:",
1015
- "",
1016
- "```bash",
1017
- "blodemd validate",
1018
- "```",
1019
- "",
1020
- "Keep `CLAUDE.md` current as your product terminology and writing rules evolve.",
1021
- ""
1022
- ].join("\n"),
1023
- path: "development.mdx"
1024
- },
1025
- {
1026
- content: [
1027
- "# Documentation starter",
1028
- "",
1029
- "This directory was scaffolded with `blodemd new --template starter`.",
1030
- "",
1031
- "## What is included",
1032
- "",
1033
- "- `docs.json` with branding, contextual actions, and starter navigation",
1034
- "- `index.mdx`, `quickstart.mdx`, and `development.mdx`",
1035
- "- Placeholder brand assets in `/logo` and `/images`",
1036
- "- Repo helper files: `.gitignore`, `README.md`, `AGENTS.md`, and `CLAUDE.md`",
1037
- "",
1038
- "## Commands",
1039
- "",
1040
- "```bash",
1041
- "blodemd dev",
1042
- "blodemd validate",
1043
- "blodemd push",
1044
- "```",
1045
- "",
1046
- "## Customize",
1047
- "",
1048
- "- Confirm the project slug and set the description in `docs.json`.",
1049
- "- Replace the assets in `/logo` and `/images`.",
1050
- "- Rewrite `CLAUDE.md` with project-specific terminology and writing rules.",
1051
- "- Rewrite the starter pages to match your product.",
1052
- "- Add a `LICENSE` file deliberately if this repo will be public.",
1053
- ""
1054
- ].join("\n"),
1055
- path: "README.md"
1056
- },
1057
- {
1058
- fallbackContent: claudeInstructions,
1059
- path: "AGENTS.md",
1060
- target: "CLAUDE.md",
1061
- type: "symlink"
1062
- },
1063
- {
1064
- content: claudeInstructions,
1065
- path: "CLAUDE.md"
1066
- },
1067
- {
1068
- content: [
1069
- "# dependencies",
1070
- "node_modules/",
1071
- "",
1072
- "# local env files",
1073
- ".env*",
1074
- "!.env.example",
1075
- "",
1076
- "# build and cache",
1077
- ".next/",
1078
- ".turbo/",
1079
- "coverage/",
1080
- "dist/",
1081
- ".vercel/",
1082
- "*.tsbuildinfo",
1083
- "",
1084
- "# logs",
1085
- "*.log",
1086
- "",
1087
- "# misc",
1088
- ".DS_Store",
1089
- ""
1090
- ].join("\n"),
1091
- path: ".gitignore"
1092
- },
1093
- {
1094
- content: [
1095
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\" fill=\"none\">",
1096
- " <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#0D9373\"/>",
1097
- " <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#CFF6EE\"/>",
1098
- " <path d=\"M28 26h6c5.523 0 10 4.477 10 10s-4.477 10-10 10h-6V26Z\" fill=\"#0C3A33\"/>",
1099
- "</svg>",
1100
- ""
1101
- ].join("\n"),
1102
- path: "favicon.svg"
1103
- },
1104
- {
1105
- content: [
1106
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 240 64\" fill=\"none\">",
1107
- " <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#0C3A33\"/>",
1108
- " <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#CFF6EE\"/>",
1109
- ` <text x="84" y="41" fill="#111827" font-family="Arial, sans-serif" font-size="28" font-weight="700">${projectSlug}</text>`,
1110
- "</svg>",
1111
- ""
1112
- ].join("\n"),
1113
- path: "logo/light.svg"
1114
- },
1115
- {
1116
- content: [
1117
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 240 64\" fill=\"none\">",
1118
- " <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#CFF6EE\"/>",
1119
- " <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#0C3A33\"/>",
1120
- ` <text x="84" y="41" fill="#F9FAFB" font-family="Arial, sans-serif" font-size="28" font-weight="700">${projectSlug}</text>`,
1121
- "</svg>",
1122
- ""
1123
- ].join("\n"),
1124
- path: "logo/dark.svg"
1125
- },
1126
- {
1127
- content: [
1128
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
1129
- " <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#F4FBF8\"/>",
1130
- " <rect x=\"48\" y=\"48\" width=\"260\" height=\"424\" rx=\"24\" fill=\"#E1F4EE\"/>",
1131
- " <rect x=\"96\" y=\"120\" width=\"164\" height=\"20\" rx=\"10\" fill=\"#0D9373\" opacity=\".25\"/>",
1132
- " <rect x=\"96\" y=\"164\" width=\"132\" height=\"16\" rx=\"8\" fill=\"#0D9373\" opacity=\".18\"/>",
1133
- " <rect x=\"96\" y=\"204\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#0D9373\" opacity=\".18\"/>",
1134
- " <rect x=\"356\" y=\"80\" width=\"556\" height=\"104\" rx=\"24\" fill=\"#0D9373\"/>",
1135
- " <rect x=\"388\" y=\"116\" width=\"220\" height=\"18\" rx=\"9\" fill=\"#CFF6EE\"/>",
1136
- " <rect x=\"388\" y=\"148\" width=\"156\" height=\"14\" rx=\"7\" fill=\"#CFF6EE\" opacity=\".7\"/>",
1137
- " <rect x=\"356\" y=\"216\" width=\"268\" height=\"256\" rx=\"24\" fill=\"#FFFFFF\"/>",
1138
- " <rect x=\"388\" y=\"260\" width=\"168\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".18\"/>",
1139
- " <rect x=\"388\" y=\"292\" width=\"196\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".12\"/>",
1140
- " <rect x=\"656\" y=\"216\" width=\"256\" height=\"256\" rx=\"24\" fill=\"#0C3A33\"/>",
1141
- " <rect x=\"692\" y=\"260\" width=\"128\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".85\"/>",
1142
- " <rect x=\"692\" y=\"292\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".45\"/>",
1143
- " <circle cx=\"804\" cy=\"380\" r=\"52\" fill=\"#0D9373\"/>",
1144
- "</svg>",
1145
- ""
1146
- ].join("\n"),
1147
- path: "images/hero-light.svg"
1148
- },
1149
- {
1150
- content: [
1151
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
1152
- " <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#071715\"/>",
1153
- " <rect x=\"48\" y=\"48\" width=\"260\" height=\"424\" rx=\"24\" fill=\"#0F2E28\"/>",
1154
- " <rect x=\"96\" y=\"120\" width=\"164\" height=\"20\" rx=\"10\" fill=\"#CFF6EE\" opacity=\".18\"/>",
1155
- " <rect x=\"96\" y=\"164\" width=\"132\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".14\"/>",
1156
- " <rect x=\"96\" y=\"204\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".14\"/>",
1157
- " <rect x=\"356\" y=\"80\" width=\"556\" height=\"104\" rx=\"24\" fill=\"#0D9373\"/>",
1158
- " <rect x=\"388\" y=\"116\" width=\"220\" height=\"18\" rx=\"9\" fill=\"#E8FFF9\"/>",
1159
- " <rect x=\"388\" y=\"148\" width=\"156\" height=\"14\" rx=\"7\" fill=\"#E8FFF9\" opacity=\".6\"/>",
1160
- " <rect x=\"356\" y=\"216\" width=\"268\" height=\"256\" rx=\"24\" fill=\"#0C3A33\"/>",
1161
- " <rect x=\"388\" y=\"260\" width=\"168\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".22\"/>",
1162
- " <rect x=\"388\" y=\"292\" width=\"196\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".16\"/>",
1163
- " <rect x=\"656\" y=\"216\" width=\"256\" height=\"256\" rx=\"24\" fill=\"#E9FFF8\"/>",
1164
- " <rect x=\"692\" y=\"260\" width=\"128\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".24\"/>",
1165
- " <rect x=\"692\" y=\"292\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".12\"/>",
1166
- " <circle cx=\"804\" cy=\"380\" r=\"52\" fill=\"#0D9373\"/>",
1167
- "</svg>",
1168
- ""
1169
- ].join("\n"),
1170
- path: "images/hero-dark.svg"
1171
- },
1172
- {
1173
- content: [
1174
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
1175
- " <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#F8FCFA\"/>",
1176
- " <rect x=\"60\" y=\"76\" width=\"840\" height=\"368\" rx=\"28\" fill=\"#FFFFFF\" stroke=\"#D7ECE6\" stroke-width=\"4\"/>",
1177
- " <rect x=\"108\" y=\"124\" width=\"96\" height=\"96\" rx=\"24\" fill=\"#0D9373\"/>",
1178
- " <path d=\"M136 172l18 18 38-48\" stroke=\"#CFF6EE\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"18\"/>",
1179
- " <rect x=\"244\" y=\"132\" width=\"280\" height=\"24\" rx=\"12\" fill=\"#0C3A33\"/>",
1180
- " <rect x=\"244\" y=\"176\" width=\"416\" height=\"18\" rx=\"9\" fill=\"#0C3A33\" opacity=\".16\"/>",
1181
- " <rect x=\"244\" y=\"214\" width=\"340\" height=\"18\" rx=\"9\" fill=\"#0C3A33\" opacity=\".12\"/>",
1182
- " <rect x=\"108\" y=\"280\" width=\"744\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
1183
- " <rect x=\"108\" y=\"326\" width=\"520\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
1184
- " <rect x=\"108\" y=\"372\" width=\"612\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
1185
- "</svg>",
1186
- ""
1187
- ].join("\n"),
1188
- path: "images/checks-passed.svg"
1189
- }
1190
- ];
971
+ const createStarterFiles = (projectSlug, displayName) => {
972
+ const escapedDisplayName = escapeXmlText(displayName);
973
+ return [
974
+ {
975
+ content: stringifyJson(createStarterDocsJson(projectSlug, displayName)),
976
+ path: "docs.json"
977
+ },
978
+ {
979
+ content: [
980
+ "---",
981
+ "title: Welcome",
982
+ "description: Start here.",
983
+ "---",
984
+ "",
985
+ "# Welcome",
986
+ "",
987
+ "This starter gives you branded assets, repo helper files, and a small docs structure you can rewrite quickly.",
988
+ "",
989
+ "![Starter illustration](images/hero-light.svg)",
990
+ "",
991
+ "## What is included",
992
+ "",
993
+ "- A starter `docs.json` with branding, contextual actions, and navigation.",
994
+ "- Placeholder brand assets in `/logo` and `/images`.",
995
+ "- Repo helper files like `.gitignore`, `README.md`, `AGENTS.md`, and `CLAUDE.md`.",
996
+ "",
997
+ "## Next steps",
998
+ "",
999
+ "- Confirm `slug` in `docs.json` matches your deployment target.",
1000
+ "- Update `name` in `docs.json` to match the visible product or docs brand.",
1001
+ "- Set `description` in `docs.json` to explain your product.",
1002
+ "- Replace the files in `/logo` and `/images` with your own brand assets.",
1003
+ "- Rewrite `CLAUDE.md` with your terminology and writing standards.",
1004
+ "- Update this page, then preview locally with `blodemd dev`.",
1005
+ "",
1006
+ "## Included pages",
1007
+ "",
1008
+ "- [Quickstart](quickstart)",
1009
+ "- [Development](development)",
1010
+ ""
1011
+ ].join("\n"),
1012
+ path: "index.mdx"
1013
+ },
1014
+ {
1015
+ content: [
1016
+ "---",
1017
+ "title: Quickstart",
1018
+ "description: Get your docs running fast.",
1019
+ "---",
1020
+ "",
1021
+ "# Quickstart",
1022
+ "",
1023
+ "![Setup checklist](images/checks-passed.svg)",
1024
+ "",
1025
+ "1. Confirm `slug` in `docs.json` matches your deployment target.",
1026
+ "2. Update `name` in `docs.json` to match your visible docs brand.",
1027
+ "3. Update the `description` field to match your product.",
1028
+ "4. Replace the assets in `/logo` and `/images`.",
1029
+ "5. Run `blodemd dev` to preview locally.",
1030
+ "6. Run `blodemd push` when you are ready to publish.",
1031
+ ""
1032
+ ].join("\n"),
1033
+ path: "quickstart.mdx"
1034
+ },
1035
+ {
1036
+ content: [
1037
+ "---",
1038
+ "title: Development",
1039
+ "description: Work on your docs locally.",
1040
+ "---",
1041
+ "",
1042
+ "# Development",
1043
+ "",
1044
+ "![Dark preview illustration](images/hero-dark.svg)",
1045
+ "",
1046
+ "Preview locally with:",
1047
+ "",
1048
+ "```bash",
1049
+ "blodemd dev",
1050
+ "```",
1051
+ "",
1052
+ "Validate your configuration with:",
1053
+ "",
1054
+ "```bash",
1055
+ "blodemd validate",
1056
+ "```",
1057
+ "",
1058
+ "Keep `CLAUDE.md` current as your product terminology and writing rules evolve.",
1059
+ ""
1060
+ ].join("\n"),
1061
+ path: "development.mdx"
1062
+ },
1063
+ {
1064
+ content: [
1065
+ "# Documentation starter",
1066
+ "",
1067
+ "This directory was scaffolded with `blodemd new --template starter`.",
1068
+ "",
1069
+ "## What is included",
1070
+ "",
1071
+ "- `docs.json` with branding, contextual actions, and starter navigation",
1072
+ "- `index.mdx`, `quickstart.mdx`, and `development.mdx`",
1073
+ "- Placeholder brand assets in `/logo` and `/images`",
1074
+ "- Repo helper files: `.gitignore`, `README.md`, `AGENTS.md`, and `CLAUDE.md`",
1075
+ "",
1076
+ "## Commands",
1077
+ "",
1078
+ "```bash",
1079
+ "blodemd dev",
1080
+ "blodemd validate",
1081
+ "blodemd push",
1082
+ "```",
1083
+ "",
1084
+ "## Customize",
1085
+ "",
1086
+ "- Confirm `slug` in `docs.json` and set the display `name` and description.",
1087
+ "- Replace the assets in `/logo` and `/images`.",
1088
+ "- Rewrite `CLAUDE.md` with project-specific terminology and writing rules.",
1089
+ "- Rewrite the starter pages to match your product.",
1090
+ "- Add a `LICENSE` file deliberately if this repo will be public.",
1091
+ ""
1092
+ ].join("\n"),
1093
+ path: "README.md"
1094
+ },
1095
+ {
1096
+ fallbackContent: claudeInstructions,
1097
+ path: "AGENTS.md",
1098
+ target: "CLAUDE.md",
1099
+ type: "symlink"
1100
+ },
1101
+ {
1102
+ content: claudeInstructions,
1103
+ path: "CLAUDE.md"
1104
+ },
1105
+ {
1106
+ content: [
1107
+ "# dependencies",
1108
+ "node_modules/",
1109
+ "",
1110
+ "# local env files",
1111
+ ".env*",
1112
+ "!.env.example",
1113
+ "",
1114
+ "# build and cache",
1115
+ ".next/",
1116
+ ".turbo/",
1117
+ "coverage/",
1118
+ "dist/",
1119
+ ".vercel/",
1120
+ "*.tsbuildinfo",
1121
+ "",
1122
+ "# logs",
1123
+ "*.log",
1124
+ "",
1125
+ "# misc",
1126
+ ".DS_Store",
1127
+ ""
1128
+ ].join("\n"),
1129
+ path: ".gitignore"
1130
+ },
1131
+ {
1132
+ content: [
1133
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\" fill=\"none\">",
1134
+ " <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#0D9373\"/>",
1135
+ " <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#CFF6EE\"/>",
1136
+ " <path d=\"M28 26h6c5.523 0 10 4.477 10 10s-4.477 10-10 10h-6V26Z\" fill=\"#0C3A33\"/>",
1137
+ "</svg>",
1138
+ ""
1139
+ ].join("\n"),
1140
+ path: "favicon.svg"
1141
+ },
1142
+ {
1143
+ content: [
1144
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 240 64\" fill=\"none\">",
1145
+ " <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#0C3A33\"/>",
1146
+ " <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#CFF6EE\"/>",
1147
+ ` <text x="84" y="41" fill="#111827" font-family="Arial, sans-serif" font-size="28" font-weight="700">${escapedDisplayName}</text>`,
1148
+ "</svg>",
1149
+ ""
1150
+ ].join("\n"),
1151
+ path: "logo/light.svg"
1152
+ },
1153
+ {
1154
+ content: [
1155
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 240 64\" fill=\"none\">",
1156
+ " <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#CFF6EE\"/>",
1157
+ " <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#0C3A33\"/>",
1158
+ ` <text x="84" y="41" fill="#F9FAFB" font-family="Arial, sans-serif" font-size="28" font-weight="700">${escapedDisplayName}</text>`,
1159
+ "</svg>",
1160
+ ""
1161
+ ].join("\n"),
1162
+ path: "logo/dark.svg"
1163
+ },
1164
+ {
1165
+ content: [
1166
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
1167
+ " <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#F4FBF8\"/>",
1168
+ " <rect x=\"48\" y=\"48\" width=\"260\" height=\"424\" rx=\"24\" fill=\"#E1F4EE\"/>",
1169
+ " <rect x=\"96\" y=\"120\" width=\"164\" height=\"20\" rx=\"10\" fill=\"#0D9373\" opacity=\".25\"/>",
1170
+ " <rect x=\"96\" y=\"164\" width=\"132\" height=\"16\" rx=\"8\" fill=\"#0D9373\" opacity=\".18\"/>",
1171
+ " <rect x=\"96\" y=\"204\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#0D9373\" opacity=\".18\"/>",
1172
+ " <rect x=\"356\" y=\"80\" width=\"556\" height=\"104\" rx=\"24\" fill=\"#0D9373\"/>",
1173
+ " <rect x=\"388\" y=\"116\" width=\"220\" height=\"18\" rx=\"9\" fill=\"#CFF6EE\"/>",
1174
+ " <rect x=\"388\" y=\"148\" width=\"156\" height=\"14\" rx=\"7\" fill=\"#CFF6EE\" opacity=\".7\"/>",
1175
+ " <rect x=\"356\" y=\"216\" width=\"268\" height=\"256\" rx=\"24\" fill=\"#FFFFFF\"/>",
1176
+ " <rect x=\"388\" y=\"260\" width=\"168\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".18\"/>",
1177
+ " <rect x=\"388\" y=\"292\" width=\"196\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".12\"/>",
1178
+ " <rect x=\"656\" y=\"216\" width=\"256\" height=\"256\" rx=\"24\" fill=\"#0C3A33\"/>",
1179
+ " <rect x=\"692\" y=\"260\" width=\"128\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".85\"/>",
1180
+ " <rect x=\"692\" y=\"292\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".45\"/>",
1181
+ " <circle cx=\"804\" cy=\"380\" r=\"52\" fill=\"#0D9373\"/>",
1182
+ "</svg>",
1183
+ ""
1184
+ ].join("\n"),
1185
+ path: "images/hero-light.svg"
1186
+ },
1187
+ {
1188
+ content: [
1189
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
1190
+ " <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#071715\"/>",
1191
+ " <rect x=\"48\" y=\"48\" width=\"260\" height=\"424\" rx=\"24\" fill=\"#0F2E28\"/>",
1192
+ " <rect x=\"96\" y=\"120\" width=\"164\" height=\"20\" rx=\"10\" fill=\"#CFF6EE\" opacity=\".18\"/>",
1193
+ " <rect x=\"96\" y=\"164\" width=\"132\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".14\"/>",
1194
+ " <rect x=\"96\" y=\"204\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".14\"/>",
1195
+ " <rect x=\"356\" y=\"80\" width=\"556\" height=\"104\" rx=\"24\" fill=\"#0D9373\"/>",
1196
+ " <rect x=\"388\" y=\"116\" width=\"220\" height=\"18\" rx=\"9\" fill=\"#E8FFF9\"/>",
1197
+ " <rect x=\"388\" y=\"148\" width=\"156\" height=\"14\" rx=\"7\" fill=\"#E8FFF9\" opacity=\".6\"/>",
1198
+ " <rect x=\"356\" y=\"216\" width=\"268\" height=\"256\" rx=\"24\" fill=\"#0C3A33\"/>",
1199
+ " <rect x=\"388\" y=\"260\" width=\"168\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".22\"/>",
1200
+ " <rect x=\"388\" y=\"292\" width=\"196\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".16\"/>",
1201
+ " <rect x=\"656\" y=\"216\" width=\"256\" height=\"256\" rx=\"24\" fill=\"#E9FFF8\"/>",
1202
+ " <rect x=\"692\" y=\"260\" width=\"128\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".24\"/>",
1203
+ " <rect x=\"692\" y=\"292\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".12\"/>",
1204
+ " <circle cx=\"804\" cy=\"380\" r=\"52\" fill=\"#0D9373\"/>",
1205
+ "</svg>",
1206
+ ""
1207
+ ].join("\n"),
1208
+ path: "images/hero-dark.svg"
1209
+ },
1210
+ {
1211
+ content: [
1212
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
1213
+ " <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#F8FCFA\"/>",
1214
+ " <rect x=\"60\" y=\"76\" width=\"840\" height=\"368\" rx=\"28\" fill=\"#FFFFFF\" stroke=\"#D7ECE6\" stroke-width=\"4\"/>",
1215
+ " <rect x=\"108\" y=\"124\" width=\"96\" height=\"96\" rx=\"24\" fill=\"#0D9373\"/>",
1216
+ " <path d=\"M136 172l18 18 38-48\" stroke=\"#CFF6EE\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"18\"/>",
1217
+ " <rect x=\"244\" y=\"132\" width=\"280\" height=\"24\" rx=\"12\" fill=\"#0C3A33\"/>",
1218
+ " <rect x=\"244\" y=\"176\" width=\"416\" height=\"18\" rx=\"9\" fill=\"#0C3A33\" opacity=\".16\"/>",
1219
+ " <rect x=\"244\" y=\"214\" width=\"340\" height=\"18\" rx=\"9\" fill=\"#0C3A33\" opacity=\".12\"/>",
1220
+ " <rect x=\"108\" y=\"280\" width=\"744\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
1221
+ " <rect x=\"108\" y=\"326\" width=\"520\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
1222
+ " <rect x=\"108\" y=\"372\" width=\"612\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
1223
+ "</svg>",
1224
+ ""
1225
+ ].join("\n"),
1226
+ path: "images/checks-passed.svg"
1227
+ }
1228
+ ];
1229
+ };
1191
1230
  const getScaffoldFiles = (template, options) => {
1192
1231
  const projectSlug = options?.projectSlug ?? "my-project";
1193
- return template === "starter" ? createStarterFiles(projectSlug) : createMinimalFiles(projectSlug);
1232
+ const displayName = options?.displayName ?? deriveDisplayNameFromProjectSlug(projectSlug);
1233
+ return template === "starter" ? createStarterFiles(projectSlug, displayName) : createMinimalFiles(projectSlug, displayName);
1194
1234
  };
1195
1235
  //#endregion
1196
1236
  //#region src/new-flow.ts
@@ -1497,6 +1537,18 @@ const promptForProjectSlug = async (initialValue) => {
1497
1537
  if (isCancel(projectSlug)) return;
1498
1538
  return projectSlug.trim();
1499
1539
  };
1540
+ const promptForDisplayName = async (initialValue) => {
1541
+ const displayName = await text({
1542
+ initialValue,
1543
+ message: "Display name",
1544
+ placeholder: initialValue,
1545
+ validate: (value) => {
1546
+ if (!value?.trim()) return "Display name is required.";
1547
+ }
1548
+ });
1549
+ if (isCancel(displayName)) return;
1550
+ return displayName.trim();
1551
+ };
1500
1552
  const resolveRequestedDirectory = async (directory, shouldPrompt) => {
1501
1553
  let currentDirectoryEntries = [];
1502
1554
  if (!directory && shouldPrompt) currentDirectoryEntries = await fs.readdir(process.cwd());
@@ -1527,14 +1579,20 @@ const confirmScaffoldTarget = async (root, template, shouldPrompt, options) => {
1527
1579
  const shouldContinue = await confirm({ message: `Scaffold into the non-empty directory ${root}? Existing files will be left untouched.` });
1528
1580
  return !isCancel(shouldContinue) && shouldContinue;
1529
1581
  };
1530
- const resolveProjectSlug = async (providedName, directory, shouldPrompt) => {
1582
+ const resolveProjectSlug = async (providedSlug, directory, shouldPrompt) => {
1531
1583
  const defaultProjectSlug = deriveDefaultProjectSlug(directory, process.cwd());
1532
- if (providedName) return providedName;
1584
+ if (providedSlug) return providedSlug;
1533
1585
  if (!shouldPrompt) return defaultProjectSlug;
1534
1586
  return await promptForProjectSlug(defaultProjectSlug);
1535
1587
  };
1536
- const writeScaffoldFiles = async (root, template, projectSlug) => {
1537
- for (const file of getScaffoldFiles(template, { projectSlug })) {
1588
+ const resolveDisplayName = async (providedDisplayName, projectSlug, shouldPrompt) => {
1589
+ const defaultDisplayName = deriveDisplayNameFromProjectSlug(projectSlug);
1590
+ if (providedDisplayName?.trim()) return providedDisplayName.trim();
1591
+ if (!shouldPrompt) return defaultDisplayName;
1592
+ return await promptForDisplayName(defaultDisplayName);
1593
+ };
1594
+ const writeScaffoldFiles = async (root, template, options) => {
1595
+ for (const file of getScaffoldFiles(template, options)) {
1538
1596
  const filePath = path.join(root, file.path);
1539
1597
  await fs.mkdir(path.dirname(filePath), { recursive: true });
1540
1598
  if (file.type === "symlink") {
@@ -1552,7 +1610,11 @@ const fetchUserEmail = async (apiUrl, token) => {
1552
1610
  }
1553
1611
  };
1554
1612
  const resolvePushConfig = async (config, options) => {
1555
- const project = options.project ?? process.env["BLODEMD_PROJECT"] ?? config.name;
1613
+ const { project, usedLegacyNameFallback } = resolveProjectTarget({
1614
+ cliProject: options.project,
1615
+ config,
1616
+ envProject: process.env[BLODE_PROJECT_ENV]
1617
+ });
1556
1618
  const apiUrl = options.apiUrl ?? process.env["BLODEMD_API_URL"] ?? "https://api.blode.md";
1557
1619
  const authToken = (await resolveAuthToken(options.apiKey))?.token;
1558
1620
  const branch = options.branch ?? process.env["BLODEMD_BRANCH"] ?? process.env.GITHUB_REF_NAME ?? readGitValue([
@@ -1565,23 +1627,30 @@ const resolvePushConfig = async (config, options) => {
1565
1627
  "-1",
1566
1628
  "--pretty=%s"
1567
1629
  ]);
1568
- if (!project) throw new Error("Missing project slug. Set \"name\" in docs.json, pass --project, or set BLODEMD_PROJECT.");
1630
+ if (!project) throw new Error("Missing project slug. Set \"slug\" in docs.json, pass --project, or set BLODEMD_PROJECT.");
1631
+ const projectSlugError = getProjectSlugError(project);
1632
+ if (projectSlugError) {
1633
+ if (usedLegacyNameFallback) throw new Error(`docs.json.name is not a valid deployment slug. Add "slug" to docs.json, pass --project, or set BLODEMD_PROJECT. ${projectSlugError}`);
1634
+ throw new Error(`Invalid project slug "${project}". ${projectSlugError}`);
1635
+ }
1569
1636
  if (!authToken) throw new Error("Missing credentials. Run \"blodemd login\", pass --api-key, or set BLODEMD_API_KEY.");
1570
1637
  return {
1571
1638
  apiUrl,
1572
1639
  authToken,
1573
1640
  branch,
1574
1641
  commitMessage,
1575
- project
1642
+ project,
1643
+ projectDisplayName: config.name?.trim() || project,
1644
+ usedLegacyNameFallback
1576
1645
  };
1577
1646
  };
1578
- const autoCreateProject = async (project, apiUrl, headers) => {
1647
+ const autoCreateProject = async (project, projectDisplayName, apiUrl, headers) => {
1579
1648
  if (!(await readAuthFile())?.session) throw new Error(`Project "${project}" not found. Create it at blode.md or login with "blodemd login" to auto-create.`);
1580
1649
  const shouldCreate = await confirm({ message: `Project "${project}" doesn't exist. Create it?` });
1581
1650
  if (isCancel(shouldCreate) || !shouldCreate) return false;
1582
1651
  const createResult = await requestJson(new URL("/projects", apiUrl).toString(), {
1583
1652
  body: JSON.stringify({
1584
- name: project,
1653
+ name: projectDisplayName,
1585
1654
  slug: project
1586
1655
  }),
1587
1656
  headers,
@@ -1594,6 +1663,7 @@ const autoCreateProject = async (project, apiUrl, headers) => {
1594
1663
  const scaffoldDocsSite = async (directory, options) => {
1595
1664
  intro(chalk.bold("blodemd new"));
1596
1665
  if (options?.deprecatedCommand) log.warn(`"${options.deprecatedCommand}" is deprecated. Use ${chalk.cyan("blodemd new")} instead.`);
1666
+ if (options?.name && !options.slug) log.warn(`"${chalk.cyan("--name")}" is deprecated. Use ${chalk.cyan("--slug")} instead.`);
1597
1667
  try {
1598
1668
  const template = options?.template ?? "minimal";
1599
1669
  const shouldPrompt = isInteractiveTerminal() && !options?.yes;
@@ -1608,15 +1678,24 @@ const scaffoldDocsSite = async (directory, options) => {
1608
1678
  log.warn("Cancelled");
1609
1679
  return;
1610
1680
  }
1611
- const projectSlug = await resolveProjectSlug(options?.name, resolvedDirectory, shouldPrompt);
1681
+ const projectSlug = await resolveProjectSlug(options?.slug ?? options?.name, resolvedDirectory, shouldPrompt);
1612
1682
  if (!projectSlug) {
1613
1683
  log.warn("Cancelled");
1614
1684
  return;
1615
1685
  }
1686
+ const displayName = await resolveDisplayName(options?.displayName, projectSlug, shouldPrompt);
1687
+ if (!displayName) {
1688
+ log.warn("Cancelled");
1689
+ return;
1690
+ }
1616
1691
  await fs.mkdir(root, { recursive: true });
1617
- await writeScaffoldFiles(root, template, projectSlug);
1692
+ await writeScaffoldFiles(root, template, {
1693
+ displayName,
1694
+ projectSlug
1695
+ });
1618
1696
  log.success(`Docs scaffolded in ${chalk.cyan(root)}`);
1619
1697
  if (template === "starter") log.info("Starter template includes brand assets and helper files.");
1698
+ log.info(`Display name: ${chalk.cyan(displayName)}`);
1620
1699
  log.info(`Project slug: ${chalk.cyan(projectSlug)}`);
1621
1700
  log.info("Done");
1622
1701
  } catch (error) {
@@ -1757,17 +1836,21 @@ program.command("whoami").description("Show current authentication").action(asyn
1757
1836
  reportCommandError("Whoami failed", error);
1758
1837
  }
1759
1838
  });
1760
- program.command("new").description("Create a new blode.md documentation site").argument("[directory]", "target directory").option("--name <slug>", "project slug for docs.json", parseProjectSlug).option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
1839
+ 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) => {
1761
1840
  await scaffoldDocsSite(directory, {
1841
+ displayName: options.displayName,
1762
1842
  name: options.name,
1843
+ slug: options.slug ?? options.name,
1763
1844
  template: options.template,
1764
1845
  yes: options.yes
1765
1846
  });
1766
1847
  });
1767
- program.command("init", { hidden: true }).argument("[directory]", "target directory").option("--name <slug>", "project slug for docs.json", parseProjectSlug).option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
1848
+ 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) => {
1768
1849
  await scaffoldDocsSite(directory, {
1769
1850
  deprecatedCommand: "blodemd init",
1851
+ displayName: options.displayName,
1770
1852
  name: options.name,
1853
+ slug: options.slug ?? options.name,
1771
1854
  template: options.template,
1772
1855
  yes: options.yes
1773
1856
  });
@@ -1792,7 +1875,8 @@ program.command("push").description("Deploy docs").argument("[dir]", "docs direc
1792
1875
  const { config, warnings } = await loadValidatedSiteConfig(root);
1793
1876
  s.stop("Configuration valid");
1794
1877
  for (const warning of warnings) log.warn(warning);
1795
- const { project, apiUrl, authToken, branch, commitMessage } = await resolvePushConfig(config, options);
1878
+ const { project, projectDisplayName, apiUrl, authToken, branch, commitMessage, usedLegacyNameFallback } = await resolvePushConfig(config, options);
1879
+ if (usedLegacyNameFallback) log.warn(LEGACY_PROJECT_NAME_FALLBACK_WARNING);
1796
1880
  s.start("Collecting files");
1797
1881
  const files = await collectFiles(root);
1798
1882
  if (files.length === 0) throw new Error("No files found to deploy.");
@@ -1817,7 +1901,7 @@ program.command("push").description("Deploy docs").argument("[dir]", "docs direc
1817
1901
  } catch (error) {
1818
1902
  if (!(error instanceof Error ? error.message : "").includes("404")) throw error;
1819
1903
  s.stop("Project not found");
1820
- if (!await autoCreateProject(project, apiUrl, headers)) {
1904
+ if (!await autoCreateProject(project, projectDisplayName, apiUrl, headers)) {
1821
1905
  log.info("Cancelled");
1822
1906
  return;
1823
1907
  }