@rohal12/twee-ts 1.6.0 → 1.8.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.
@@ -151,6 +151,9 @@ function hasInfoTags(p) {
151
151
  function hasInfoName(p) {
152
152
  return INFO_PASSAGE_NAMES.has(p.name);
153
153
  }
154
+ function isInfoPassage(p) {
155
+ return hasInfoName(p) || hasInfoTags(p);
156
+ }
154
157
  function isStoryPassage(p) {
155
158
  return !hasInfoName(p) && !hasInfoTags(p);
156
159
  }
@@ -1841,7 +1844,7 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
1841
1844
  }
1842
1845
 
1843
1846
  // src/remote-formats.ts
1844
- import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
1847
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync4, rmSync } from "fs";
1845
1848
  import { join as join4 } from "path";
1846
1849
  import { homedir as homedir2 } from "os";
1847
1850
  var DEFAULT_SFA_INDICES = [
@@ -2072,6 +2075,75 @@ function discoverCachedFormats() {
2072
2075
  }
2073
2076
  return formats;
2074
2077
  }
2078
+ function listCachedFormats() {
2079
+ const cacheDir = getCacheDir();
2080
+ const entries = [];
2081
+ try {
2082
+ if (!existsSync(cacheDir)) return entries;
2083
+ } catch {
2084
+ return entries;
2085
+ }
2086
+ let names;
2087
+ try {
2088
+ names = readdirSync3(cacheDir);
2089
+ } catch {
2090
+ return entries;
2091
+ }
2092
+ for (const name of names) {
2093
+ const nameDir = join4(cacheDir, name);
2094
+ try {
2095
+ if (!statSync4(nameDir).isDirectory()) continue;
2096
+ } catch {
2097
+ continue;
2098
+ }
2099
+ let versions;
2100
+ try {
2101
+ versions = readdirSync3(nameDir);
2102
+ } catch {
2103
+ continue;
2104
+ }
2105
+ for (const version of versions) {
2106
+ const formatPath = join4(nameDir, version, "format.js");
2107
+ try {
2108
+ const stat = statSync4(formatPath);
2109
+ entries.push({
2110
+ name,
2111
+ version,
2112
+ sizeBytes: stat.size,
2113
+ modifiedAt: stat.mtime
2114
+ });
2115
+ } catch {
2116
+ }
2117
+ }
2118
+ }
2119
+ return entries;
2120
+ }
2121
+ function clearCachedFormats(name) {
2122
+ const cacheDir = getCacheDir();
2123
+ if (!existsSync(cacheDir)) return 0;
2124
+ if (!name) {
2125
+ const entries = listCachedFormats();
2126
+ const count2 = entries.length;
2127
+ rmSync(cacheDir, { recursive: true, force: true });
2128
+ return count2;
2129
+ }
2130
+ const nameDir = join4(cacheDir, name);
2131
+ if (!existsSync(nameDir)) return 0;
2132
+ let versions;
2133
+ try {
2134
+ versions = readdirSync3(nameDir);
2135
+ } catch {
2136
+ return 0;
2137
+ }
2138
+ const count = versions.length;
2139
+ rmSync(nameDir, { recursive: true, force: true });
2140
+ return count;
2141
+ }
2142
+ function getCacheSize() {
2143
+ const entries = listCachedFormats();
2144
+ const totalBytes = entries.reduce((sum, e) => sum + e.sizeBytes, 0);
2145
+ return { totalBytes, count: entries.length };
2146
+ }
2075
2147
 
2076
2148
  // src/compiler.ts
2077
2149
  var CREATOR_NAME3 = "Twee-ts";
@@ -2272,6 +2344,171 @@ function storyToJSON(story) {
2272
2344
  return JSON.stringify(obj, null, 2);
2273
2345
  }
2274
2346
 
