create-thally-docs 0.10.62 → 0.10.65

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
@@ -66,6 +66,12 @@ projected into Thally. A live Mintlify URL uses the structured configuration
66
66
  embedded by Mintlify when it is available, with bounded same-site crawling as a
67
67
  fallback.
68
68
 
69
+ Migrated OpenAPI specs are written to `openapi/`, not `public/` (which the host
70
+ serves as-is). `--into` never deletes files: an older copy of a spec already in
71
+ `public/` stays publicly downloadable (a `public/openapi.json` even answers
72
+ `/openapi.json` in place of the filtered spec), so the migration lists such files and
73
+ `thally check` warns about them until you delete them manually.
74
+
69
75
  Repository-local JSX/TSX components and supported static dependencies are copied
70
76
  into the customer-owned MDX registry. Simple interactive HTML blocks are moved
71
77
  into client components, and locale/root routing is normalized. Unsupported
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  STABLE_SCAFFOLD_RELEASE
4
- } from "./chunk-I6IIZGQY.js";
4
+ } from "./chunk-MSCEHMMG.js";
5
5
 
6
6
  // src/starter-sync.ts
7
7
  import { createHash } from "crypto";
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-XYRM4GAT.js";
6
6
  import {
7
7
  downloadStarter
8
- } from "./chunk-UX57AKM4.js";
8
+ } from "./chunk-UBIX5ISG.js";
9
9
 
10
10
  // src/scaffold.ts
11
11
  import { existsSync, mkdirSync, readdirSync } from "fs";
@@ -3,19 +3,19 @@
3
3
  // src/stable-scaffold-release.json
