create-thally-docs 0.9.0 → 0.10.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,164 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ readStarterReleaseManifest
4
+ } from "./chunk-KXJT2MG5.js";
5
+ import {
6
+ STABLE_SCAFFOLD_RELEASE
7
+ } from "./chunk-WMHBVXWW.js";
8
+
9
+ // src/download.ts
10
+ import { Readable, Transform, pipeline } from "stream";
11
+ import { promisify } from "util";
12
+ import {
13
+ cpSync,
14
+ lstatSync,
15
+ mkdtempSync,
16
+ readdirSync,
17
+ renameSync,
18
+ rmdirSync,
19
+ rmSync
20
+ } from "fs";
21
+ import { tmpdir } from "os";
22
+ import { dirname, join } from "path";
23
+ import { extract } from "tar";
24
+ var pipelineAsync = promisify(pipeline);
25
+ var STARTER_FETCH_TIMEOUT_MS = 15e3;
26
+ var MAX_COMPRESSED_ARCHIVE_BYTES = 64 * 1024 * 1024;
27
+ var MAX_EXTRACTED_ARCHIVE_BYTES = 256 * 1024 * 1024;
28
+ var MAX_ARCHIVE_FILE_BYTES = 32 * 1024 * 1024;
29
+ var MAX_ARCHIVE_ENTRIES = 1e4;
30
+ var STARTER_REPOSITORY = STABLE_SCAFFOLD_RELEASE.source.repository;
31
+ var STARTER_COMMIT_SHA = STABLE_SCAFFOLD_RELEASE.source.commitSha;
32
+ var STARTER_ARCHIVE_ROOT = `${STARTER_REPOSITORY.split("/").at(-1)}-${STARTER_COMMIT_SHA}`;
33
+ function starterArchiveRoot(release) {
34
+ return `${release.source.repository.split("/").at(-1)}-${release.source.commitSha}`;
35
+ }
36
+ function validateStarterArchiveEntry(archivePath, entry, archiveRoot = STARTER_ARCHIVE_ROOT) {
37
+ if (!archivePath || archivePath.includes("\0") || archivePath.includes("\\") || archivePath.startsWith("/")) {
38
+ throw new Error("The stable Thally starter archive contains an unsafe path.");
39
+ }
40
+ const pathWithoutTrailingSlash = archivePath.endsWith("/") ? archivePath.slice(0, -1) : archivePath;
41
+ const parts = pathWithoutTrailingSlash.split("/");
42
+ const [root, ...relativeParts] = parts;
43
+ if (root !== archiveRoot || relativeParts.some((part) => !part || part === "." || part === "..")) {
44
+ throw new Error("The stable Thally starter archive contains an unsafe path.");
45
+ }
46
+ if (relativeParts.length === 0 && entry.type !== "Directory") {
47
+ throw new Error("The stable Thally starter archive has an invalid root entry.");
48
+ }
49
+ if (!["File", "OldFile", "Directory"].includes(entry.type)) {
50
+ throw new Error(
51
+ `The stable Thally starter archive contains unsupported entry type ${entry.type}.`
52
+ );
53
+ }
54
+ if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > MAX_ARCHIVE_FILE_BYTES) {
55
+ throw new Error("The stable Thally starter archive contains an oversized file.");
56
+ }
57
+ }
58
+ function starterArchiveFilter(archiveRoot) {
59
+ let entryCount = 0;
60
+ let extractedBytes = 0;
61
+ return (path, entry) => {
62
+ if (!("type" in entry)) {
63
+ throw new Error("The starter archive filter requires extraction metadata.");
64
+ }
65
+ validateStarterArchiveEntry(path, entry, archiveRoot);
66
+ entryCount += 1;
67
+ extractedBytes += entry.size;
68
+ if (entryCount > MAX_ARCHIVE_ENTRIES || extractedBytes > MAX_EXTRACTED_ARCHIVE_BYTES) {
69
+ throw new Error("The stable Thally starter archive exceeds extraction limits.");
70
+ }
71
+ return true;
72
+ };
73
+ }
74
+ function compressedArchiveLimit() {
75
+ let compressedBytes = 0;
76
+ return new Transform({
77
+ transform(chunk, _encoding, callback) {
78
+ compressedBytes += chunk.length;
79
+ if (compressedBytes > MAX_COMPRESSED_ARCHIVE_BYTES) {
80
+ callback(new Error("The stable Thally starter archive is too large."));
81
+ return;
82
+ }
83
+ callback(null, chunk);
84
+ }
85
+ });
86
+ }
87
+ async function downloadStarter(targetDir, siteName, release = STABLE_SCAFFOLD_RELEASE, options = {}) {
88
+ if (options.announce !== false) {
89
+ console.log("");
90
+ console.log(` \u23F3 Creating ${siteName?.trim() || "your docs site"}...`);
91
+ }
92
+ const targetEntry = lstatSync(targetDir);
93
+ if (!targetEntry.isDirectory() || targetEntry.isSymbolicLink()) {
94
+ throw new Error("The Thally starter target must be a regular directory.");
95
+ }
96
+ if (readdirSync(targetDir).length > 0) {
97
+ throw new Error("The Thally starter target directory must be empty.");
98
+ }
99
+ const response = await fetch(release.source.archiveUrl, {
100
+ headers: { accept: "application/gzip, application/octet-stream;q=0.9" },
101
+ cache: "no-store",
102
+ signal: AbortSignal.timeout(STARTER_FETCH_TIMEOUT_MS)
103
+ });
104
+ if (!response.ok) {
105
+ throw new Error(
106
+ `Failed to download the stable Thally starter: ${response.status} ${response.statusText}`
107
+ );
108
+ }
109
+ if (!response.body) {
110
+ throw new Error("The stable Thally starter response body is empty.");
111
+ }
112
+ const contentLength = Number(response.headers.get("content-length"));
113
+ if (Number.isFinite(contentLength) && contentLength > MAX_COMPRESSED_ARCHIVE_BYTES) {
114
+ throw new Error("The stable Thally starter archive is too large.");
115
+ }
116
+ const nodeStream = Readable.fromWeb(
117
+ response.body
118
+ );
119
+ const stagingDir = mkdtempSync(join(tmpdir(), "thally-starter-download-"));
120
+ try {
121
+ await pipelineAsync(
122
+ nodeStream,
123
+ compressedArchiveLimit(),
124
+ extract({
125
+ cwd: stagingDir,
126
+ strip: 1,
127
+ filter: starterArchiveFilter(starterArchiveRoot(release)),
128
+ preservePaths: false,
129
+ strict: true,
130
+ unlink: true,
131
+ maxDecompressionRatio: 100
132
+ })
133
+ );
134
+ readStarterReleaseManifest(stagingDir, release);
135
+ const deliveryDir = mkdtempSync(
136
+ join(dirname(targetDir), ".thally-starter-delivery-")
137
+ );
138
+ try {
139
+ for (const entry of readdirSync(stagingDir)) {
140
+ cpSync(join(stagingDir, entry), join(deliveryDir, entry), {
141
+ recursive: true,
142
+ errorOnExist: true
143
+ });
144
+ }
145
+ if (readdirSync(targetDir).length > 0) {
146
+ throw new Error("The Thally starter target changed during download.");
147
+ }
148
+ rmdirSync(targetDir);
149
+ renameSync(deliveryDir, targetDir);
150
+ } finally {
151
+ rmSync(deliveryDir, { recursive: true, force: true });
152
+ }
153
+ } finally {
154
+ rmSync(stagingDir, { recursive: true, force: true });
155
+ }
156
+ }
157
+
158
+ export {
159
+ STARTER_REPOSITORY,
160
+ STARTER_COMMIT_SHA,
161
+ STARTER_ARCHIVE_ROOT,
162
+ validateStarterArchiveEntry,
163
+ downloadStarter
164
+ };
@@ -3,7 +3,7 @@ import {
3
3
  initGit,
4
4
  installDeps,
5
5
  scaffold
6
- } from "./chunk-JNDRCCJV.js";
6
+ } from "./chunk-GTGHYJXS.js";
7
7
 