2347
+ // src/inspect.ts
2348
+ function extractLinksFromText(text) {
2349
+ const targets = /* @__PURE__ */ new Set();
2350
+ const wikiLinkRe = /\[\[([^\]]+)\]\]/g;
2351
+ let m;
2352
+ while ((m = wikiLinkRe.exec(text)) !== null) {
2353
+ const content = m[1];
2354
+ const arrowIdx = content.indexOf("->");
2355
+ if (arrowIdx !== -1) {
2356
+ targets.add(content.slice(arrowIdx + 2).trim());
2357
+ continue;
2358
+ }
2359
+ const pipeIdx = content.indexOf("|");
2360
+ if (pipeIdx !== -1) {
2361
+ targets.add(content.slice(pipeIdx + 1).trim());
2362
+ continue;
2363
+ }
2364
+ targets.add(content.trim());
2365
+ }
2366
+ const gotoRe = /<<goto\s+["']([^"']+)["']\s*>>/g;
2367
+ while ((m = gotoRe.exec(text)) !== null) {
2368
+ targets.add(m[1]);
2369
+ }
2370
+ const linkMacroRe = /<<link\s+["'][^"']*["']\s+["']([^"']+)["']\s*>>/g;
2371
+ while ((m = linkMacroRe.exec(text)) !== null) {
2372
+ targets.add(m[1]);
2373
+ }
2374
+ return [...targets];
2375
+ }
2376
+ function storyInspect(story) {
2377
+ const allNames = new Set(story.passages.map((p) => p.name));
2378
+ const passages = [];
2379
+ const storyPassages = [];
2380
+ const infoPassages = [];
2381
+ const tagSet = /* @__PURE__ */ new Set();
2382
+ const passagesByTag = /* @__PURE__ */ new Map();
2383
+ const tagsByPassage = /* @__PURE__ */ new Map();
2384
+ const links = /* @__PURE__ */ new Map();
2385
+ for (const p of story.passages) {
2386
+ passages.push(p.name);
2387
+ if (isStoryPassage(p)) {
2388
+ storyPassages.push(p.name);
2389
+ }
2390
+ if (isInfoPassage(p)) {
2391
+ infoPassages.push(p.name);
2392
+ }
2393
+ tagsByPassage.set(p.name, [...p.tags]);
2394
+ for (const tag of p.tags) {
2395
+ tagSet.add(tag);
2396
+ let list = passagesByTag.get(tag);
2397
+ if (!list) {
2398
+ list = [];
2399
+ passagesByTag.set(tag, list);
2400
+ }
2401
+ list.push(p.name);
2402
+ }
2403
+ const targets = extractLinksFromText(p.text);
2404
+ links.set(p.name, targets);
2405
+ }
2406
+ const brokenLinks = [];
2407
+ for (const [from, targets] of links) {
2408
+ for (const to of targets) {
2409
+ if (!allNames.has(to)) {
2410
+ brokenLinks.push({ from, to });
2411
+ }
2412
+ }
2413
+ }
2414
+ const deadEnds = [];
2415
+ for (const name of storyPassages) {
2416
+ const targets = links.get(name);
2417
+ if (!targets || targets.length === 0) {
2418
+ deadEnds.push(name);
2419
+ }
2420
+ }
2421
+ const start = story.twine2.start || "Start";
2422
+ const linkedTo = /* @__PURE__ */ new Set();
2423
+ for (const targets of links.values()) {
2424
+ for (const t of targets) {
2425
+ linkedTo.add(t);
2426
+ }
2427
+ }
2428
+ const orphans = [];
2429
+ for (const name of storyPassages) {
2430
+ if (name !== start && !linkedTo.has(name)) {
2431
+ orphans.push(name);
2432
+ }
2433
+ }
2434
+ return {
2435
+ passages,
2436
+ storyPassages,
2437
+ infoPassages,
2438
+ tags: [...tagSet].sort(),
2439
+ passagesByTag,
2440
+ tagsByPassage,
2441
+ links,
2442
+ brokenLinks,
2443
+ deadEnds,
2444
+ orphans,
2445
+ start
2446
+ };
2447
+ }
2448
+
2449
+ // src/lint.ts
2450
+ async function lint(options) {
2451
+ const result = await compile({ ...options, outputMode: "json" });
2452
+ const map = storyInspect(result.story);
2453
+ return {
2454
+ diagnostics: result.diagnostics,
2455
+ stats: result.stats,
2456
+ formatName: result.story.twine2.format,
2457
+ formatVersion: result.story.twine2.formatVersion,
2458
+ start: map.start,
2459
+ passages: map.passages.length,
2460
+ storyPassages: map.storyPassages.length,
2461
+ infoPassages: map.infoPassages.length,
2462
+ brokenLinks: map.brokenLinks,
2463
+ deadEnds: map.deadEnds,
2464
+ orphans: map.orphans
2465
+ };
2466
+ }
2467
+ function formatLintReport(result) {
2468
+ const lines = [];
2469
+ const formatStr = result.formatName ? `${result.formatName}${result.formatVersion ? " " + result.formatVersion : ""}` : "unknown";
2470
+ lines.push(`Format: ${formatStr}`);
2471
+ lines.push(
2472
+ `Passages: ${result.passages} total (${result.storyPassages} story, ${result.infoPassages} info), ${result.stats.words.toLocaleString()} words, ${result.stats.files.length} files`
2473
+ );
2474
+ lines.push(`Start: ${result.start}`);
2475
+ if (result.brokenLinks.length > 0) {
2476
+ lines.push("");
2477
+ lines.push(`Broken links (${result.brokenLinks.length}):`);
2478
+ for (const link of result.brokenLinks) {
2479
+ lines.push(` ${link.from} -> ${link.to} (passage "${link.to}" does not exist)`);
2480
+ }
2481
+ }
2482
+ if (result.deadEnds.length > 0) {
2483
+ lines.push("");
2484
+ lines.push(`Dead ends (${result.deadEnds.length}): ${result.deadEnds.join(", ")}`);
2485
+ }
2486
+ if (result.orphans.length > 0) {
2487
+ lines.push("");
2488
+ lines.push(`Orphans (${result.orphans.length}): ${result.orphans.join(", ")}`);
2489
+ }
2490
+ const errors = result.diagnostics.filter((d) => d.level === "error");
2491
+ const warnings = result.diagnostics.filter((d) => d.level === "warning");
2492
+ if (errors.length > 0 || warnings.length > 0) {
2493
+ lines.push("");
2494
+ lines.push(`Diagnostics: ${errors.length} error(s), ${warnings.length} warning(s)`);
2495
+ for (const d of errors) {
2496
+ lines.push(` error: ${d.message}`);
2497
+ }
2498
+ for (const d of warnings) {
2499
+ lines.push(` warning: ${d.message}`);
2500
+ }
2501
+ }
2502
+ lines.push("");
2503
+ const hasErrors = errors.length > 0 || result.brokenLinks.length > 0;
2504
+ if (hasErrors) {
2505
+ lines.push("Lint failed.");
2506
+ } else {
2507
+ lines.push("Lint passed.");
2508
+ }
2509
+ return lines.join("\n");
2510
+ }
2511
+
2275
2512
  // src/config.ts