4
4
  var stable_scaffold_release_default = {
5
5
  schemaVersion: 1,
6
- id: "2026-09-29.5b063972.7f16574b",
6
+ id: "2026-09-30.f7cdf474.f4a15647",
7
7
  source: {
8
8
  repository: "thallylabs/starter",
9
- commitSha: "5b0639728efe2b6a16ac93d3166e499c959e976b",
10
- treeSha: "251a4f91ad6deda5a4c3e257b067c3b7f74a5c00",
11
- archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/5b0639728efe2b6a16ac93d3166e499c959e976b",
9
+ commitSha: "f7cdf47487d58aaa0587d16bda1a4292ac7c8580",
10
+ treeSha: "0306927e5906195c4f8ec5174b0a661d4a29bd21",
11
+ archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/f7cdf47487d58aaa0587d16bda1a4292ac7c8580",
12
12
  manifestPath: "starter-release.json",
13
- manifestSha256: "ad9eafe8a243eb23670ca39a314cda7843b3e6da809f9a3cd1c49bd80f44848d"
13
+ manifestSha256: "ff505eb5f628cc3a7d09fd605ae5c5afc2301f65c74394abf88ddf7aeef3584f"
14
14
  },
15
15
  runtime: {
16
16
  repository: "thallylabs/thally",
17
- commitSha: "7f16574beefb0fdd91bc5bdaa322d3e7dc527c61",
18
- treeSha: "e046affbd74d72e7a6c041361ec3418d4fd95120",
17
+ commitSha: "f4a1564759c049bd82226c769485ead66817c78d",
18
+ treeSha: "1680f037c03f6b41c2a564f00983a7e2a084c87d",
19
19
  contentSource: "assets",
20
20
  identityContractVersion: 1
21
21
  },
@@ -24,6 +24,26 @@ var stable_scaffold_release_default = {
24
24
 
25
25
  // src/previous-scaffold-releases.json
26
26
  var previous_scaffold_releases_default = [
27
+ {
28
+ schemaVersion: 1,
29
+ id: "2026-09-29.5b063972.7f16574b",
30
+ source: {
31
+ repository: "thallylabs/starter",
32
+ commitSha: "5b0639728efe2b6a16ac93d3166e499c959e976b",
33
+ treeSha: "251a4f91ad6deda5a4c3e257b067c3b7f74a5c00",
34
+ archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/5b0639728efe2b6a16ac93d3166e499c959e976b",
35
+ manifestPath: "starter-release.json",
36
+ manifestSha256: "ad9eafe8a243eb23670ca39a314cda7843b3e6da809f9a3cd1c49bd80f44848d"
37
+ },
38
+ runtime: {
39
+ repository: "thallylabs/thally",
40
+ commitSha: "7f16574beefb0fdd91bc5bdaa322d3e7dc527c61",
41
+ treeSha: "e046affbd74d72e7a6c041361ec3418d4fd95120",
42
+ contentSource: "assets",
43
+ identityContractVersion: 1
44
+ },
45
+ starterVersion: 2
46
+ },
27
47
  {
28
48
  schemaVersion: 1,
29
49
  id: "2026-09-28.b0612637.c9291377",
@@ -5,12 +5,12 @@ import {
5
5
  readDocsJson,
6
6
  scaffold,
7
7
  writeDocsJson
8
- } from "./chunk-SLDENDOT.js";
8
+ } from "./chunk-KCBUOBEF.js";
9
9
 
10
10
  // src/migrate/index.ts
11
- import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
11
+ import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
12
12
  import { tmpdir } from "os";
13
- import { dirname, isAbsolute, join as join3, relative as relative2, resolve, sep } from "path";
13
+ import { basename, dirname, isAbsolute, join as join4, relative as relative3, resolve, sep } from "path";
14
14
  import {
15
15
  cloneGitHubRepository,
16
16
  hydrateRemoteApiSpecs,
@@ -20,18 +20,98 @@ import {
20
20
  renderMigrationFiles
21
21
  } from "@thallylabs/migrate";
22
22
 
23
+ // src/public-specs.ts
24
+ import { lstatSync, readFileSync, readdirSync, statSync } from "fs";
25
+ import { join, relative } from "path";
26
+ import { parse as parseYaml } from "yaml";
27
+ var OPENAPI_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
28
+ var SPEC_EXTENSIONS = /\.(?:json|ya?ml)$/i;
29
+ var MAX_SPEC_BYTES = 1e7;
30
+ var MAX_DEPTH = 8;
31
+ var MAX_ENTRIES = 5e3;
32
+ function shadowNote(urlPath) {
33
+ 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.` : "";
34
+ }
35
+ var isFlagged = (value) => value === true || value === "true";
36
+ function specHasHiddenOperations(spec, overrides) {
37
+ if (overrides && typeof overrides === "object" && Object.values(overrides).some((o) => o?.hidden === true)) return true;
38
+ const components = spec.components;
39
+ for (const group of [spec.paths, spec.webhooks, spec["x-webhooks"], components?.pathItems]) {
40
+ if (!group || typeof group !== "object") continue;
41
+ for (const item of Object.values(group)) {
42
+ if (!item || typeof item !== "object") continue;
43
+ const record = item;
44
+ if (isFlagged(record["x-excluded"]) || isFlagged(record["x-hidden"])) return true;
45
+ for (const method of OPENAPI_METHODS) {
46
+ const op = record[method];
47
+ if (op && typeof op === "object" && (isFlagged(op["x-excluded"]) || isFlagged(op["x-hidden"]))) return true;
48
+ }
49
+ }
50
+ }
51
+ return false;
52
+ }
53
+ function findPublicSpecs(projectDir) {
54
+ const publicDir = join(projectDir, "public");
55
+ const scan = { specs: [], skipped: [] };
56
+ let seen = 0;
57
+ const walk = (dir, depth) => {
58
+ let entries;
59
+ try {
60
+ entries = readdirSync(dir).sort();
61
+ } catch {
62
+ return;
63
+ }
64
+ for (const entry of entries) {
65
+ if (++seen > MAX_ENTRIES) return;
66
+ const full = join(dir, entry);
67
+ let stat;
68
+ try {
69
+ stat = lstatSync(full);
70
+ if (stat.isSymbolicLink()) {
71
+ const target = statSync(full);
72
+ if (!target.isFile()) continue;
73
+ stat = target;
74
+ }
75
+ } catch {
76
+ continue;
77
+ }
78
+ if (stat.isDirectory()) {
79
+ if (depth < MAX_DEPTH && entry !== "node_modules") walk(full, depth + 1);
80
+ continue;
81
+ }
82
+ if (!stat.isFile() || !SPEC_EXTENSIONS.test(entry)) continue;
83
+ const urlPath = relative(publicDir, full).split(/[\\/]/).join("/");
84
+ if (stat.size > MAX_SPEC_BYTES) {
85
+ scan.skipped.push(`public/${urlPath}`);
86
+ continue;
87
+ }
88
+ try {
89
+ const raw = readFileSync(full, "utf8");
90
+ const parsed = /\.json$/i.test(entry) ? JSON.parse(raw) : parseYaml(raw);
91
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
92
+ const spec = parsed;
93
+ if (typeof spec.openapi !== "string" && typeof spec.swagger !== "string") continue;
94
+ scan.specs.push({ path: `public/${urlPath}`, urlPath, hasHiddenOperations: specHasHiddenOperations(spec) });
95
+ } catch {
96
+ }
97
+ }
98
+ };
99
+ walk(publicDir, 0);
100
+ return scan;
101
+ }
102
+
23
103
  // src/migrate/validate.ts
24
104
  import { spawnSync } from "child_process";
25
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
26
- import { join as join2 } from "path";
105
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
106
+ import { join as join3 } from "path";
27
107
 
28
108
  // src/check.ts
29
- import { existsSync, readFileSync, readdirSync, statSync } from "fs";
30
- import { join, extname, relative } from "path";
109
+ import { existsSync, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
110
+ import { join as join2, extname, relative as relative2 } from "path";
31
111
  import { execFileSync } from "child_process";
32
112
 
33
113
  // src/frontmatter.ts
34
- import { parse as parseYaml } from "yaml";
114
+ import { parse as parseYaml2 } from "yaml";
35
115
  function parseFrontmatter(raw) {
36
116
  const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
37
117
  const opening = /^---([^\r\n]*)\r?\n/.exec(source);
@@ -46,7 +126,7 @@ function parseFrontmatter(raw) {
46
126
  if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
47
127
  return { content, data: {} };
48
128
  }
49
- const parsed = parseYaml(matter);
129
+ const parsed = parseYaml2(matter);
50
130
  return {
51
131
  content,
52
132
  data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
@@ -54,7 +134,57 @@ function parseFrontmatter(raw) {
54
134
  }
55
135
 
56
136
  // src/check.ts
57
- import { parse as parseYaml2 } from "yaml";
137
+ import { parse as parseYaml3 } from "yaml";
138
+
139
+ // src/openapi-publication.ts
140
+ var METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
141
+ var isObj = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
142
+ var flagged = (value) => value === true || value === "true";
143
+ function parseDocOperation(raw) {
144
+ if (typeof raw !== "string") return null;
145
+ const parts = raw.trim().split(/\s+/);
146
+ if (parts.length < 2) return null;
147
+ const path = parts.slice(1).join(" ");
148
+ return path.startsWith("/") ? { method: parts[0].toUpperCase(), path } : null;
149
+ }
150
+ function resolveLocalRef(document, ref) {
151
+ if (ref !== "#" && !ref.startsWith("#/")) return void 0;
152
+ let node = document;
153
+ for (const segment of ref.slice(2).split("/").filter(Boolean)) {
154
+ if (!isObj(node) && !Array.isArray(node)) return void 0;
155
+ let decoded = segment;
156
+ try {
157
+ decoded = decodeURIComponent(segment);
158
+ } catch {
159
+ }
160
+ node = node[decoded.replace(/~1/g, "/").replace(/~0/g, "~")];
161
+ }
162
+ return node;
163
+ }
164
+ function operationState(document, method, path, overrides) {
165
+ const verb = method.toLowerCase();
166
+ if (!METHODS.includes(verb) || !isObj(document) || !isObj(document.paths)) return "unknown";
167
+ const entry = document.paths[path];
168
+ if (!isObj(entry)) return "unknown";
169
+ const chain = [entry];
170
+ const seen = /* @__PURE__ */ new Set([entry]);
171
+ for (let current = entry; typeof current.$ref === "string"; ) {
172
+ const target = resolveLocalRef(document, current.$ref);
173
+ if (!isObj(target) || seen.has(target)) break;
174
+ seen.add(target);
175
+ chain.push(target);
176
+ current = target;
177
+ }
178
+ const item = Object.assign({}, ...[...chain].reverse());
179
+ const operation = item[verb];
180
+ if (!isObj(operation)) return "unknown";
181
+ if (chain.some((level) => flagged(level["x-excluded"])) || flagged(operation["x-excluded"])) return "excluded";
182
+ const override = isObj(overrides) ? overrides[`${verb.toUpperCase()} ${path}`] : void 0;
183
+ const hidden = isObj(override) && typeof override.hidden === "boolean" ? override.hidden : chain.some((level) => flagged(level["x-hidden"])) || flagged(operation["x-hidden"]);
184
+ return hidden ? "hidden" : "published";
185
+ }
186
+
187
+ // src/check.ts
58
188
  import { projectNavigationContract } from "@thallylabs/core/navigation";
59
189
  import { slugify } from "@thallylabs/core/slugify";
60
190
  function gitLocal(projectDir, args) {
@@ -111,14 +241,14 @@ function checkDrift(projectDir, file, data, issues) {
111
241
  function scanMdx(dir, results) {
112
242
  let entries;
113
243
  try {
114
- entries = readdirSync(dir);
244
+ entries = readdirSync2(dir);
115
245
  } catch {
116
246
  return;
117
247
  }
118
248
  for (const entry of entries) {
119
- const fullPath = join(dir, entry);
249
+ const fullPath = join2(dir, entry);
120
250
  try {
121
- const stat = statSync(fullPath);
251
+ const stat = statSync2(fullPath);
122
252
  if (stat.isDirectory()) scanMdx(fullPath, results);
123
253
  else if (extname(entry).toLowerCase() === ".mdx") results.push(fullPath);
124
254
  } catch {
@@ -294,21 +424,70 @@ function resolveLink(target, redirects, dynamicRedirects = []) {
294
424
  current = resolveDynamicRedirectDestination(dynamicMatch.redirect, dynamicMatch.match);
295
425
  }
296
426
  }
297
- function validateOpenApi(projectDir, source, issues) {
298
- const specPath = source.startsWith("/") ? join(projectDir, "public", source.slice(1)) : join(projectDir, source);
427
+ function checkPublicSpecs(projectDir, tabs, issues) {
428
+ const configured = tabs.flatMap((tab) => tab.api?.source ? [tab.api.source.replace(/^\.\//, "")] : []);
429
+ const isConfigured = (spec) => configured.some((source) => source === `/${spec.urlPath}` || source === spec.path);
430
+ const hasElsewhereSource = configured.some((source) => !/^https?:/i.test(source));
431
+ const { specs, skipped } = findPublicSpecs(projectDir);
432
+ for (const spec of specs) {
433
+ if (isConfigured(spec)) continue;
434
+ if (!spec.hasHiddenOperations && !hasElsewhereSource) continue;
435
+ issues.push({
436
+ severity: "warning",
437
+ file: spec.path,
438
+ message: `${spec.path} is an OpenAPI file served publicly as /${spec.urlPath}${spec.hasHiddenOperations ? " and it contains x-excluded/x-hidden operations, which are downloadable from it" : " but is not the spec this site is configured with (an old copy?)"}.${shadowNote(spec.urlPath)} Re-running migration into an existing site does not remove old copies from public/: delete this file manually`
439
+ });
440
+ }
441
+ for (const path of skipped) {
442
+ issues.push({ severity: "warning", file: path, message: `${path} is too large to inspect; if it is an OpenAPI file with hidden operations it is served as-is from public/` });
443
+ }
444
+ }
445
+ function checkUnpublishedOperationPages(projectDir, tabs, pages, issues) {
446
+ const api = tabs.find((tab) => !tab.hidden && tab.api?.source)?.api;
447
+ const source = api?.source;
448
+ if (!source || pages.length === 0 || /^https?:/i.test(source)) return;
449
+ const specPath = source.startsWith("/") ? join2(projectDir, "public", source.slice(1)) : join2(projectDir, source);
450
+ let spec;
451
+ try {
452
+ const raw = readFileSync2(specPath, "utf8");
453
+ spec = /\.json$/i.test(source) ? JSON.parse(raw) : parseYaml3(raw);
454
+ } catch {
455
+ return;
456
+ }
457
+ const overrides = api.overrides;
458
+ for (const { file, method, path } of pages) {
459
+ const state = operationState(spec, method, path, overrides);
460
+ if (state === "hidden" || state === "excluded") {
461
+ issues.push({
462
+ severity: "warning",
463
+ file,
464
+ message: `page ${file} points at ${state} operation ${method} ${path} and is not published: it returns 404 and is left out of navigation, search, sitemap and llms.txt. Publish the operation or point the page at another one`
465
+ });
466
+ }
467
+ }
468
+ }
469
+ function validateOpenApi(projectDir, source, issues, overrides) {
470
+ const specPath = source.startsWith("/") ? join2(projectDir, "public", source.slice(1)) : join2(projectDir, source);
299
471
  if (!existsSync(specPath)) {
300
472
  issues.push({ severity: "error", message: `API reference points at "${source}" but the file does not exist`, file: source });
301
473
  return;
302
474
  }
303
475
  let spec;
304
476
  try {
305
- const raw = readFileSync(specPath, "utf8");
306
- spec = source.endsWith(".json") ? JSON.parse(raw) : parseYaml2(raw);
477
+ const raw = readFileSync2(specPath, "utf8");
478
+ spec = source.endsWith(".json") ? JSON.parse(raw) : parseYaml3(raw);
307
479
  } catch (err) {
308
480
  issues.push({ severity: "error", message: `OpenAPI spec is not valid ${source.endsWith(".json") ? "JSON" : "YAML"}: ${err.message}`, file: source });
309
481
  return;
310
482
  }
311
483
  const s = spec;
484
+ if (s && typeof s === "object" && (source.startsWith("/") || source.replace(/^\.\//, "").startsWith("public/")) && specHasHiddenOperations(s, overrides)) {
485
+ issues.push({
486
+ severity: "warning",
487
+ message: `OpenAPI spec "${source}" lives under public/ and contains x-excluded/x-hidden operations; public/ is served as-is, so those operations would be downloadable.${shadowNote(source.replace(/^\.?\/+/, "").replace(/^public\//, ""))} Move it to a project-root path such as "openapi/${source.split("/").pop()}", update docs.json, and delete the copy in public/ (re-running migration into an existing site does not remove old copies from public/)`,
488
+ file: source
489
+ });
490
+ }
312
491
  if (typeof s?.openapi !== "string" && typeof s?.swagger !== "string") {
313
492
  issues.push({ severity: "error", message: 'OpenAPI spec is missing the "openapi" (or "swagger") version field', file: source });
314
493
  }
@@ -326,7 +505,7 @@ function validateOpenApi(projectDir, source, issues) {
326
505
  continue;
327
506
  }
328
507
  const hasOp = Object.keys(ops).some((k) => methods.has(k.toLowerCase()));
329
- if (!hasOp) {
508
+ if (!hasOp && typeof ops.$ref !== "string") {
330
509
  issues.push({ severity: "warning", message: `OpenAPI path "${p}" has no operations`, file: source });
331
510
  }
332
511
  }
@@ -334,13 +513,13 @@ function validateOpenApi(projectDir, source, issues) {
334
513
  }
335
514
  async function runCheck(projectDir, options) {
336
515
  const { fix, ci } = options;
337
- if (!existsSync(join(projectDir, "docs.json"))) {
516
+ if (!existsSync(join2(projectDir, "docs.json"))) {
338
517
  console.error(`
339
518
  \u274C Not a Thally project: docs.json not found in ${projectDir}
340
519
  `);
341
520
  return 1;
342
521
  }
343
- const contentDir = join(projectDir, "src", "content");
522
+ const contentDir = join2(projectDir, "src", "content");
344
523
  const issues = [];
345
524
  const config = readDocsJson(projectDir);
346
525
  const secondaryLocales = new Set(
@@ -369,7 +548,7 @@ async function runCheck(projectDir, options) {
369
548
  issues.push({ severity: "warning", message: `[duplicate] "${dup}" appears more than once in docs.json` });
370
549
  }
371
550
  for (const pageId of navPageIds) {
372
- const candidates = [join(contentDir, `${pageId}.mdx`), join(contentDir, `${pageId}/index.mdx`)];
551
+ const candidates = [join2(contentDir, `${pageId}.mdx`), join2(contentDir, `${pageId}/index.mdx`)];
373
552
  if (!candidates.some((c) => existsSync(c))) {
374
553
  issues.push({ severity: "error", message: `"${pageId}" is in docs.json but has no MDX file`, file: `src/content/${pageId}.mdx` });
375
554
  }
@@ -377,6 +556,7 @@ async function runCheck(projectDir, options) {
377
556
  const allFiles = [];
378
557
  if (existsSync(contentDir)) scanMdx(contentDir, allFiles);
379
558
  const fixedOrphans = [];
559
+ const operationPages = [];
380
560
  const validPaths = /* @__PURE__ */ new Set(["/"]);
381
561
  const anchorsByPath = /* @__PURE__ */ new Map();
382
562
  const linksByFile = [];
@@ -387,13 +567,13 @@ async function runCheck(projectDir, options) {
387
567
  let content = "";
388
568
  let lineOffset = 0;
389
569
  try {
390
- const raw = readFileSync(filePath, "utf8");
570
+ const raw = readFileSync2(filePath, "utf8");
391
571
  const parsed = parseFrontmatter(raw);
392
572
  data = parsed.data;
393
573
  content = parsed.content;
394
574
  lineOffset = raw.slice(0, raw.indexOf(content)).split("\n").length - 1;
395
575
  } catch {
396
- issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative(projectDir, filePath) });
576
+ issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative2(projectDir, filePath) });
397
577
  continue;
398
578
  }
399
579
  const { navPageId } = localizedPage(pageId, secondaryLocales);
@@ -402,15 +582,17 @@ async function runCheck(projectDir, options) {
402
582
  addOrphanToNav(projectDir, pageId);
403
583
  fixedOrphans.push(pageId);
404
584
  } else {
405
- issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file: relative(projectDir, filePath) });
585
+ issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file: relative2(projectDir, filePath) });
406
586
  }
407
587
  }
408
- const rel2 = relative(projectDir, filePath);
588
+ const rel2 = relative2(projectDir, filePath);
409
589
  if (!data.title) issues.push({ severity: "warning", message: `Missing "title" in frontmatter`, file: rel2 });
410
590
  if (!data.description) issues.push({ severity: "warning", message: `Missing "description" in frontmatter`, file: rel2 });
411
591
  if (typeof data.openapi !== "string" && content.trim().length < 50) {
412
592
  issues.push({ severity: "warning", message: `Very short body (${content.trim().length} chars) \u2014 page may be empty`, file: rel2 });
413
593
  }
594
+ const operation = parseDocOperation(data.openapi);
595
+ if (operation) operationPages.push({ file: rel2, ...operation });
414
596
  if (options.drift) checkDrift(projectDir, rel2, data, issues);
415
597
  const path = pageIdToPath(pageId, secondaryLocales);
416
598
  const anchors = extractHeadingAnchors(content);
@@ -454,7 +636,7 @@ async function runCheck(projectDir, options) {
454
636
  }
455
637
  }
456
638
  }
