create-thally-docs 0.10.118 → 0.10.119

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/README.md CHANGED
@@ -13,13 +13,13 @@ cd my-docs
13
13
  npm run dev
14
14
  ```
15
15
 
16
- Sites created from a starter that includes the `thally` CLI
17
- (`@thallylabs/cli`) as a dev dependency can run `npx thally dev` instead, which
18
- also prints the local `llms.txt` and MCP endpoints once the server is ready.
19
- The completion message prints the right command for the project you created.
16
+ The default site does not bundle the Thally CLI or deployment tooling. Run
17
+ `npm run check` to validate documentation, or `npm run starter:update` to preview
18
+ a framework update using the current official CLI. Legacy sites that explicitly
19
+ install a local CLI retain their existing preview command.
20
20
 
21
- The dev server starts at [http://localhost:3040](http://localhost:3040) and
22
- automatically uses the next available port when needed. A freshly scaffolded
21
+ The dev server starts at [http://localhost:3040](http://localhost:3040). If that
22
+ port is occupied, run `npm run dev -- --port 3041`. A freshly scaffolded
23
23
  site is agent-ready out of the box and scores 100/A on the built-in
24
24
  [Agent Readiness Score](https://github.com/thallylabs/thally).
25
25
 
@@ -74,7 +74,7 @@ runs retain subprocess output for scripts and CI. These options also work with
74
74
  | Command | What it does |
75
75
  | --- | --- |
76
76
  | `create-thally-docs <dir>` | Scaffold a new project |
77
- | `create-thally-docs migrate <github-or-docs-url> [dir]` | Import a docs repository or public docs site through the shared migration engine |
77
+ | `create-thally-docs migrate <github-or-docs-url-or-directory> [dir]` | Import a docs repository or public docs site through the shared migration engine |
78
78
  | `create-thally-docs check [dir] [--fix] [--ci] [--drift] [--external]` | Lint content for broken links, orphan pages, and missing frontmatter; `--external` also checks links to public hosts (bounded, warnings only) |
79
79
  | `create-thally-docs translate --locale <code>` | Translate content into another locale |
80
80
 
@@ -88,6 +88,36 @@ destination runtime still apply. For example:
88
88
  npx create-thally-docs migrate https://docs.example.com my-docs --max-pages 1500 --yes
89
89
  ```
90
90
 
91
+ A local checkout works without making a private repository public:
92
+
93
+ ```bash
94
+ npx create-thally-docs migrate "../private docs" my-docs --platform mintlify --yes
95
+ npx create-thally-docs migrate https://docs.example.com my-docs --openapi https://docs.example.com/openapi.json --yes
96
+ ```
97
+
98
+ Local sources are supported by the local CLI (`thally migrate` forwards the same
99
+ arguments). The selected input and output must be separate directories. The
100
+ importer snapshots documentation inputs without executing configuration; it skips
101
+ hidden files, dependency directories and symlinks that escape the selected root.
102
+ The Cloud and MCP URL adapters do not gain access to local paths or `file://` URLs.
103
+
104
+ Website migration restores publisher-provided embedded OpenAPI operations and
105
+ can discover a single linked `openapi.json`/`openapi.yaml` specification. Supply
106
+ `--openapi <https-url>` when the source does not expose one, or links several.
107
+ Only unambiguous operation routes are bound to crawled pages; their original
108
+ prose and URLs stay intact, and generated API navigation excludes those operations.
109
+ Markdown mirror links retain `.md`; machine indexes and spec links use the new
110
+ site's equivalent routes. The migration report identifies unresolved internal links
111
+ and unsupported frontmatter that need review. Supported code-fence labels,
112
+ line numbers, wrapping, focus and highlights remain portable.
113
+
114
+ An in-place import (`--into <dir>`) preserves authored files that conflict with
115
+ incoming content and lists them in `migration-report.json`; validation fails until
116
+ you resolve those conflicts. Authored navigation is merged. Untouched sample
117
+ navigation is replaced only after comparison with the project's verified immutable
118
+ starter archive and unchanged sample content. If provenance cannot be proved, the
119
+ existing navigation is preserved and the report explains why.
120
+
91
121
  Interactive migrations ask whether the source is Mintlify, Docusaurus, Fern, or