2276
2513
  import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
2277
2514
  import { join as join5 } from "path";
@@ -2388,6 +2625,7 @@ var { values, positionals } = parseArgs({
2388
2625
  "archive-twine1": { type: "boolean" },
2389
2626
  "twee2-compat": { type: "boolean" },
2390
2627
  "no-trim": { type: "boolean" },
2628
+ lint: { type: "boolean" },
2391
2629
  test: { type: "boolean", short: "t" },
2392
2630
  watch: { type: "boolean", short: "w" },
2393
2631
  "log-stats": { type: "boolean", short: "l" },
@@ -2424,6 +2662,10 @@ async function main() {
2424
2662
  runInit();
2425
2663
  return;
2426
2664
  }
2665
+ if (positionals[0] === "cache") {
2666
+ runCache(positionals.slice(1));
2667
+ return;
2668
+ }
2427
2669
  let config = null;
2428
2670
  if (!values["no-config"]) {
2429
2671
  if (values.config) {
@@ -2456,6 +2698,27 @@ async function main() {
2456
2698
  tagAliases[pair.slice(0, eq)] = pair.slice(eq + 1);
2457
2699
  }
2458
2700
  }
2701
+ if (values.lint) {
2702
+ const lintResult = await lint({
2703
+ sources,
2704
+ formatId: values.format ?? config?.formatId,
2705
+ startPassage: values.start ?? config?.startPassage,
2706
+ formatPaths: config?.formatPaths,
2707
+ modules: values.module ?? config?.modules,
2708
+ headFile: values.head ?? config?.headFile,
2709
+ trim: values["no-trim"] ? false : config?.trim ?? true,
2710
+ twee2Compat: values["twee2-compat"] ?? config?.twee2Compat ?? false,
2711
+ testMode: values.test ?? config?.testMode ?? false,
2712
+ formatIndices: values["format-index"] ?? config?.formatIndices,
2713
+ formatUrls: values["format-url"] ?? config?.formatUrls,
2714
+ noRemote: values["no-remote"] ?? config?.noRemote ?? false,
2715
+ tagAliases,
2716
+ sourceInfo: values["source-info"] ?? config?.sourceInfo ?? false
2717
+ });
2718
+ console.log(formatLintReport(lintResult));
2719
+ const hasErrors = lintResult.brokenLinks.length > 0 || lintResult.diagnostics.some((d) => d.level === "error");
2720
+ process.exit(hasErrors ? 1 : 0);
2721
+ }
2459
2722
  const compileOptions = {
2460
2723
  sources,
2461
2724
  outputMode,
@@ -2571,6 +2834,63 @@ This is the starting passage. Edit this file to begin writing your story.
2571
2834
  console.log(" src/Start.tw");
2572
2835
  console.log("\nRun: npx @rohal12/twee-ts");
2573
2836
  }
2837
+ function formatBytes(bytes) {
2838
+ if (bytes < 1024) return `${bytes}B`;
2839
+ const kb = bytes / 1024;
2840
+ if (kb < 1024) return `${Math.round(kb)}K`;
2841
+ const mb = kb / 1024;
2842
+ return `${mb.toFixed(1)}M`;
2843
+ }
2844
+ function runCache(args) {
2845
+ const subcommand = args[0];
2846
+ switch (subcommand) {
2847
+ case "list": {
2848
+ const entries = listCachedFormats();
2849
+ if (entries.length === 0) {
2850
+ console.log("No cached formats.");
2851
+ return;
2852
+ }
2853
+ for (const e of entries) {
2854
+ const date = e.modifiedAt.toISOString().slice(0, 10);
2855
+ console.log(`${e.name.padEnd(16)} ${e.version.padEnd(10)} ${formatBytes(e.sizeBytes).padStart(6)} ${date}`);
2856
+ }
2857
+ return;
2858
+ }
2859
+ case "clear": {
2860
+ const name = args[1];
2861
+ const count = clearCachedFormats(name);
2862
+ if (count === 0) {
2863
+ console.log(name ? `No cached formats matching "${name}".` : "Cache is already empty.");
2864
+ } else {
2865
+ console.log(`Cleared ${count} cached format${count === 1 ? "" : "s"}.`);
2866
+ }
2867
+ return;
2868
+ }
2869
+ case "size": {
2870
+ const { totalBytes, count } = getCacheSize();
2871
+ if (count === 0) {
2872
+ console.log("Cache is empty.");
2873
+ } else {
2874
+ console.log(`Total: ${formatBytes(totalBytes)} (${count} format${count === 1 ? "" : "s"})`);
2875
+ }
2876
+ return;
2877
+ }
2878
+ case "path": {
2879
+ console.log(getCacheDir());
2880
+ return;
2881
+ }
2882
+ default: {
2883
+ console.error(`Usage: twee-ts cache <list|clear|size|path>
2884
+
2885
+ list List cached formats with name, version, size
2886
+ clear Delete all cached formats
2887
+ clear <name> Delete cached formats matching name
2888
+ size Show total cache size
2889
+ path Print cache directory path`);
2890
+ process.exit(subcommand ? 1 : 0);
2891
+ }
2892
+ }
2893
+ }
2574
2894
  function printUsage() {
2575
2895
  console.log(`twee-ts v${VERSION} \u2014 TypeScript Twee-to-HTML compiler
2576
2896
 
@@ -2588,6 +2908,7 @@ Options:
2588
2908
  --archive-twine1 Output as Twine 1 archive
2589
2909
  --json Output as JSON
2590
2910
  --twee2-compat Enable Twee2 syntax compatibility
2911
+ --lint Lint story structure (broken links, dead ends, orphans)
2591
2912
  --no-trim Don't trim passage whitespace
2592
2913
  -t, --test Enable test/debug mode
2593
2914
  -w, --watch Watch for changes and rebuild
@@ -2603,7 +2924,13 @@ Options:
2603
2924
  -c, --config <file> Config file path (default: ${CONFIG_FILENAME})
2604
2925
  --no-config Skip config file loading
2605
2926
  -h, --help Show this help
2606
- -v, --version Show version`);
2927
+ -v, --version Show version
2928
+
2929
+ Subcommands:
2930
+ cache list List cached remote formats
2931
+ cache clear [name] Clear cached formats (all or by name)
2932
+ cache size Show total cache size
2933
+ cache path Print cache directory path`);
2607
2934
  }
2608
2935
  main().catch((err) => {
2609
2936
  console.error(err instanceof Error ? err.message : String(err));