create-thally-docs 0.10.78 → 0.10.79

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,324 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findPublicSpecs,
4
+ shadowNote
5
+ } from "./chunk-KDLP27DS.js";
6
+ import {
7
+ initGit,
8
+ installDeps,
9
+ runProjectCommand,
10
+ scaffold
11
+ } from "./chunk-KGKSB3PU.js";
12
+ import {
13
+ pruneMissingSiteLinks
14
+ } from "./chunk-4VKRBJV2.js";
15
+ import {
16
+ terminal
17
+ } from "./chunk-L2YCVQBR.js";
18
+
19
+ // src/migrate/index.ts
20
+ import { existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
21
+ import { tmpdir } from "os";
22
+ import { setImmediate } from "timers/promises";
23
+ import { basename, dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
24
+ import {
25
+ cloneGitHubRepository,
26
+ hydrateRemoteApiSpecs,
27
+ migrateUrl,
28
+ parseGitHubRepositoryUrl
29
+ } from "@thallylabs/migrate";
30
+
31
+ // src/migration-work.ts
32
+ import { Worker } from "worker_threads";
33
+ async function runMigrationWork(request, options) {
34
+ const worker = new Worker(options.workerUrl ?? new URL("./migration-worker.js", import.meta.url), {
35
+ workerData: request,
36
+ stdout: true,
37
+ stderr: true
38
+ });
39
+ worker.stdout.resume();
40
+ worker.stderr.resume();
41
+ try {
42
+ return await new Promise((complete, reject) => {
43
+ worker.once("message", (response) => {
44
+ if (response.error) reject(response.error);
45
+ else complete(response.result);
46
+ });
47
+ worker.once("error", reject);
48
+ worker.once("exit", (code) => reject(new Error(`Migration worker exited before returning a result (exit ${code}).`)));
49
+ });
50
+ } finally {
51
+ await worker.terminate();
52
+ }
53
+ }
54
+ function convertRepository(options, workerOptions = {}) {
55
+ return runMigrationWork({ operation: "repository", args: [options] }, workerOptions);
56
+ }
57
+ function renderFiles(bundle, options, workerOptions = {}) {
58
+ return runMigrationWork({ operation: "render", args: [bundle, options] }, workerOptions);
59
+ }
60
+ function discoverUrl(options, workerOptions = {}) {
61
+ return runMigrationWork({ operation: "url", args: [options] }, workerOptions);
62
+ }
63
+ function checkContent(projectDir, workerOptions = {}) {
64
+ return runMigrationWork({ operation: "check", args: [projectDir] }, workerOptions);
65
+ }
66
+
67
+ // src/migrate/validate.ts
68
+ import { existsSync, readFileSync } from "fs";
69
+ import { join } from "path";
70
+ async function validateMigration(projectDir, skip = false, installationFailed = false) {
71
+ const result = { content: "skipped", build: "skipped", messages: [], diagnostics: [] };
72
+ if (skip) {
73
+ result.messages.push("Validation was explicitly skipped; this import is not verified.");
74
+ return result;
75
+ }
76
+ try {
77
+ const isQuiet = terminal.isRich && !terminal.isVerbose;
78
+ await terminal.step("Validate content", async () => {
79
+ const checked = await checkContent(projectDir);
80
+ const code = checked.code;
81
+ result.diagnostics = checked.diagnostics;
82
+ if (!isQuiet) {
83
+ for (const issue of checked.diagnostics) {
84
+ const location = issue.file ? `file=${issue.file}${issue.line ? `,line=${issue.line}` : ""}` : "";
85
+ console.log(`::${issue.severity} ${location}::${issue.message}`);
86
+ }
87
+ const errors = checked.diagnostics.filter((issue) => issue.severity === "error").length;
88
+ const warnings = checked.diagnostics.length - errors;
89
+ console.log(`
90
+ thally check: ${errors} error(s), ${warnings} warning(s)`);
91
+ }
92
+ if (isQuiet) {
93
+ for (const issue of result.diagnostics) {
94
+ result.messages.push(`${issue.file ? `${issue.file}${issue.line ? `:${issue.line}` : ""}: ` : ""}${issue.message}`);
95
+ }
96
+ }
97
+ if (code !== 0) throw new Error("Review the content diagnostics; source links are not silently rewritten.");
98
+ }, "Content validated");
99
+ result.content = "passed";
100
+ } catch (error) {
101
+ if ([130, 143].includes(error.exitCode ?? 0)) throw error;
102
+ result.content = "failed";
103
+ result.messages.push(`Content validation failed: ${error instanceof Error ? error.message : String(error)}`);
104
+ }
105
+ if (installationFailed) {
106
+ result.build = "failed";
107
+ result.messages.push("Dependency installation failed; production build was not attempted. Review the installation output before retrying.");
108
+ return result;
109
+ }
110
+ const packagePath = join(projectDir, "package.json");
111
+ try {
112
+ const manifest = existsSync(packagePath) ? JSON.parse(readFileSync(packagePath, "utf8")) : void 0;
113
+ if (!manifest?.scripts?.build) {
114
+ result.messages.push("No build script is available; production rendering is not verified.");
115
+ return result;
116
+ }
117
+ try {
118
+ await terminal.step("Validate production build", (update) => runProjectCommand("npm", ["run", "build"], projectDir, 10 * 60 * 1e3, (message) => update(`Production build: ${message}`)), "Production build validated");
119
+ result.build = "passed";
120
+ } catch (error) {
121
+ const failure = error;
122
+ if (failure.output?.trim()) terminal.error(failure.output.trimEnd());
123
+ throw error;
124
+ }
125
+ } catch (error) {
126
+ if ([130, 143].includes(error.exitCode ?? 0)) throw error;
127
+ result.build = "failed";
128
+ result.messages.push(`Production build failed: ${error instanceof Error ? error.message : String(error)}`);
129
+ }
130
+ return result;
131
+ }
132
+
133
+ // src/migrate/index.ts
134
+ function projectPath(projectDir, candidate) {
135
+ const target = resolve(projectDir, candidate);
136
+ const fromRoot = relative(resolve(projectDir), target);
137
+ if (isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${sep}`)) {
138
+ throw new Error(`Generated migration path escapes the project: ${candidate}`);
139
+ }
140
+ return target;
141
+ }
142
+ function ignoreQuarantineDirectory(projectDir) {
143
+ const ignorePath = projectPath(projectDir, ".gitignore");
144
+ const existing = existsSync2(ignorePath) ? readFileSync2(ignorePath, "utf8") : "";
145
+ const rules = existing.split(/\r?\n/).map((line) => line.trim()).filter((line) => /^!?\/?migration-quarantine(?:\/(?:\*\*?)?)?$/.test(line));
146
+ if (rules.length > 0 && !rules[rules.length - 1].startsWith("!")) return;
147
+ const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
148
+ writeFileSync(ignorePath, `${existing}${separator}
149
+ # Access-restricted pages withheld by migration: local only, never commit or deploy
150
+ /migration-quarantine/
151
+ `);
152
+ }
153
+ function readExistingConfig(projectDir) {
154
+ const configPath = projectPath(projectDir, "docs.json");
155
+ if (!existsSync2(configPath)) return void 0;
156
+ return JSON.parse(readFileSync2(configPath, "utf8"));
157
+ }
158
+ function resetFreshMigrationContent(projectDir) {
159
+ const contentDirectory = projectPath(projectDir, "src/content");
160
+ rmSync(contentDirectory, { recursive: true, force: true });
161
+ mkdirSync(contentDirectory, { recursive: true });
162
+ for (const sampleSpec of ["openapi.yaml", "openapi.json"]) {
163
+ rmSync(projectPath(projectDir, sampleSpec), { force: true });
164
+ }
165
+ }
166
+ async function discoverMigration(options) {
167
+ const url = new URL(options.sourceUrl);
168
+ if (url.hostname.toLowerCase() !== "github.com") {
169
+ return terminal.step("Discovering public documentation", () => {
170
+ const discovery = {
171
+ sourceUrl: options.sourceUrl,
172
+ platform: options.platform,
173
+ maxPages: options.maxPages
174
+ };
175
+ return options.fetcher ? migrateUrl({ ...discovery, fetcher: options.fetcher }) : discoverUrl(discovery);
176
+ }, "Public documentation discovered");
177
+ }
178
+ const source = parseGitHubRepositoryUrl(options.sourceUrl);
179
+ if (options.branch) source.branch = options.branch;
180
+ const temporaryRoot = mkdtempSync(join2(tmpdir(), "thally-migrate-"));
181
+ const cloneDir = join2(temporaryRoot, "repository");
182
+ try {
183
+ const cloneWarnings = [];
184
+ await terminal.step(`Cloning ${source.owner}/${source.repo}`, () => cloneGitHubRepository(source, cloneDir, cloneWarnings), "Source repository cloned");
185
+ const bundle = await terminal.step("Converting repository documentation", () => convertRepository({
186
+ repositoryDir: cloneDir,
187
+ sourceUrl: options.sourceUrl,
188
+ docsDir: options.docsDir ?? (source.docsDir || void 0),
189
+ platform: options.platform
190
+ }), "Repository documentation converted");
191
+ const hydrated = await terminal.step("Loading remote API specifications", () => hydrateRemoteApiSpecs(bundle, options.fetcher), "Remote API specifications loaded");
192
+ return cloneWarnings.length > 0 ? { ...hydrated, warnings: [...cloneWarnings, ...hydrated.warnings] } : hydrated;
193
+ } finally {
194
+ rmSync(temporaryRoot, { recursive: true, force: true });
195
+ }
196
+ }
197
+ function publicSpecWarning(projectDir, bundle) {
198
+ const { specs, skipped } = findPublicSpecs(projectDir);
199
+ if (specs.length === 0 && skipped.length === 0) return null;
200
+ const migrated = new Set(bundle.assets.filter((asset) => asset.projectRelative).map((asset) => basename(asset.path)));
201
+ const lines = [
202
+ ...specs.map((spec) => `${spec.path}${migrated.has(basename(spec.path)) ? " (same file name as a migrated spec)" : ""}${spec.hasHiddenOperations ? " (contains x-excluded/x-hidden operations)" : ""}${shadowNote(spec.urlPath) ? ` (answers /${spec.urlPath} in place of the filtered specification)` : ""}`),
203
+ ...skipped.map((path) => `${path} (too large to inspect)`)
204
+ ];
205
+ return {
206
+ code: "unsupported-config",
207
+ message: `Existing OpenAPI files under public/ are served publicly as-is: ${lines.join("; ")}. Migrated specs are written to openapi/, and re-running migration does not remove old copies from public/. Delete any old copy manually (nothing was deleted), or its hidden operations stay downloadable.`
208
+ };
209
+ }
210
+ async function migrateDocs(options) {
211
+ const projectDir = resolve(options.projectDir);
212
+ const bundle = await discoverMigration(options);
213
+ if (!options.into) {
214
+ terminal.detail("Project", projectDir);
215
+ await scaffold({
216
+ projectDir,
217
+ projectName: options.projectName ?? bundle.site?.name ?? "My Docs",
218
+ description: bundle.site?.description ?? `Documentation migrated from ${new URL(options.sourceUrl).hostname}`,
219
+ brandPreset: "primary",
220
+ // Source provenance is not the destination repository. Migrated pages
221
+ // live at new paths, so source URLs cannot power edit/issue actions.
222
+ repoUrl: "",
223
+ doInstall: false,
224
+ colors: bundle.site?.colors
225
+ });
226
+ resetFreshMigrationContent(projectDir);
227
+ pruneMissingSiteLinks(projectDir, new Set(bundle.pages.map((page) => page.id)));
228
+ } else if (!existsSync2(projectDir)) {
229
+ throw new Error(`Project directory "${projectDir}" does not exist. Use without --into to scaffold a new one.`);
230
+ }
231
+ if (!options.into) {
232
+ const starterConfig = readExistingConfig(projectDir);
233
+ if (starterConfig?.markdown) bundle.docsConfig.markdown ??= starterConfig.markdown;
234
+ if (!bundle.docsConfig.navbar?.primary) {
235
+ bundle.docsConfig.navbar = { ...bundle.docsConfig.navbar, primary: null };
236
+ }
237
+ bundle.docsConfig.i18n ??= { defaultLocale: "en", locales: [{ code: "en", label: "English" }] };
238
+ }
239
+ if (options.into) {
240
+ const stale = publicSpecWarning(projectDir, bundle);
241
+ if (stale) bundle.warnings.unshift(stale);
242
+ }
243
+ const rendered = await terminal.step("Rendering documentation", () => renderFiles(bundle, {
244
+ existingConfig: options.into ? readExistingConfig(projectDir) : void 0,
245
+ existingComponentRegistry: existsSync2(projectPath(projectDir, "src/mdx/custom-components.tsx")) ? readFileSync2(projectPath(projectDir, "src/mdx/custom-components.tsx"), "utf8") : void 0
246
+ }), "Documentation rendered");
247
+ await terminal.step("Writing documentation", async (update) => {
248
+ let written = 0;
249
+ for (const file of rendered) {
250
+ const destination = projectPath(projectDir, file.path);
251
+ mkdirSync(dirname(destination), { recursive: true });
252
+ writeFileSync(destination, file.content);
253
+ written += 1;
254
+ if (written % 32 === 0) {
255
+ update(`Writing documentation (${written}/${rendered.length} files)`);
256
+ await setImmediate();
257
+ }
258
+ }
259
+ if (bundle.quarantinedFiles?.length) ignoreQuarantineDirectory(projectDir);
260
+ }, `Imported ${bundle.pages.length} pages and ${bundle.assets.length} assets from ${bundle.platform}`);
261
+ const format = (warning) => `${warning.message}${warning.source ? ` (${warning.source})` : ""}`;
262
+ for (const warning of bundle.warnings.filter((item) => item.code !== "gated-page")) terminal.warn(format(warning));
263
+ const gatedWarnings = bundle.warnings.filter((item) => item.code === "gated-page");
264
+ if (gatedWarnings.length > 0) {
265
+ terminal.warn("ACCESS-RESTRICTED CONTENT \u2014 review before publishing");
266
+ for (const warning of gatedWarnings) terminal.warn(format(warning));
267
+ if (bundle.droppedGatedPages) {
268
+ terminal.warn(`${bundle.droppedGatedPages} access-restricted page(s) were dropped by the file limit: not published and not saved under migration-quarantine/; recover them from the source repository.`);
269
+ }
270
+ if (bundle.quarantinedFiles?.length || gatedWarnings.some((warning) => !/dashboard access settings/i.test(warning.message))) {
271
+ terminal.warn("Before publishing, review migration-quarantine/assets/ and the dashboard access settings of the source site.");
272
+ }
273
+ }
274
+ let installationFailed = false;
275
+ if (!options.skipValidation) terminal.info("Migration validation runs project code locally, including imported MDX and components.");
276
+ if (!options.into && !options.skipValidation) {
277
+ try {
278
+ await installDeps(projectDir);
279
+ } catch (error) {
280
+ const exitCode = error.exitCode;
281
+ if (exitCode === 130 || exitCode === 143) throw error;
282
+ installationFailed = true;
283
+ }
284
+ }
285
+ terminal.info("Validating imported documentation...");
286
+ const validation = await validateMigration(projectDir, options.skipValidation, installationFailed);
287
+ const quarantinedPages = (bundle.quarantinedFiles ?? []).filter((file) => /\.mdx?$/i.test(file.path)).length;
288
+ const reportPath = projectPath(projectDir, "migration-report.json");
289
+ writeFileSync(reportPath, `${JSON.stringify({
290
+ version: 1,
291
+ sourceUrl: `${new URL(options.sourceUrl).origin}${new URL(options.sourceUrl).pathname}`,
292
+ platform: bundle.platform,
293
+ pages: bundle.pages.length,
294
+ assets: bundle.assets.length,
295
+ components: bundle.componentFiles?.length ?? 0,
296
+ quarantined: quarantinedPages,
297
+ quarantinedAssets: (bundle.quarantinedFiles?.length ?? 0) - quarantinedPages,
298
+ droppedGatedPages: bundle.droppedGatedPages ?? 0,
299
+ warnings: bundle.warnings,
300
+ validation
301
+ }, null, 2)}
302
+ `);
303
+ for (const message of validation.messages) terminal.warn(message);
304
+ terminal.detail("Migration report", reportPath);
305
+ if (validation.content === "passed" && validation.build === "passed") {
306
+ terminal.success(`Content and production build passed.${bundle.warnings.length ? " Review the migration warnings for compatibility limitations." : ""}`);
307
+ } else {
308
+ terminal.warn("Import retained, but validation is incomplete. Do not publish without reviewing the report.");
309
+ }
310
+ if (!options.into) await initGit(projectDir);
311
+ return {
312
+ pagesWritten: bundle.pages.length,
313
+ assetsWritten: bundle.assets.length,
314
+ projectDir,
315
+ platform: bundle.platform,
316
+ warnings: bundle.warnings,
317
+ validation,
318
+ reportPath
319
+ };
320
+ }
321
+
322
+ export {
323
+ migrateDocs
324
+ };
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/docs-json.ts
4
+ import { readFileSync, writeFileSync } from "fs";
5
+ import { join } from "path";
6
+ function readDocsJson(projectDir) {
7
+ const docsPath = join(projectDir, "docs.json");
8
+ const raw = readFileSync(docsPath, "utf8");
9
+ return JSON.parse(raw);
10
+ }
11
+ function writeDocsJson(projectDir, config) {
12
+ const docsPath = join(projectDir, "docs.json");
13
+ writeFileSync(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
14
+ }
15
+ function resetTrackingConfig(projectDir) {
16
+ const config = readDocsJson(projectDir);
17
+ if (config.tracking) {
18
+ delete config.tracking;
19
+ writeDocsJson(projectDir, config);
20
+ }
21
+ }
22
+ function writeTrackingConfig(projectDir, repos) {
23
+ if (repos.length === 0) return;
24
+ const config = readDocsJson(projectDir);
25
+ config.tracking = { repos: repos.map((r) => ({ owner: r.owner, repo: r.repo, branch: "main" })) };
26
+ writeDocsJson(projectDir, config);
27
+ }
28
+
29
+ export {
30
+ readDocsJson,
31
+ writeDocsJson,
32
+ resetTrackingConfig,
33
+ writeTrackingConfig
34
+ };
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/public-specs.ts
4
+ import { lstatSync, readFileSync, readdirSync, statSync } from "fs";
5
+ import { join, relative } from "path";
6
+ import { parse as parseYaml } from "yaml";
7
+ var OPENAPI_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
8
+ var SPEC_EXTENSIONS = /\.(?:json|ya?ml)$/i;
9
+ var MAX_SPEC_BYTES = 1e7;
10
+ var MAX_DEPTH = 8;
11
+ var MAX_ENTRIES = 5e3;
12
+ function shadowNote(urlPath) {
13
+ return urlPath === "openapi.json" || urlPath === "openapi.yaml" ? ` It is also served at /${urlPath} in place of the filtered specification, so visitors get this raw file there.` : "";
14
+ }
15
+ var isFlagged = (value) => value === true || value === "true";
16
+ function specHasHiddenOperations(spec, overrides) {
17
+ if (overrides && typeof overrides === "object" && Object.values(overrides).some((o) => o?.hidden === true)) return true;
18
+ const components = spec.components;
19
+ for (const group of [spec.paths, spec.webhooks, spec["x-webhooks"], components?.pathItems]) {
20
+ if (!group || typeof group !== "object") continue;
21
+ for (const item of Object.values(group)) {
22
+ if (!item || typeof item !== "object") continue;
23
+ const record = item;
24
+ if (isFlagged(record["x-excluded"]) || isFlagged(record["x-hidden"])) return true;
25
+ for (const method of OPENAPI_METHODS) {
26
+ const op = record[method];
27
+ if (op && typeof op === "object" && (isFlagged(op["x-excluded"]) || isFlagged(op["x-hidden"]))) return true;
28
+ }
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+ function findPublicSpecs(projectDir) {
34
+ const publicDir = join(projectDir, "public");
35
+ const scan = { specs: [], skipped: [] };
36
+ let seen = 0;
37
+ const walk = (dir, depth) => {
38
+ let entries;
39
+ try {
40
+ entries = readdirSync(dir).sort();
41
+ } catch {
42
+ return;
43
+ }
44
+ for (const entry of entries) {
45
+ if (++seen > MAX_ENTRIES) return;
46
+ const full = join(dir, entry);
47
+ let stat;
48
+ try {
49
+ stat = lstatSync(full);
50
+ if (stat.isSymbolicLink()) {
51
+ const target = statSync(full);
52
+ if (!target.isFile()) continue;
53
+ stat = target;
54
+ }
55
+ } catch {
56
+ continue;
57
+ }
58
+ if (stat.isDirectory()) {
59
+ if (depth < MAX_DEPTH && entry !== "node_modules") walk(full, depth + 1);
60
+ continue;
61
+ }
62
+ if (!stat.isFile() || !SPEC_EXTENSIONS.test(entry)) continue;
63
+ const urlPath = relative(publicDir, full).split(/[\\/]/).join("/");
64
+ if (stat.size > MAX_SPEC_BYTES) {
65
+ scan.skipped.push(`public/${urlPath}`);
66
+ continue;
67
+ }
68
+ try {
69
+ const raw = readFileSync(full, "utf8");
70
+ const parsed = /\.json$/i.test(entry) ? JSON.parse(raw) : parseYaml(raw);
71
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
72
+ const spec = parsed;
73
+ if (typeof spec.openapi !== "string" && typeof spec.swagger !== "string") continue;
74
+ scan.specs.push({ path: `public/${urlPath}`, urlPath, hasHiddenOperations: specHasHiddenOperations(spec) });
75
+ } catch {
76
+ }
77
+ }
78
+ };
79
+ walk(publicDir, 0);
80
+ return scan;
81
+ }
82
+
83
+ export {
84
+ shadowNote,
85
+ specHasHiddenOperations,
86
+ findPublicSpecs
87
+ };
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ personalizeStarter,
4
+ updateEnvExample
5
+ } from "./chunk-4VKRBJV2.js";
6
+ import {
7
+ resetTrackingConfig,
8
+ writeTrackingConfig
9
+ } from "./chunk-J7255UQA.js";
10
+ import {
11
+ downloadStarter
12
+ } from "./chunk-WWIATVAD.js";
13
+ import {
14
+ terminal
15
+ } from "./chunk-L2YCVQBR.js";
16
+
17
+ // src/scaffold.ts
18
+ import { existsSync, mkdirSync, readdirSync } from "fs";
19
+ import { resolve as resolve2 } from "path";
20
+
21
+ // src/utils.ts
22
+ import { execFileSync, execSync, spawn } from "child_process";
23
+ import { resolve } from "path";
24
+ function slugify(name) {
25
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
26
+ }
27
+ async function runProjectCommand(command, args, cwd, timeout, onProgress) {
28
+ const isQuiet = terminal.isRich && !terminal.isVerbose;
29
+ await new Promise((complete, reject) => {
30
+ const child = spawn(command, args, {
31
+ cwd,
32
+ detached: process.platform !== "win32",
33
+ stdio: isQuiet ? ["ignore", "pipe", "pipe"] : "inherit",
34
+ // Windows resolves npm via a .cmd shim; command and arguments here are
35
+ // fixed by the toolchain, never interpolated from imported content.
36
+ shell: process.platform === "win32" && command === "npm"
37
+ });
38
+ let tail = Buffer.alloc(0);
39
+ let omitted = false;
40
+ const collect = (chunk) => {
41
+ const combined = Buffer.concat([tail, chunk]);
42
+ omitted ||= combined.length > 64 * 1024;
43
+ tail = combined.subarray(Math.max(0, combined.length - 64 * 1024));
44
+ const stages = chunk.toString("utf8").split(/[\r\n]+/).map((line) => line.trim()).filter((line) => /^(?:>|npm (?:warn|error)|added \d+|.*(?:Creating an optimized|Compiled successfully|Collecting page data|Generating static pages|Finalizing page optimization))/.test(line));
45
+ if (stages.length) onProgress?.(stages.at(-1));
46
+ };
47
+ child.stdout?.on("data", collect);
48
+ child.stderr?.on("data", collect);
49
+ let interrupted;
50
+ let didTimeOut = false;
51
+ let escalation;
52
+ const killTree = (signal) => {
53
+ if (!child.pid) return;
54
+ try {
55
+ if (process.platform === "win32") {
56
+ execFileSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
57
+ } else {
58
+ process.kill(-child.pid, signal);
59
+ }
60
+ } catch {
61
+ child.kill(signal);
62
+ }
63
+ };
64
+ const interrupt = (signal) => {
65
+ interrupted = signal;
66
+ killTree(signal);
67
+ escalation = setTimeout(() => killTree("SIGKILL"), 1e3);
68
+ };
69
+ const onInterrupt = () => interrupt("SIGINT");
70
+ const onTerminate = () => interrupt("SIGTERM");
71
+ const onExit = () => killTree("SIGKILL");
72
+ const deadline = timeout ? setTimeout(() => {
73
+ didTimeOut = true;
74
+ interrupt("SIGTERM");
75
+ }, timeout) : void 0;
76
+ process.once("SIGINT", onInterrupt);
77
+ process.once("SIGTERM", onTerminate);
78
+ process.once("exit", onExit);
79
+ const cleanup = () => {
80
+ if (interrupted) killTree("SIGKILL");
81
+ if (deadline) clearTimeout(deadline);
82
+ if (escalation) clearTimeout(escalation);
83
+ process.removeListener("SIGINT", onInterrupt);
84
+ process.removeListener("SIGTERM", onTerminate);
85
+ process.removeListener("exit", onExit);
86
+ };
87
+ child.once("error", (error) => {
88
+ cleanup();
89
+ reject(error);
90
+ });
91
+ child.once("close", (code, signal) => {
92
+ cleanup();
93
+ if (code === 0 && !interrupted) {
94
+ complete();
95
+ return;
96
+ }
97
+ const failure = new Error(`${command} ${args.join(" ")} failed (${interrupted ?? signal ?? `exit ${code}`}).`);
98
+ if (didTimeOut) failure.message += ` Timed out after ${timeout}ms.`;
99
+ if (interrupted && !didTimeOut) failure.exitCode = interrupted === "SIGINT" ? 130 : 143;
100
+ failure.output = `${omitted ? "Earlier output omitted; use --verbose for the full log.\n" : ""}${tail.toString("utf8")}`;
101
+ reject(failure);
102
+ });
103
+ });
104
+ }
105
+ async function initGit(targetDir) {
106
+ try {
107
+ await terminal.step("Initialize Git repository", async (update) => {
108
+ update("Initialize Git repository");
109
+ await runProjectCommand("git", ["init"], targetDir);
110
+ update("Stage generated project files");
111
+ await runProjectCommand("git", ["add", "-A"], targetDir);
112
+ update("Create initial project commit");
113
+ await runProjectCommand("git", ["commit", "-m", "Initial commit from create-thally-docs"], targetDir);
114
+ }, "Git repository initialized");
115
+ } catch (error) {
116
+ if (error.exitCode === 130 || error.exitCode === 143) throw error;
117
+ const failure = error;
118
+ const diagnostic = [failure.output, failure.stdout, failure.stderr].filter(Boolean).map((value) => String(value)).join("\n").slice(-64 * 1024);
119
+ if (diagnostic.trim()) terminal.error(diagnostic.trimEnd());
120
+ terminal.warn("Could not finish Git setup. Run git status in the project directory, fix the reported issue, then retry the commit.");
121
+ }
122
+ }
123
+ async function installDeps(targetDir) {
124
+ try {
125
+ await terminal.step("Install dependencies", (update) => runProjectCommand("npm", [
126
+ "install",
127
+ "--prefer-offline",
128
+ "--no-audit",
129
+ "--no-fund",
130
+ "--progress=false"
131
+ ], targetDir, void 0, (message) => update(`Install dependencies: ${message}`)), "Dependencies installed");
132
+ } catch (error) {
133
+ const failure = error;
134
+ if (failure.output?.trim()) terminal.error(failure.output.trimEnd());
135
+ throw error;
136
+ }
137
+ }
138
+ function quoteDirectory(directory) {
139
+ const absolute = resolve(directory);
140
+ return process.platform === "win32" ? `"${absolute.replace(/"/g, '""')}"` : `'${absolute.replace(/'/g, "'\\''")}'`;
141
+ }
142
+ function previewCommand(directory, needsInstall = false) {
143
+ return `cd ${quoteDirectory(directory)} && ${needsInstall ? "npm install && " : ""}npm run dev`;
144
+ }
145
+ function logo(version) {
146
+ terminal.intro("init", version, "Build and publish docs for people and AI tools.");
147
+ }
148
+ function success(projectDir, projectName, dependenciesInstalled) {
149
+ terminal.detail("Project", projectName);
150
+ terminal.detail("Output", projectDir);
151
+ terminal.section("Make it yours", [
152
+ "src/content/ Write your documentation",
153
+ "docs.json Set up navigation",
154
+ "src/data/site.ts Name, links, and branding"
155
+ ]);
156
+ terminal.outro("Your docs project is ready. The dev server will print your preview URL.");
157
+ terminal.nextAction("Preview locally", previewCommand(projectDir, !dependenciesInstalled));
158
+ }
159
+
160
+ // src/scaffold.ts
161
+ async function scaffold(options) {
162
+ const {
163
+ projectDir,
164
+ projectName,
165
+ description,
166
+ brandPreset,
167
+ repoUrl,
168
+ doInstall,
169
+ enableAiChat = true,
170
+ i18nLocales,
171
+ trackRepos,
172
+ colors
173
+ } = options;
174
+ const targetDir = resolve2(projectDir);
175
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
176
+ throw new Error(`Directory "${targetDir}" already exists and is not empty.`);
177
+ }
178
+ mkdirSync(targetDir, { recursive: true });
179
+ const slug = slugify(projectName);
180
+ await terminal.step(
181
+ "Downloading starter",
182
+ () => downloadStarter(targetDir, projectName, void 0, { announce: false }),
183
+ "Starter downloaded"
184
+ );
185
+ await terminal.step("Configuring project", () => {
186
+ personalizeStarter(targetDir, {
187
+ projectName,
188
+ packageName: slug,
189
+ description,
190
+ brandPreset,
191
+ repoUrl,
192
+ enableAiChat,
193
+ i18nLocales,
194
+ colors
195
+ });
196
+ resetTrackingConfig(targetDir);
197
+ if (trackRepos?.length) writeTrackingConfig(targetDir, trackRepos);
198
+ updateEnvExample(targetDir);
199
+ }, "Project configured");
200
+ if (trackRepos?.length) {
201
+ const list = trackRepos.map((r) => `${r.owner}/${r.repo}`).join(", ");
202
+ terminal.info(`Thally Track enabled \u2014 watching ${list} (branch main, all files; refine in docs.json).`);
203
+ terminal.info("To finish wiring it: `thally track setup` (pick a trigger) + `thally agent init`,");
204
+ terminal.info("then add your ANTHROPIC_API_KEY. See /guides/thally-track.");
205
+ }
206
+ if (doInstall) {
207
+ await installDeps(targetDir);
208
+ }
209
+ await initGit(targetDir);
210
+ return { projectDir: targetDir };
211
+ }
212
+
213
+ export {
214
+ slugify,
215
+ runProjectCommand,
216
+ initGit,
217
+ installDeps,
218
+ previewCommand,
219
+ logo,
220
+ success,
221
+ scaffold
222
+ };