8
8
  // src/migrate/index.ts
9
9
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -1,83 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- patchApiReferenceGuard,
4
- patchGitignore,
5
- patchOpenApiFetch,
6
- patchPackageJson,
7
- patchTopBarNavigation,
8
- updateEnvExample,
9
- updateSiteConfig,
10
- writeCloudflareRuntimeConfig,
11
- writeStarterAgentGuide,
12
- writeStarterContent,
13
- writeStarterReadme
14
- } from "./chunk-BW7J7DJV.js";
3
+ personalizeStarter,
4
+ updateEnvExample
5
+ } from "./chunk-ORUAMPNF.js";
15
6
  import {
16
- STABLE_SCAFFOLD_RELEASE
17
- } from "./chunk-PJW4JJIT.js";
7
+ downloadStarter
8
+ } from "./chunk-ENQIF6MA.js";
18
9
 
19
10
  // src/scaffold.ts
20
11
  import { existsSync, mkdirSync, readdirSync } from "fs";
21
12
  import { resolve } from "path";
22
13
 
23
- // src/download.ts
24
- import { Readable, pipeline } from "stream";
25
- import { promisify } from "util";
26
- import { extract } from "tar";
27
- var pipelineAsync = promisify(pipeline);
28
- var TEMPLATE_REPOSITORY = STABLE_SCAFFOLD_RELEASE.source.repository;
29
- var TEMPLATE_COMMIT_SHA = STABLE_SCAFFOLD_RELEASE.source.commitSha;
30
- var TARBALL_URL = STABLE_SCAFFOLD_RELEASE.source.archiveUrl;
31
- var EXCLUDE_PATHS = [
32
- // Match both the directory entry itself and every nested file. Tar invokes
33
- // the filter for `.../node_modules` before descendants, without a trailing `/`.
34
- "/node_modules",
35
- // The canonical docs repository may temporarily retain package sources while
36
- // runtime work is being upstreamed. A scaffold consumes the published
37
- // packages declared in package.json; it must never inherit those sources.
38
- "/packages",
39
- "/.git/",
40
- "/.next/",
41
- "/.data/",
42
- "/.thally/",
43
- // The first-party docs deployment pins its own canonical origin.
44
- "/.env.production",
45
- "/thally-track.yml",
46
- "/CODEOWNERS",
47
- "/CLAUDE.md",
48
- "/notes/",
49
- "/public/images/",
50
- "/src/public/",
51
- "/snippets/",
52
- "/.github/ISSUE_TEMPLATE/",
53
- "/.github/PULL_REQUEST_TEMPLATE.md",
54
- "/README.md"
55
- ];
56
- function shouldInclude(path) {
57
- for (const excluded of EXCLUDE_PATHS) {
58
- if (path.includes(excluded)) {
59
- return false;
60
- }
61
- }
62
- return true;
63
- }
64
- async function downloadTemplate(targetDir, siteName) {
65
- console.log("");
66
- console.log(` \u23F3 Creating ${siteName?.trim() || "your docs site"}...`);
67
- const response = await fetch(TARBALL_URL);
68
- if (!response.ok) {
69
- throw new Error(`Failed to download template: ${response.status} ${response.statusText}`);
70
- }
71
- if (!response.body) {
72
- throw new Error("Response body is empty");
73
- }
74
- const nodeStream = Readable.fromWeb(response.body);
75
- await pipelineAsync(
76
- nodeStream,
77
- extract({ cwd: targetDir, strip: 1, filter: shouldInclude })
78
- );
79
- }
80
-
81
14
  // src/docs-json.ts