457
- const publicDir = join(projectDir, "public");
639
+ const publicDir = join2(projectDir, "public");
458
640
  for (const { file, images, offset } of linksByFile) {
459
641
  for (const { target, line: contentLine } of images) {
460
642
  const line = contentLine + offset;
@@ -467,14 +649,16 @@ async function runCheck(projectDir, options) {
467
649
  } catch {
468
650
  continue;
469
651
  }
470
- if (!existsSync(join(publicDir, decoded))) {
652
+ if (!existsSync(join2(publicDir, decoded))) {
471
653
  issues.push({ severity: "warning", message: `Image not found: "${target}" has no file under public/`, file, line });
472
654
  }
473
655
  }
474
656
  }
475
657
  for (const tab of config.tabs) {
476
- if (tab.api?.source) validateOpenApi(projectDir, tab.api.source, issues);
658
+ if (tab.api?.source) validateOpenApi(projectDir, tab.api.source, issues, tab.api.overrides);
477
659
  }
660
+ checkPublicSpecs(projectDir, config.tabs, issues);
661
+ checkUnpublishedOperationPages(projectDir, config.tabs, operationPages, issues);
478
662
  const errors = issues.filter((i) => i.severity === "error");
479
663
  options.onIssues?.(issues);
480
664
  const warnings = issues.filter((i) => i.severity === "warning");
@@ -544,9 +728,9 @@ async function validateMigration(projectDir, skip = false, installationFailed =
544
728
  result.messages.push("Dependency installation failed; production build was not attempted. Review the installation output before retrying.");
545
729
  return result;
546
730
  }
547
- const packagePath = join2(projectDir, "package.json");
731
+ const packagePath = join3(projectDir, "package.json");
548
732
  try {
549
- const manifest = existsSync2(packagePath) ? JSON.parse(readFileSync2(packagePath, "utf8")) : void 0;
733
+ const manifest = existsSync2(packagePath) ? JSON.parse(readFileSync3(packagePath, "utf8")) : void 0;
550
734
  if (!manifest?.scripts?.build) {
551
735
  result.messages.push("No build script is available; production rendering is not verified.");
552
736
  return result;
@@ -568,7 +752,7 @@ async function validateMigration(projectDir, skip = false, installationFailed =
568
752
  // src/migrate/index.ts
569
753
  function projectPath(projectDir, candidate) {
570
754
  const target = resolve(projectDir, candidate);
571
- const fromRoot = relative2(resolve(projectDir), target);
755
+ const fromRoot = relative3(resolve(projectDir), target);
572
756
  if (isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${sep}`)) {
573
757
  throw new Error(`Generated migration path escapes the project: ${candidate}`);
574
758
  }
@@ -577,7 +761,7 @@ function projectPath(projectDir, candidate) {
577
761
  function readExistingConfig(projectDir) {
578
762
  const configPath = projectPath(projectDir, "docs.json");
579
763
  if (!existsSync3(configPath)) return void 0;
580
- return JSON.parse(readFileSync3(configPath, "utf8"));
764
+ return JSON.parse(readFileSync4(configPath, "utf8"));
581
765
  }
582
766
  function resetFreshMigrationContent(projectDir) {
583
767
  const contentDirectory = projectPath(projectDir, "src/content");
@@ -600,8 +784,8 @@ async function discoverMigration(options) {
600
784
  }
601
785
  const source = parseGitHubRepositoryUrl(options.sourceUrl);
602
786
  if (options.branch) source.branch = options.branch;
603
- const temporaryRoot = mkdtempSync(join3(tmpdir(), "thally-migrate-"));
604
- const cloneDir = join3(temporaryRoot, "repository");
787
+ const temporaryRoot = mkdtempSync(join4(tmpdir(), "thally-migrate-"));
788
+ const cloneDir = join4(temporaryRoot, "repository");
605
789
  console.log(` \u{1F4E6} Cloning ${source.owner}/${source.repo}...`);
606
790
  try {
607
791
  const cloneWarnings = [];
@@ -618,6 +802,19 @@ async function discoverMigration(options) {
618
802
  rmSync(temporaryRoot, { recursive: true, force: true });
619
803
  }
620
804
  }
805
+ function publicSpecWarning(projectDir, bundle) {
806
+ const { specs, skipped } = findPublicSpecs(projectDir);
807
+ if (specs.length === 0 && skipped.length === 0) return null;
808
+ const migrated = new Set(bundle.assets.filter((asset) => asset.projectRelative).map((asset) => basename(asset.path)));
809
+ const lines = [
810
+ ...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)` : ""}`),
811
+ ...skipped.map((path) => `${path} (too large to inspect)`)
812
+ ];
813
+ return {
814
+ code: "unsupported-config",
815
+ 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.`
816
+ };
817
+ }
621
818
  async function migrateDocs(options) {
622
819
  const projectDir = resolve(options.projectDir);
623
820
  const bundle = await discoverMigration(options);
@@ -647,9 +844,13 @@ async function migrateDocs(options) {
647
844
  }
648
845
  bundle.docsConfig.i18n ??= { defaultLocale: "en", locales: [{ code: "en", label: "English" }] };
649
846
  }
847
+ if (options.into) {
848
+ const stale = publicSpecWarning(projectDir, bundle);
849
+ if (stale) bundle.warnings.unshift(stale);
850
+ }
650
851
  const rendered = renderMigrationFiles(bundle, {
651
852
  existingConfig: options.into ? readExistingConfig(projectDir) : void 0,
652
- existingComponentRegistry: existsSync3(projectPath(projectDir, "src/mdx/custom-components.tsx")) ? readFileSync3(projectPath(projectDir, "src/mdx/custom-components.tsx"), "utf8") : void 0
853
+ existingComponentRegistry: existsSync3(projectPath(projectDir, "src/mdx/custom-components.tsx")) ? readFileSync4(projectPath(projectDir, "src/mdx/custom-components.tsx"), "utf8") : void 0
653
854
  });
654
855
  for (const file of rendered) {
655
856
  const destination = projectPath(projectDir, file.path);
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readStarterReleaseManifest
4
- } from "./chunk-ND33X4NN.js";
4
+ } from "./chunk-BNVHCZWS.js";
5
5
  import {
6
6
  STABLE_SCAFFOLD_RELEASE
7
- } from "./chunk-I6IIZGQY.js";
7
+ } from "./chunk-MSCEHMMG.js";
8
8
 
9
9
  // src/download.ts
10
10
  import { Readable, Transform, pipeline } from "stream";
package/dist/index.js CHANGED
@@ -3,17 +3,17 @@ import {
3
3
  migrateDocs,
4
4
  parseFrontmatter,
5
5
  runCheck
6
- } from "./chunk-NNT7JTC2.js";
6
+ } from "./chunk-TPPRBG4B.js";
7
7
  import {
8
8
  logo,
9
9
  scaffold,
10
10
  slugify,
11
11
  success
12
- } from "./chunk-SLDENDOT.js";
12
+ } from "./chunk-KCBUOBEF.js";
13
13
  import "./chunk-XYRM4GAT.js";