92
122
  another auto-detected platform. When automatic detection receives a live
93
123
  website URL, the CLI recommends its source GitHub repository and requires
@@ -111,7 +141,7 @@ embedded by Mintlify when it is available, with bounded same-site crawling as a
111
141
  fallback.
112
142
 
113
143
  Migrated OpenAPI specs are written to `openapi/`, not `public/` (which the host
114
- serves as-is). `--into` never deletes files: an older copy of a spec already in
144
+ serves as-is). `--into` never deletes authored files; only verified unchanged starter samples may be removed: an older copy of a spec already in
115
145
  `public/` stays publicly downloadable (a `public/openapi.json` even answers
116
146
  `/openapi.json` in place of the filtered spec), so the migration lists such files and
117
147
  `thally check` warns about them until you delete them manually.
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-L2YCVQBR.js";
5
5
  import {
6
6
  readStarterReleaseManifest
7
- } from "./chunk-QPHUUFSH.js";
7
+ } from "./chunk-C4DPGWFM.js";
8
8
  import {
9
9
  STABLE_SCAFFOLD_RELEASE
10
10
  } from "./chunk-FMVUFWZP.js";
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-J7255UQA.js";
10
10
  import {
11
11
  downloadStarter
12
- } from "./chunk-JKC4FXSW.js";
12
+ } from "./chunk-44WRU5LD.js";
13
13
  import {
14
14
  terminal
15
15
  } from "./chunk-L2YCVQBR.js";
@@ -22,6 +22,7 @@ import { resolve as resolve2 } from "path";
22
22
  import { execFileSync, execSync, spawn } from "child_process";
23
23
  import { existsSync, readFileSync } from "fs";
24
24
  import { join, resolve } from "path";
25
+ import semver from "semver";
25
26
  function slugify(name) {
26
27
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
27
28
  }
@@ -34,7 +35,7 @@ async function runProjectCommand(command, args, cwd, timeout, onProgress) {
34
35
  stdio: isQuiet ? ["ignore", "pipe", "pipe"] : "inherit",
35
36
  // Windows resolves npm via a .cmd shim; command and arguments here are
36
37
  // fixed by the toolchain, never interpolated from imported content.
37
- shell: process.platform === "win32" && command === "npm"
38
+ shell: process.platform === "win32" && ["npm", "pnpm", "yarn"].includes(command)
38
39
  });
39
40
  let tail = Buffer.alloc(0);
40
41
  let omitted = false;
@@ -121,18 +122,51 @@ async function initGit(targetDir) {
121
122
  terminal.warn("Could not finish Git setup. Run git status in the project directory, fix the reported issue, then retry the commit.");
122
123
  }
123
124
  }
