create-thally-docs 0.7.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,435 @@
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/thallylabs/thally/tar.gz/main";
13
+ var EXCLUDE_PATHS = [
14
+ "/cli/",
15
+ "/packages/",
16
+ "/node_modules/",
17
+ "/.git/",
18
+ "/thally-agent.yml",
19
+ "/thally-track.yml",
20
+ "/CODEOWNERS"
21
+ ];
22
+ function shouldInclude(path) {
23
+ for (const excluded of EXCLUDE_PATHS) {
24
+ if (path.includes(excluded)) {
25
+ return false;
26
+ }
27
+ }
28
+ return true;
29
+ }
30
+ async function downloadTemplate(targetDir, siteName) {
31
+ console.log("");
32
+ console.log(` \u23F3 Creating ${siteName?.trim() || "your docs site"}...`);
33
+ const response = await fetch(TARBALL_URL);
34
+ if (!response.ok) {
35
+ throw new Error(`Failed to download template: ${response.status} ${response.statusText}`);
36
+ }
37
+ if (!response.body) {
38
+ throw new Error("Response body is empty");
39
+ }
40
+ const nodeStream = Readable.fromWeb(response.body);
41
+ await pipelineAsync(
42
+ nodeStream,
43
+ tar.extract({ cwd: targetDir, strip: 1, filter: shouldInclude })
44
+ );
45
+ }
46
+
47
+ // src/docs-json.ts
48
+ import { readFileSync, writeFileSync } from "fs";
49
+ import { join } from "path";
50
+ function readDocsJson(projectDir) {
51
+ const docsPath = join(projectDir, "docs.json");
52
+ const raw = readFileSync(docsPath, "utf8");
53
+ return JSON.parse(raw);
54
+ }
55
+ function writeDocsJson(projectDir, config) {
56
+ const docsPath = join(projectDir, "docs.json");
57
+ writeFileSync(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
58
+ }
59
+ function resetTrackingConfig(projectDir) {
60
+ const config = readDocsJson(projectDir);
61
+ if (config.tracking) {
62
+ delete config.tracking;
63
+ writeDocsJson(projectDir, config);
64
+ }
65
+ }
66
+ function writeTrackingConfig(projectDir, repos) {
67
+ if (repos.length === 0) return;
68
+ const config = readDocsJson(projectDir);
69
+ config.tracking = { repos: repos.map((r) => ({ owner: r.owner, repo: r.repo, branch: "main" })) };
70
+ writeDocsJson(projectDir, config);
71
+ }
72
+
73
+ // src/customize.ts
74
+ import { existsSync, mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync, cpSync } from "fs";
75
+ import { join as join2 } from "path";
76
+ import { execSync } from "child_process";
77
+ var STARTER_PAGES = {
78
+ "introduction.mdx": `---
79
+ title: Introduction
80
+ description: Welcome to {NAME} \u2014 learn what it does, how the docs are organized, and where to start.
81
+ keywords:
82
+ - {NAME}
83
+ - documentation
84
+ - overview
85
+ - getting started
86
+ ---
87
+
88
+ ## Welcome
89
+
90
+ Welcome to the **{NAME}** documentation. This is your home base for guides, API
91
+ references, and everything you need to build with {NAME}. The site is powered by
92
+ [Thally](https://github.com/thallylabs/thally), an agent-native docs platform \u2014 every page
93
+ is served to humans as polished HTML and to AI agents as structured JSON, JSON-LD,
94
+ and Markdown from the same URL, so assistants can read your docs accurately.
95
+
96
+ ## What you'll find here
97
+
98
+ - **Guides** \u2014 step-by-step walkthroughs of common tasks and workflows.
99
+ - **API reference** \u2014 generated from your OpenAPI spec, with a live "Try It" console.
100
+ - **Quickstart** \u2014 install {NAME} and make your first call in a few minutes.
101
+
102
+ ## Next steps
103
+
104
+ Start with the [Quickstart](/quickstart) to get {NAME} running, then make this site
105
+ your own by editing \`src/content/introduction.mdx\` and updating the navigation in
106
+ \`docs.json\`. Every change you save is instantly reflected for both readers and agents.
107
+ `,
108
+ "quickstart.mdx": `---
109
+ title: Quickstart
110
+ description: Install {NAME}, configure your API key, and make your first call in under five minutes.
111
+ keywords:
112
+ - {NAME}
113
+ - quickstart
114
+ - installation
115
+ - getting started
116
+ ---
117
+
118
+ ## Installation
119
+
120
+ Install {NAME} with your package manager of choice. We recommend pinning the
121
+ version in your project so builds stay reproducible across machines and CI:
122
+
123
+ \`\`\`bash
124
+ npm install {SLUG}
125
+ \`\`\`
126
+
127
+ ## Basic usage
128
+
129
+ Import the client and initialize it with your API key. Keep the key in an
130
+ environment variable rather than committing it to source control, so it never
131
+ leaks into your repository or build logs:
132
+
133
+ \`\`\`ts
134
+ import { create } from '{SLUG}'
135
+
136
+ const client = create({ apiKey: process.env.API_KEY })
137
+ \`\`\`
138
+
139
+ ## What's next
140
+
141
+ That's the basics \u2014 you're ready to build. Explore the guides for common workflows,
142
+ open the API reference to try endpoints against a live "Try It" console, or edit this
143
+ page at \`src/content/quickstart.mdx\` to document your own onboarding flow.
144
+ `,
145
+ "changelog.mdx": `---
146
+ title: Changelog
147
+ description: Notable changes, releases, and improvements to {NAME}.
148
+ keywords:
149
+ - {NAME}
150
+ - changelog
151
+ - releases
152
+ - updates
153
+ ---
154
+
155
+ ## v0.1.0
156
+
157
+ The first release of your **{NAME}** documentation.
158
+
159
+ - Initial docs site scaffolded with [Thally](https://github.com/thallylabs/thally)
160
+ - Agent-ready endpoints live: \`/llms.txt\`, \`/ai.txt\`, \`/api/docs-index\`, and \`/api/agent-readiness\`
161
+ - Starter guides in the Overview tab and an interactive API reference
162
+
163
+ Edit this page at \`src/content/changelog.mdx\` to announce your own releases as you ship.
164
+ `
165
+ };
166
+ function buildStarterDocsJson({
167
+ enableAiChat,
168
+ repoUrl,
169
+ i18nLocales
170
+ }) {
171
+ const config = {};
172
+ config.theme = "sharp";
173
+ config.fonts = {
174
+ body: { family: "Plus Jakarta Sans", weight: ["400", "500", "600", "700"] },
175
+ heading: { family: "Outfit", weight: ["600", "700"] }
176
+ };
177
+ if (enableAiChat) {
178
+ config.ai = { chat: true };
179
+ }
180
+ if (repoUrl) {
181
+ config.navbar = {
182
+ links: [{ label: "GitHub", href: repoUrl, type: "github" }],
183
+ primary: { label: "Get started", href: "/quickstart" }
184
+ };
185
+ }
186
+ if (i18nLocales && i18nLocales.length > 0) {
187
+ config.i18n = {
188
+ defaultLocale: "en",
189
+ locales: [{ code: "en", label: "English" }, ...i18nLocales]
190
+ };
191
+ }
192
+ config.tabs = [
193
+ {
194
+ tab: "Overview",
195
+ groups: [{ group: "Getting Started", pages: ["introduction", "quickstart"] }]
196
+ },
197
+ { tab: "API Reference", api: { source: "openapi.yaml" } },
198
+ { tab: "Changelog", href: "/changelog" }
199
+ ];
200
+ return JSON.stringify(config, null, 2) + "\n";
201
+ }
202
+ function writeStarterContent(targetDir, projectName, slug, enableAiChat = true, repoUrl = "", i18nLocales) {
203
+ const contentDir = join2(targetDir, "src", "content");
204
+ if (existsSync(contentDir)) {
205
+ const entries = readdirSync(contentDir);
206
+ for (const entry of entries) {
207
+ const fullPath = join2(contentDir, entry);
208
+ execSync(`rm -rf "${fullPath}"`);
209
+ }
210
+ } else {
211
+ mkdirSync(contentDir, { recursive: true });
212
+ }
213
+ for (const [filename, template] of Object.entries(STARTER_PAGES)) {
214
+ const content = template.replace(/\{NAME\}/g, projectName).replace(/\{SLUG\}/g, slug);
215
+ writeFileSync2(join2(contentDir, filename), content, "utf8");
216
+ }
217
+ writeFileSync2(
218
+ join2(targetDir, "docs.json"),
219
+ buildStarterDocsJson({ enableAiChat, repoUrl: repoUrl || void 0, i18nLocales }),
220
+ "utf8"
221
+ );
222
+ }
223
+ function updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl) {
224
+ const siteFile = join2(targetDir, "src", "data", "site.ts");
225
+ if (!existsSync(siteFile)) {
226
+ console.log(" \u26A0\uFE0F Could not find src/data/site.ts \u2014 skipping config update.");
227
+ return;
228
+ }
229
+ let source = readFileSync2(siteFile, "utf8");
230
+ source = source.replace(
231
+ /name:\s*'[^']*'/,
232
+ `name: '${projectName.replace(/'/g, "\\'")}'`
233
+ );
234
+ source = source.replace(
235
+ /description:\s*\n\s*'[^']*'/,
236
+ `description:
237
+ '${description.replace(/'/g, "\\'")}'`
238
+ );
239
+ source = source.replace(
240
+ /const brandPreset:\s*BrandPresetKey\s*=\s*'[^']*'/,
241
+ `const brandPreset: BrandPresetKey = '${brandPreset}'`
242
+ );
243
+ source = source.replace(/repoUrl:\s*'[^']*'/, `repoUrl: '${repoUrl}'`);
244
+ source = source.replace(
245
+ /\{\s*label:\s*'GitHub',\s*href:\s*'[^']*'\s*\}/,
246
+ `{ label: 'GitHub', href: '${repoUrl}' }`
247
+ );
248
+ source = source.replace(
249
+ /\{\s*label:\s*'Support',\s*href:\s*'[^']*'\s*\}/,
250
+ `{ label: 'Support', href: '${repoUrl ? `${repoUrl}/issues/new` : ""}' }`
251
+ );
252
+ writeFileSync2(siteFile, source, "utf8");
253
+ }
254
+ function patchApiReferenceGuard(targetDir) {
255
+ const filePath = join2(targetDir, "src", "data", "api-reference.ts");
256
+ if (!existsSync(filePath)) return;
257
+ let source = readFileSync2(filePath, "utf8");
258
+ source = source.replace(
259
+ /export async function buildApiNavigation\([^)]*\)[^{]*\{\n/,
260
+ (match) => `${match} if (apiReferenceConfig.specs.length === 0) return []
261
+ `
262
+ );
263
+ writeFileSync2(filePath, source, "utf8");
264
+ }
265
+ function patchTopBarNavigation(targetDir) {
266
+ const filePath = join2(targetDir, "src", "components", "layout", "top-bar.tsx");
267
+ if (!existsSync(filePath)) return;
268
+ const source = readFileSync2(filePath, "utf8");
269
+ if (!source.includes("target={isExternal ? '_blank' : undefined}")) return;
270
+ const patched = source.replace(
271
+ /if \(collection\.href\) \{\n const isExternal[^\n]+\n return \(\n <a[\s\S]*?<\/a>\n \)\n \}/,
272
+ `if (collection.href) {
273
+ const isExternal = /^https?:\\/\\//.test(collection.href)
274
+ if (isExternal) {
275
+ return (
276
+ <a
277
+ key={collection.id}
278
+ href={collection.href}
279
+ target="_blank"
280
+ rel="noreferrer"
281
+ className={baseClasses}
282
+ >
283
+ {collection.label}
284
+ </a>
285
+ )
286
+ }
287
+ return (
288
+ <Link
289
+ key={collection.id}
290
+ href={collection.href}
291
+ className={baseClasses}
292
+ >
293
+ {collection.label}
294
+ </Link>
295
+ )
296
+ }`
297
+ );
298
+ writeFileSync2(filePath, patched, "utf8");
299
+ }
300
+ function patchOpenApiFetch(targetDir) {
301
+ const filePath = join2(targetDir, "src", "lib", "openapi", "fetch.ts");
302
+ if (!existsSync(filePath)) return;
303
+ let source = readFileSync2(filePath, "utf8");
304
+ source = source.replace(
305
+ /const absolutePath = path\.isAbsolute\(filePath\) \? filePath : path\.resolve\(process\.cwd\(\), filePath\)/,
306
+ `const absolutePath = filePath.startsWith('/')
307
+ ? path.resolve(process.cwd(), 'public', filePath.slice(1))
308
+ : path.resolve(process.cwd(), filePath)`
309
+ );
310
+ writeFileSync2(filePath, source, "utf8");
311
+ }
312
+ function updateEnvExample(targetDir) {
313
+ const envFile = join2(targetDir, ".env.example");
314
+ if (existsSync(envFile)) {
315
+ const envLocal = join2(targetDir, ".env.local");
316
+ if (!existsSync(envLocal)) {
317
+ cpSync(envFile, envLocal);
318
+ }
319
+ }
320
+ }
321
+
322
+ // src/utils.ts
323
+ import { execSync as execSync2 } from "child_process";
324
+ import { basename } from "path";
325
+ function slugify(name) {
326
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
327
+ }
328
+ function run(cmd, cwd) {
329
+ execSync2(cmd, { cwd, stdio: "inherit" });
330
+ }
331
+ function initGit(targetDir) {
332
+ try {
333
+ run("git init", targetDir);
334
+ run("git add -A", targetDir);
335
+ run('git commit -m "Initial commit from create-thally-docs"', targetDir);
336
+ } catch {
337
+ console.log(" \u26A0\uFE0F Could not initialize git (you can do this manually).");
338
+ }
339
+ }
340
+ function installDeps(targetDir) {
341
+ console.log("");
342
+ console.log(" \u{1F4E6} Installing dependencies...");
343
+ console.log("");
344
+ run("npm install", targetDir);
345
+ }
346
+ function logo() {
347
+ console.log("");
348
+ 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\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
349
+ console.log(" \u2551 \u2551");
350
+ console.log(" \u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2551");
351
+ console.log(" \u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D \u2551");
352
+ console.log(" \u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2551");
353
+ console.log(" \u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D \u2551");
354
+ console.log(" \u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2551");
355
+ console.log(" \u2551 \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u2551");
356
+ console.log(" \u2551 \u2551");
357
+ console.log(" \u2551 Beautiful docs, zero lock-in. \u2551");
358
+ console.log(" \u2551 \u2551");
359
+ 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\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
360
+ console.log("");
361
+ }
362
+ function success(projectDir, projectName) {
363
+ console.log("");
364
+ console.log(" \u2705 Your Thally project is ready!");
365
+ console.log("");
366
+ console.log(` \u{1F4C2} ${projectDir}`);
367
+ console.log("");
368
+ console.log(" Next steps:");
369
+ console.log("");
370
+ console.log(` cd ${basename(projectDir)}`);
371
+ console.log(" npm run dev");
372
+ console.log("");
373
+ console.log(` Then open http://localhost:3040 to see your ${projectName} docs.`);
374
+ console.log("");
375
+ console.log(" \u{1F4DD} Key files to edit:");
376
+ console.log(" \u2022 src/data/site.ts \u2014 name, links, branding");
377
+ console.log(" \u2022 docs.json \u2014 navigation structure");
378
+ console.log(" \u2022 src/content/*.mdx \u2014 your documentation");
379
+ console.log(" \u2022 openapi.yaml \u2014 API spec (optional)");
380
+ console.log("");
381
+ console.log(" Happy documenting! \u{1F680}");
382
+ console.log("");
383
+ }
384
+
385
+ // src/scaffold.ts
386
+ async function scaffold(options) {
387
+ const {
388
+ projectDir,
389
+ projectName,
390
+ description,
391
+ brandPreset,
392
+ repoUrl,
393
+ doInstall,
394
+ enableAiChat = true,
395
+ i18nLocales,
396
+ trackRepos
397
+ } = options;
398
+ const targetDir = resolve(projectDir);
399
+ if (existsSync2(targetDir) && readdirSync2(targetDir).length > 0) {
400
+ throw new Error(`Directory "${targetDir}" already exists and is not empty.`);
401
+ }
402
+ mkdirSync2(targetDir, { recursive: true });
403
+ const slug = slugify(projectName);
404
+ await downloadTemplate(targetDir, projectName);
405
+ resetTrackingConfig(targetDir);
406
+ if (trackRepos?.length) {
407
+ writeTrackingConfig(targetDir, trackRepos);
408
+ const list = trackRepos.map((r) => `${r.owner}/${r.repo}`).join(", ");
409
+ console.log(` \u2713 Thally Track enabled \u2014 watching ${list} (branch main, all files; refine in docs.json).`);
410
+ console.log(" To finish wiring it: `thally track setup` (pick a trigger) + `thally agent init`,");
411
+ console.log(" then add your ANTHROPIC_API_KEY. See /guides/thally-track.");
412
+ }
413
+ writeStarterContent(targetDir, projectName, slug, enableAiChat, repoUrl, i18nLocales);
414
+ updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl);
415
+ patchApiReferenceGuard(targetDir);
416
+ patchTopBarNavigation(targetDir);
417
+ patchOpenApiFetch(targetDir);
418
+ updateEnvExample(targetDir);
419
+ if (doInstall) {
420
+ installDeps(targetDir);
421
+ }
422
+ initGit(targetDir);
423
+ return { projectDir: targetDir };
424
+ }
425
+
426
+ export {
427
+ slugify,
428
+ initGit,
429
+ installDeps,
430
+ logo,
431
+ success,
432
+ readDocsJson,
433
+ writeDocsJson,
434
+ scaffold
435
+ };