14
- import "./chunk-UX57AKM4.js";
15
- import "./chunk-ND33X4NN.js";
16
- import "./chunk-I6IIZGQY.js";
14
+ import "./chunk-UBIX5ISG.js";
15
+ import "./chunk-BNVHCZWS.js";
16
+ import "./chunk-MSCEHMMG.js";
17
17
 
18
18
  // src/index.ts
19
19
  import { existsSync as existsSync2, readdirSync } from "fs";
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  migrateDocs
4
- } from "../chunk-NNT7JTC2.js";
5
- import "../chunk-SLDENDOT.js";
4
+ } from "../chunk-TPPRBG4B.js";
5
+ import "../chunk-KCBUOBEF.js";
6
6
  import "../chunk-XYRM4GAT.js";
7
- import "../chunk-UX57AKM4.js";
8
- import "../chunk-ND33X4NN.js";
9
- import "../chunk-I6IIZGQY.js";
7
+ import "../chunk-UBIX5ISG.js";
8
+ import "../chunk-BNVHCZWS.js";
9
+ import "../chunk-MSCEHMMG.js";
10
10
  export {
11
11
  migrateDocs
12
12
  };
package/dist/release.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  STABLE_SCAFFOLD_RELEASE,
4
4
  SUPPORTED_SCAFFOLD_RELEASES,