82
15
  import { readFileSync, writeFileSync } from "fs";
83
16
  import { join } from "path";
@@ -189,7 +122,16 @@ async function scaffold(options) {
189
122
  }
190
123
  mkdirSync(targetDir, { recursive: true });
191
124
  const slug = slugify(projectName);
192
- await downloadTemplate(targetDir, projectName);
125
+ await downloadStarter(targetDir, projectName);
126
+ personalizeStarter(targetDir, {
127
+ projectName,
128
+ packageName: slug,
129
+ description,
130
+ brandPreset,
131
+ repoUrl,
132
+ enableAiChat,
133
+ i18nLocales
134
+ });
193
135
  resetTrackingConfig(targetDir);
194
136
  if (trackRepos?.length) {
195
137
  writeTrackingConfig(targetDir, trackRepos);
@@ -198,16 +140,6 @@ async function scaffold(options) {
198
140
  console.log(" To finish wiring it: `thally track setup` (pick a trigger) + `thally agent init`,");
199
141
  console.log(" then add your ANTHROPIC_API_KEY. See /guides/thally-track.");
200
142
  }
201
- writeStarterContent(targetDir, projectName, enableAiChat, repoUrl, i18nLocales);
202
- writeStarterReadme(targetDir, projectName);
203
- writeStarterAgentGuide(targetDir, projectName);
204
- updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl);
205
- patchApiReferenceGuard(targetDir);
206
- patchTopBarNavigation(targetDir);
207
- patchOpenApiFetch(targetDir);
208
- patchPackageJson(targetDir, slug);
209
- writeCloudflareRuntimeConfig(targetDir, slug);
210
- patchGitignore(targetDir);
211
143
  updateEnvExample(targetDir);
212
144
  if (doInstall) {
213
145
  installDeps(targetDir);
@@ -222,10 +154,6 @@ export {
222
154
  installDeps,
223
155
  logo,
224
156
  success,
225
- TEMPLATE_REPOSITORY,
226
- TEMPLATE_COMMIT_SHA,
227
- EXCLUDE_PATHS,
228
- shouldInclude,
229
157
  readDocsJson,
230
158
  writeDocsJson,
231
159
  scaffold