create-dox 0.1.0 → 0.3.0

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.
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/scaffold.ts
4
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "fs";
5
+ import { resolve } from "path";
6
+
7
+ // src/download.ts
8
+ import { Readable, pipeline } from "stream";
9
+ import { promisify } from "util";
10
+ import tar from "tar";
11
+ var pipelineAsync = promisify(pipeline);
12
+ var TARBALL_URL = "https://codeload.github.com/kenny-io/Dox/tar.gz/main";
13
+ var EXCLUDE_PATHS = ["/cli/", "/packages/", "/node_modules/", "/.git/"];
14
+ function shouldInclude(path) {
15
+ for (const excluded of EXCLUDE_PATHS) {
16
+ if (path.includes(excluded)) {
17
+ return false;
18
+ }
19
+ }
20
+ return true;
21
+ }
22
+ async function downloadTemplate(targetDir) {
23
+ console.log("");
24
+ console.log(" \u23F3 Downloading Dox template...");
25
+ const response = await fetch(TARBALL_URL);
26
+ if (!response.ok) {
27
+ throw new Error(`Failed to download template: ${response.status} ${response.statusText}`);
28
+ }
29
+ if (!response.body) {
30
+ throw new Error("Response body is empty");
31
+ }
32
+ const nodeStream = Readable.fromWeb(response.body);
33
+ await pipelineAsync(
34
+ nodeStream,
35
+ tar.extract({ cwd: targetDir, strip: 1, filter: shouldInclude })
36
+ );
37
+ }
38
+
39
+ // src/customize.ts
40
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, cpSync } from "fs";
41
+ import { join } from "path";
42
+ import { execSync } from "child_process";
43
+ var STARTER_PAGES = {
44
+ "introduction.mdx": `---
45
+ title: Introduction
46
+ description: Welcome to {NAME} documentation.
47
+ ---
48
+
49
+ ## Welcome
50
+
51
+ This is the home page of your **{NAME}** documentation site, powered by [Dox](https://github.com/kenny-io/Dox).
52
+
53
+ Get started by editing this file at \`src/content/introduction.mdx\`.
54
+ `,
55
+ "quickstart.mdx": `---
56
+ title: Quickstart
57
+ description: Get up and running with {NAME} in under 5 minutes.
58
+ ---
59
+
60
+ ## Installation
61
+
62
+ \`\`\`bash
63
+ npm install {SLUG}
64
+ \`\`\`
65
+
66
+ ## Basic usage
67
+
68
+ \`\`\`ts
69
+ import { create } from '{SLUG}'
70
+
71
+ const client = create({ apiKey: 'your-api-key' })
72
+ \`\`\`
73
+
74
+ That's it \u2014 you're ready to go!
75
+ `
76
+ };
77
+ function buildStarterDocsJson({
78
+ enableAiChat,
79
+ repoUrl,
80
+ i18nLocales
81
+ }) {
82
+ const config = {};
83
+ if (enableAiChat) {
84
+ config.ai = { chat: true };
85
+ }
86
+ if (repoUrl) {
87
+ config.navbar = {
88
+ links: [{ label: "GitHub", href: repoUrl, type: "github" }],
89
+ primary: { label: "Get started", href: "/quickstart" }
90
+ };
91
+ }
92
+ if (i18nLocales && i18nLocales.length > 0) {
93
+ config.i18n = {
94
+ defaultLocale: "en",
95
+ locales: [{ code: "en", label: "English" }, ...i18nLocales]
96
+ };
97
+ }
98
+ config.tabs = [
99
+ {
100
+ tab: "Overview",
101
+ groups: [{ group: "Getting Started", pages: ["introduction", "quickstart"] }]
102
+ },
103
+ { tab: "API Reference", api: { source: "openapi.yaml" } },
104
+ { tab: "Changelog", href: "/changelog" }
105
+ ];
106
+ return JSON.stringify(config, null, 2) + "\n";
107
+ }
108
+ function writeStarterContent(targetDir, projectName, slug, enableAiChat = true, repoUrl = "", i18nLocales) {
109
+ const contentDir = join(targetDir, "src", "content");
110
+ if (existsSync(contentDir)) {
111
+ const entries = readdirSync(contentDir);
112
+ for (const entry of entries) {
113
+ const fullPath = join(contentDir, entry);
114
+ execSync(`rm -rf "${fullPath}"`);
115
+ }
116
+ } else {
117
+ mkdirSync(contentDir, { recursive: true });
118
+ }
119
+ for (const [filename, template] of Object.entries(STARTER_PAGES)) {
120
+ const content = template.replace(/\{NAME\}/g, projectName).replace(/\{SLUG\}/g, slug);
121
+ writeFileSync(join(contentDir, filename), content, "utf8");
122
+ }
123
+ writeFileSync(
124
+ join(targetDir, "docs.json"),
125
+ buildStarterDocsJson({ enableAiChat, repoUrl: repoUrl || void 0, i18nLocales }),
126
+ "utf8"
127
+ );
128
+ }
129
+ function updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl) {
130
+ const siteFile = join(targetDir, "src", "data", "site.ts");
131
+ if (!existsSync(siteFile)) {
132
+ console.log(" \u26A0\uFE0F Could not find src/data/site.ts \u2014 skipping config update.");
133
+ return;
134
+ }
135
+ let source = readFileSync(siteFile, "utf8");
136
+ source = source.replace(
137
+ /name:\s*'[^']*'/,
138
+ `name: '${projectName.replace(/'/g, "\\'")}'`
139
+ );
140
+ source = source.replace(
141
+ /description:\s*\n\s*'[^']*'/,
142
+ `description:
143
+ '${description.replace(/'/g, "\\'")}'`
144
+ );
145
+ source = source.replace(
146
+ /const brandPreset:\s*BrandPresetKey\s*=\s*'[^']*'/,
147
+ `const brandPreset: BrandPresetKey = '${brandPreset}'`
148
+ );
149
+ if (repoUrl) {
150
+ source = source.replace(
151
+ /repoUrl:\s*'[^']*'/,
152
+ `repoUrl: '${repoUrl}'`
153
+ );
154
+ source = source.replace(
155
+ /\{\s*label:\s*'GitHub',\s*href:\s*'[^']*'\s*\}/,
156
+ `{ label: 'GitHub', href: '${repoUrl}' }`
157
+ );
158
+ source = source.replace(
159
+ /\{\s*label:\s*'Support',\s*href:\s*'[^']*'\s*\}/,
160
+ `{ label: 'Support', href: '${repoUrl}/issues/new' }`
161
+ );
162
+ }
163
+ writeFileSync(siteFile, source, "utf8");
164
+ }
165
+ function patchApiReferenceGuard(targetDir) {
166
+ const filePath = join(targetDir, "src", "data", "api-reference.ts");
167
+ if (!existsSync(filePath)) return;
168
+ let source = readFileSync(filePath, "utf8");
169
+ source = source.replace(
170
+ /export async function buildApiNavigation\([^)]*\)[^{]*\{\n/,
171
+ (match) => `${match} if (apiReferenceConfig.specs.length === 0) return []
172
+ `
173
+ );
174
+ writeFileSync(filePath, source, "utf8");
175
+ }
176
+ function patchTopBarNavigation(targetDir) {
177
+ const filePath = join(targetDir, "src", "components", "layout", "top-bar.tsx");
178
+ if (!existsSync(filePath)) return;
179
+ const source = readFileSync(filePath, "utf8");
180
+ if (!source.includes("target={isExternal ? '_blank' : undefined}")) return;
181
+ const patched = source.replace(
182
+ /if \(collection\.href\) \{\n const isExternal[^\n]+\n return \(\n <a[\s\S]*?<\/a>\n \)\n \}/,
183
+ `if (collection.href) {
184
+ const isExternal = /^https?:\\/\\//.test(collection.href)
185
+ if (isExternal) {
186
+ return (
187
+ <a
188
+ key={collection.id}
189
+ href={collection.href}
190
+ target="_blank"
191
+ rel="noreferrer"
192
+ className={baseClasses}
193
+ >
194
+ {collection.label}
195
+ </a>
196
+ )
197
+ }
198
+ return (
199
+ <Link
200
+ key={collection.id}
201
+ href={collection.href}
202
+ className={baseClasses}
203
+ >
204
+ {collection.label}
205
+ </Link>
206
+ )
207
+ }`
208
+ );
209
+ writeFileSync(filePath, patched, "utf8");
210
+ }
211
+ function patchOpenApiFetch(targetDir) {
212
+ const filePath = join(targetDir, "src", "lib", "openapi", "fetch.ts");
213
+ if (!existsSync(filePath)) return;
214
+ let source = readFileSync(filePath, "utf8");
215
+ source = source.replace(
216
+ /const absolutePath = path\.isAbsolute\(filePath\) \? filePath : path\.resolve\(process\.cwd\(\), filePath\)/,
217
+ `const absolutePath = filePath.startsWith('/')
218
+ ? path.resolve(process.cwd(), 'public', filePath.slice(1))
219
+ : path.resolve(process.cwd(), filePath)`
220
+ );
221
+ writeFileSync(filePath, source, "utf8");
222
+ }
223
+ function updateEnvExample(targetDir) {
224
+ const envFile = join(targetDir, ".env.example");
225
+ if (existsSync(envFile)) {
226
+ const envLocal = join(targetDir, ".env.local");
227
+ if (!existsSync(envLocal)) {
228
+ cpSync(envFile, envLocal);
229
+ }
230
+ }
231
+ }
232
+
233
+ // src/utils.ts
234
+ import { execSync as execSync2 } from "child_process";
235
+ import { basename } from "path";
236
+ function slugify(name) {
237
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
238
+ }
239
+ function run(cmd, cwd) {
240
+ execSync2(cmd, { cwd, stdio: "inherit" });
241
+ }
242
+ function initGit(targetDir) {
243
+ try {
244
+ run("git init", targetDir);
245
+ run("git add -A", targetDir);
246
+ run('git commit -m "Initial commit from create-dox"', targetDir);
247
+ } catch {
248
+ console.log(" \u26A0\uFE0F Could not initialize git (you can do this manually).");
249
+ }
250
+ }
251
+ function installDeps(targetDir) {
252
+ console.log("");
253
+ console.log(" \u{1F4E6} Installing dependencies...");
254
+ console.log("");
255
+ run("npm install", targetDir);
256
+ }
257
+ function logo() {
258
+ console.log("");
259
+ console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
260
+ console.log(" \u2551 \u2551");
261
+ console.log(" \u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2551");
262
+ console.log(" \u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D \u2551");
263
+ console.log(" \u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2554\u255D \u2551");
264
+ console.log(" \u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2588\u2588\u2557 \u2551");
265
+ console.log(" \u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u255D \u2588\u2588\u2557 \u2551");
266
+ console.log(" \u2551 \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u2551");
267
+ console.log(" \u2551 \u2551");
268
+ console.log(" \u2551 Beautiful docs, zero lock-in. \u2551");
269
+ console.log(" \u2551 \u2551");
270
+ console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
271
+ console.log("");
272
+ }
273
+ function success(projectDir, projectName) {
274
+ console.log("");
275
+ console.log(" \u2705 Your Dox project is ready!");
276
+ console.log("");
277
+ console.log(` \u{1F4C2} ${projectDir}`);
278
+ console.log("");
279
+ console.log(" Next steps:");
280
+ console.log("");
281
+ console.log(` cd ${basename(projectDir)}`);
282
+ console.log(" npm run dev");
283
+ console.log("");
284
+ console.log(` Then open http://localhost:3040 to see your ${projectName} docs.`);
285
+ console.log("");
286
+ console.log(" \u{1F4DD} Key files to edit:");
287
+ console.log(" \u2022 src/data/site.ts \u2014 name, links, branding");
288
+ console.log(" \u2022 docs.json \u2014 navigation structure");
289
+ console.log(" \u2022 src/content/*.mdx \u2014 your documentation");
290
+ console.log(" \u2022 openapi.yaml \u2014 API spec (optional)");
291
+ console.log("");
292
+ console.log(" Happy documenting! \u{1F680}");
293
+ console.log("");
294
+ }
295
+
296
+ // src/scaffold.ts
297
+ async function scaffold(options) {
298
+ const {
299
+ projectDir,
300
+ projectName,
301
+ description,
302
+ brandPreset,
303
+ repoUrl,
304
+ doInstall,
305
+ enableAiChat = true,
306
+ i18nLocales
307
+ } = options;
308
+ const targetDir = resolve(projectDir);
309
+ if (existsSync2(targetDir) && readdirSync2(targetDir).length > 0) {
310
+ throw new Error(`Directory "${targetDir}" already exists and is not empty.`);
311
+ }
312
+ mkdirSync2(targetDir, { recursive: true });
313
+ const slug = slugify(projectName);
314
+ await downloadTemplate(targetDir);
315
+ writeStarterContent(targetDir, projectName, slug, enableAiChat, repoUrl, i18nLocales);
316
+ updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl);
317
+ patchApiReferenceGuard(targetDir);
318
+ patchTopBarNavigation(targetDir);
319
+ patchOpenApiFetch(targetDir);
320
+ updateEnvExample(targetDir);
321
+ if (doInstall) {
322
+ installDeps(targetDir);
323
+ }
324
+ initGit(targetDir);
325
+ return { projectDir: targetDir };
326
+ }
327
+
328
+ export {
329
+ slugify,
330
+ initGit,
331
+ installDeps,
332
+ logo,
333
+ success,
334
+ scaffold
335
+ };