5
5
  isStableScaffoldRelease
6
- } from "./chunk-I6IIZGQY.js";
6
+ } from "./chunk-MSCEHMMG.js";
7
7
  export {
8
8
  STABLE_SCAFFOLD_RELEASE,
9
9
  SUPPORTED_SCAFFOLD_RELEASES,
package/dist/scaffold.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scaffold
4
- } from "./chunk-SLDENDOT.js";
4
+ } from "./chunk-KCBUOBEF.js";
5
5
  import "./chunk-XYRM4GAT.js";
6
6
  import {
7
7
  STARTER_ARCHIVE_ROOT,
8
8
  STARTER_COMMIT_SHA,
9
9
  STARTER_REPOSITORY,
10
10
  validateStarterArchiveEntry
11
- } from "./chunk-UX57AKM4.js";
12
- import "./chunk-ND33X4NN.js";
11
+ } from "./chunk-UBIX5ISG.js";
12
+ import "./chunk-BNVHCZWS.js";
13
13
  import {
14
14
  STABLE_SCAFFOLD_RELEASE
15
- } from "./chunk-I6IIZGQY.js";
15
+ } from "./chunk-MSCEHMMG.js";
16
16
  export {
17
17
  STABLE_SCAFFOLD_RELEASE,
18
18
  STARTER_ARCHIVE_ROOT,
@@ -8,8 +8,8 @@ import {
8
8
  planStarterRuntimeSync,
9
9
  readStarterReleaseManifest,
10
10
  starterManifestSha256
11
- } from "./chunk-ND33X4NN.js";
12
- import "./chunk-I6IIZGQY.js";
11
+ } from "./chunk-BNVHCZWS.js";
12
+ import "./chunk-MSCEHMMG.js";
13
13
  export {
14
14
  applyStarterRuntimeSyncPlan,
15
15
  classifyStarterPath,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  downloadStarter
4
- } from "./chunk-UX57AKM4.js";
4
+ } from "./chunk-UBIX5ISG.js";
5
5
  import {
6
6
  applyStarterRuntimeSyncPlan,
7
7
  mergeStarterOwnershipContracts,
@@ -9,11 +9,11 @@ import {
9
9
  planStarterRuntimeSync,
10
10
  readStarterReleaseManifest,
11
11
  starterManifestSha256
12
- } from "./chunk-ND33X4NN.js";
12
+ } from "./chunk-BNVHCZWS.js";
13
13
  import {
14
14
  STABLE_SCAFFOLD_RELEASE,
15
15
  SUPPORTED_SCAFFOLD_RELEASES
16
- } from "./chunk-I6IIZGQY.js";
16
+ } from "./chunk-MSCEHMMG.js";
17
17
 
18
18
  // src/starter-update.ts
19
19
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-thally-docs",
3
- "version": "0.10.62",
3
+ "version": "0.10.65",
4
4
  "description": "Scaffold an open-source Thally docs site for people, search engines, and AI tools.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,7 +45,7 @@
45
45
  "@anthropic-ai/sdk": "^0.36.0",
46
46
  "@inquirer/prompts": "^7.0.0",
47
47
  "@thallylabs/core": "0.2.9",
48
- "@thallylabs/migrate": "0.2.12",
48
+ "@thallylabs/migrate": "0.2.14",
49
49
  "p-limit": "^6.1.0",
50
50
  "tar": "^7.5.22",
51
51
  "yaml": "^2.6.0"