124
- async function installDeps(targetDir) {
125
+ function detectPackageManager(targetDir, requested, userAgent = process.env.npm_config_user_agent) {
126
+ if (requested) return requested;
127
+ if (existsSync(join(targetDir, "pnpm-lock.yaml"))) return "pnpm";
128
+ if (existsSync(join(targetDir, "yarn.lock"))) return "yarn";
129
+ if (userAgent?.startsWith("pnpm/")) return "pnpm";
130
+ if (userAgent?.startsWith("yarn/")) return "yarn";
131
+ return "npm";
132
+ }
133
+ function dependencyInstallArguments(targetDir, manager) {
134
+ if (manager !== "npm") return ["install"];
135
+ return [
136
+ existsSync(join(targetDir, "package-lock.json")) ? "ci" : "install",
137
+ "--no-audit",
138
+ "--no-fund",
139
+ "--progress=false"
140
+ ];
141
+ }
142
+ function assertProjectNodeVersion(targetDir, nodeVersion = process.versions.node) {
143
+ const manifest = JSON.parse(readFileSync(join(targetDir, "package.json"), "utf8"));
144
+ const range = manifest.engines?.node;
145
+ if (range === void 0) return;
146
+ if (typeof range !== "string" || !semver.validRange(range)) {
147
+ throw new Error("The documentation project declares an invalid Node.js engine range.");
148
+ }
149
+ if (!semver.satisfies(nodeVersion, range)) {
150
+ throw new Error(`This documentation project requires Node.js ${range}; you are using ${nodeVersion}. Switch to a supported Node.js version and retry installation. Your project files and lockfile are preserved.`);
151
+ }
152
+ }
153
+ async function installDeps(targetDir, requested) {
154
+ const manager = detectPackageManager(targetDir, requested);
155
+ const args = dependencyInstallArguments(targetDir, manager);
125
156
  try {
126
- await terminal.step("Install dependencies", (update) => runProjectCommand("npm", [
127
- "install",
128
- "--prefer-offline",
129
- "--no-audit",
130
- "--no-fund",
131
- "--progress=false"
132
- ], targetDir, void 0, (message) => update(`Install dependencies: ${message}`)), "Dependencies installed");
157
+ assertProjectNodeVersion(targetDir);
158
+ await terminal.step("Install dependencies", (update) => runProjectCommand(
159
+ manager,
160
+ args,
161
+ targetDir,
162
+ void 0,
163
+ (message) => update(`Install dependencies: ${message}`)
164
+ ), "Dependencies installed");
133
165
  } catch (error) {
134
166
  const failure = error;
135
167
  if (failure.output?.trim()) terminal.error(failure.output.trimEnd());
168
+ else terminal.error(failure.message);
169
+ terminal.warn(`Installation did not complete. Your content and lockfile were kept. Retry: cd ${quoteDirectory(targetDir)} && ${manager} ${args.join(" ")}. If a package is incomplete, remove only node_modules before retrying.`);
136
170
  throw error;
137
171
  }
138
172
  }
@@ -148,18 +182,22 @@ function declaresThallyCli(directory) {
148
182
  return false;
149
183
  }
150
184
  }
151
- function devCommand(directory, needsInstall = false) {
185
+ function devCommand(directory, needsInstall = false, requested) {
152
186
  const binary = join(directory, "node_modules", ".bin", "thally");
153
187
  const hasLocalCli = needsInstall ? declaresThallyCli(directory) : existsSync(binary) || existsSync(`${binary}.cmd`);
154
- return hasLocalCli ? "npx thally dev" : "npm run dev";
188
+ const manager = detectPackageManager(directory, requested);
189
+ if (!hasLocalCli) return `${manager} run dev`;
190
+ return manager === "npm" ? "npx thally dev" : manager === "pnpm" ? "pnpm exec thally dev" : "yarn thally dev";
155
191
  }
156
- function previewCommand(directory, needsInstall = false) {
157
- return `cd ${quoteDirectory(directory)} && ${needsInstall ? "npm install && " : ""}${devCommand(directory, needsInstall)}`;
192
+ function previewCommand(directory, needsInstall = false, requested) {
193
+ const manager = detectPackageManager(directory, requested);
194
+ const install = manager === "npm" && existsSync(join(directory, "package-lock.json")) ? "npm ci" : `${manager} install`;
195
+ return `cd ${quoteDirectory(directory)} && ${needsInstall ? `${install} && ` : ""}${devCommand(directory, needsInstall, requested)}`;
158
196
  }
159
197
  function logo(version) {
160
198
  terminal.intro("init", version, "Build and publish docs for people and AI tools.");
161
199
  }
162
- function success(projectDir, projectName, dependenciesInstalled) {
200
+ function success(projectDir, projectName, dependenciesInstalled, requested) {
163
201
  terminal.detail("Project", projectName);
164
202
  terminal.detail("Output", projectDir);
165
203
  terminal.section("Make it yours", [
@@ -168,7 +206,7 @@ function success(projectDir, projectName, dependenciesInstalled) {
168
206
  "src/data/site.ts Name, links, and branding"
169
207
  ]);
170
208
  terminal.outro("Your docs project is ready. The dev server will print your preview URL.");
171
- terminal.nextAction("Preview locally", previewCommand(projectDir, !dependenciesInstalled));
209
+ terminal.nextAction("Preview locally", previewCommand(projectDir, !dependenciesInstalled, requested));
172
210
  }
173
211
 
174
212
  // src/scaffold.ts
@@ -221,12 +259,12 @@ async function scaffold(options) {
221
259
  let dependenciesInstalled = false;
222
260
  if (doInstall) {
223
261
  try {
224
- await installDeps(targetDir);
262
+ await installDeps(targetDir, options.packageManager);
225
263
  dependenciesInstalled = true;
226
264
  } catch (error) {
227
265
  const exitCode = error.exitCode;
228
266
  if (!continueOnInstallFailure || exitCode === 130 || exitCode === 143) throw error;
229
- terminal.warn('Dependency installation failed. The project was created; run "npm install" in it to finish.');
267
+ terminal.warn("Dependency installation failed. The project was created; use the recovery command above to finish.");
230
268
  }
231
269
  }
232
270
  await initGit(targetDir);
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  downloadStarter
4
- } from "./chunk-JKC4FXSW.js";
4
+ } from "./chunk-44WRU5LD.js";
5
5
  import {
6
6
  applyStarterRuntimeSyncPlan,
7
+ areStarterFileContentsEquivalent,
7
8
  captureTargetPrecondition,
8
9
  mergeStarterOwnershipContracts,
9
10
  parseStarterReleaseManifest,
10
11
  planStarterRuntimeSync,
11
12
  readStarterReleaseManifest,
12
13
  starterManifestSha256
13
- } from "./chunk-QPHUUFSH.js";
14
+ } from "./chunk-C4DPGWFM.js";
14
15
  import {
15
16
  STABLE_SCAFFOLD_RELEASE,
16
17
  SUPPORTED_SCAFFOLD_RELEASES
@@ -36,10 +37,6 @@ var STRUCTURED_MANUAL_REVIEW_PATHS = /* @__PURE__ */ new Set([
36
37
  "package-lock.json"
37
38
  ]);
38
39
  var MISSING_JSON_VALUE = /* @__PURE__ */ Symbol("missing-json-value");
39
- function hasSameBytes(left, right) {
40
- if (!left || !right) return left === right;
41
- return left.equals(right);
42
- }
43
40
  function isJsonObject(value) {
44
41
  return value !== MISSING_JSON_VALUE && value !== null && typeof value === "object" && !Array.isArray(value);
45
42
  }
@@ -121,10 +118,10 @@ function resolveManualReviewMutations(paths, downstreamFiles, previousReleaseFil
121
118
  const downstream = downstreamFiles.get(path);
122
119
  const previous = previousReleaseFiles.get(path);
123
120
  const target = targetReleaseFiles.get(path);
124
- if (hasSameBytes(previous, target) || hasSameBytes(downstream, target)) {
121
+ if (areStarterFileContentsEquivalent(path, previous, target) || areStarterFileContentsEquivalent(path, downstream, target)) {
125
122
  continue;
126
123
  }
127
- if (hasSameBytes(downstream, previous)) {
124
+ if (areStarterFileContentsEquivalent(path, downstream, previous)) {
128
125
  if (target) mutations.writeFiles.set(path, target);
129
126
  else mutations.deletePaths.push(path);
130
127
  continue;
@@ -136,7 +133,7 @@ function resolveManualReviewMutations(paths, downstreamFiles, previousReleaseFil
136
133
  target
137
134
  });
138
135
  if (merged.content) {
139
- if (!hasSameBytes(downstream, merged.content)) {
136
+ if (!areStarterFileContentsEquivalent(path, downstream, merged.content)) {
140
137
  mutations.writeFiles.set(path, merged.content);
141
138
  }
142
139
  continue;
@@ -343,7 +340,7 @@ async function resolveStarterUpdateBase(options) {
343
340
  assertRecoveryPackages(options.targetDir, baselineDir, baseline.packages);
344
341
  return { release, recoveredProvenance: true, manifestSha256 };
345
342
  }
346
- throw new Error("The project starter manifest is edited, unknown, or has no verified published framework baseline; refusing to update.");
343
+ throw new Error("The project starter manifest is edited, unknown, or has no verified published framework baseline; refusing to update. If this project was created by a newer release, retry with npx --yes --package=@thallylabs/cli@latest thally starter update. Keep starter-release.json unchanged.");
347
344
  } finally {
348
345
  rmSync(workspace, { recursive: true, force: true });
349
346
  }
@@ -354,7 +351,7 @@ function assertForwardRelease(previous, target, releases) {
354
351
  }
355
352
  if (previous.source.commitSha === target.source.commitSha) return;
356
353
  if (previous.starterVersion > target.starterVersion) {
357
- throw new Error("The installed CLI does not contain a newer starter release.");
354
+ throw new Error("The installed CLI does not contain a newer starter release. Retry with npx --yes --package=@thallylabs/cli@latest thally starter update.");
358
355
  }
359
356
  if (previous.starterVersion === target.starterVersion) {
360
357
  const targetIndex = releases.findIndex(
@@ -364,7 +361,7 @@ function assertForwardRelease(previous, target, releases) {
364
361
  (release) => release.source.commitSha === previous.source.commitSha
365
362
  );
366
363
  if (targetIndex === -1 || previousIndex === -1 || previousIndex <= targetIndex) {
367
- throw new Error("The installed CLI does not contain a newer starter release.");
364
+ throw new Error("The installed CLI does not contain a newer starter release. Retry with npx --yes --package=@thallylabs/cli@latest thally starter update.");
368
365
  }
369
366
  }
370
367
  }
@@ -5,6 +5,22 @@ import {
5
5
 
6
6
  // src/starter-sync.ts
7
7
  import { createHash } from "crypto";
8
+
9
+ // src/starter-file-comparison.ts
10
+ var textPolicies = /* @__PURE__ */ new Set([".gitignore", ".gitattributes"]);
11
+ function areStarterFileContentsEquivalent(path, left, right) {
12
+ if (!left || !right) return left === right;
13
+ if (left.equals(right)) return true;
14
+ const filename = path.split(/[\\/]/).at(-1) ?? "";
15
+ if (!/\.(?:[cm]?[jt]sx?|jsonc?|mdx?|ya?ml|css|html|svg|txt)$/.test(filename) && !textPolicies.has(filename)) return false;
16
+ if (left.includes(0) || right.includes(0)) return false;
17
+ const leftText = left.toString("utf8");
18
+ const rightText = right.toString("utf8");
19
+ if (!Buffer.from(leftText).equals(left) || !Buffer.from(rightText).equals(right)) return false;
20
+ return leftText.replace(/\r\n/g, "\n") === rightText.replace(/\r\n/g, "\n");
21
+ }
22
+
23
+ // src/starter-sync.ts
8
24
  import {
9
25
  copyFileSync,
10
26
  existsSync,
@@ -249,7 +265,7 @@ function collectFiles(root, contract, desiredOwnership) {
249
265
  }
250
266
  function sameFile(left, right) {
251
267
  if (!left || !right) return left === right;
252
- return readFileSync(left).equals(readFileSync(right));
268
+ return areStarterFileContentsEquivalent(left, readFileSync(left), readFileSync(right));
253
269
  }
254
270
  function fileSha256(path) {
255
271
  return createHash("sha256").update(readFileSync(path)).digest("hex");
@@ -520,6 +536,7 @@ function applyStarterRuntimeSyncPlan(newStarterDir, targetDir, plan, ownership,
520
536
  }
521
537
 
522
538
  export {
539
+ areStarterFileContentsEquivalent,
523
540
  parseStarterOwnershipContract,
524
541
  mergeStarterOwnershipContracts,
525
542
  starterManifestSha256,