@twango/twiki 0.0.0-stage → 0.1.0-rc.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.
Files changed (225) hide show
  1. package/.github/actions/product-cache/action.yml +32 -0
  2. package/.github/actions/product-cache/index.mjs +450 -0
  3. package/LICENSE +21 -0
  4. package/README.md +146 -2
  5. package/bun.lock +1701 -0
  6. package/cli/runtime.test.ts +420 -0
  7. package/cli/runtime.ts +385 -0
  8. package/cli/twiki.ts +305 -0
  9. package/package.json +138 -4
  10. package/site/_headers +7 -0
  11. package/site/assets/fonts/OFL.txt +93 -0
  12. package/site/assets/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
  13. package/site/assets/social-background.svg +38 -0
  14. package/site/bun.lock +1701 -0
  15. package/site/database/index.ts +7 -0
  16. package/site/database/schema.ts +95 -0
  17. package/site/drizzle.config.ts +7 -0
  18. package/site/migrations/0001_auth.sql +14 -0
  19. package/site/migrations/0002_site_access.sql +5 -0
  20. package/site/migrations/0003_github_username.sql +1 -0
  21. package/site/migrations/meta/0002_snapshot.json +432 -0
  22. package/site/migrations/meta/0003_snapshot.json +439 -0
  23. package/site/migrations/meta/_journal.json +27 -0
  24. package/site/package.json +120 -0
  25. package/site/src/app.d.ts +14 -0
  26. package/site/src/app.html +11 -0
  27. package/site/src/hooks.server.ts +12 -0
  28. package/site/src/lib/analytics-config.json +1 -0
  29. package/site/src/lib/analytics.ts +40 -0
  30. package/site/src/lib/auth-config.json +3 -0
  31. package/site/src/lib/components/SEO.svelte +113 -0
  32. package/site/src/lib/components/doc/AccessLink.svelte +27 -0
  33. package/site/src/lib/components/doc/Backlinks.svelte +59 -0
  34. package/site/src/lib/components/doc/Breadcrumbs.svelte +29 -0
  35. package/site/src/lib/components/doc/ContentMeta.svelte +61 -0
  36. package/site/src/lib/components/doc/DocPager.svelte +91 -0
  37. package/site/src/lib/components/doc/DocShell.svelte +115 -0
  38. package/site/src/lib/components/doc/LicenseNotice.svelte +26 -0
  39. package/site/src/lib/components/doc/LockedContent.svelte +29 -0
  40. package/site/src/lib/components/doc/NavTree.svelte +158 -0
  41. package/site/src/lib/components/doc/NoteActions.svelte +139 -0
  42. package/site/src/lib/components/doc/PublicationBadge.svelte +49 -0
  43. package/site/src/lib/components/doc/ReaderModeToggle.svelte +19 -0
  44. package/site/src/lib/components/doc/Sidebar.svelte +351 -0
  45. package/site/src/lib/components/doc/Toc.svelte +339 -0
  46. package/site/src/lib/components/doc/enhancements.ts +146 -0
  47. package/site/src/lib/components/doc/heading-tracker.svelte.ts +39 -0
  48. package/site/src/lib/components/doc/image-preview.ts +114 -0
  49. package/site/src/lib/components/doc/reader-mode.svelte.ts +40 -0
  50. package/site/src/lib/components/embeds/mermaid.ts +353 -0
  51. package/site/src/lib/components/files/FileCode.svelte +81 -0
  52. package/site/src/lib/components/files/FileExplorer.svelte +74 -0
  53. package/site/src/lib/components/files/FileHistory.svelte +170 -0
  54. package/site/src/lib/components/files/FileIcon.svelte +28 -0
  55. package/site/src/lib/components/files/FileRmd.svelte +209 -0
  56. package/site/src/lib/components/files/FileTabs.svelte +180 -0
  57. package/site/src/lib/components/files/FileTree.svelte +67 -0
  58. package/site/src/lib/components/files/blame-gutter.ts +45 -0
  59. package/site/src/lib/components/files/code-view.ts +98 -0
  60. package/site/src/lib/components/files/file-workspace.svelte.ts +100 -0
  61. package/site/src/lib/components/graph/GraphPanel.svelte +275 -0
  62. package/site/src/lib/components/graph/GraphView.svelte +100 -0
  63. package/site/src/lib/components/graph/graph-data.ts +137 -0
  64. package/site/src/lib/components/graph/graph-model.ts +103 -0
  65. package/site/src/lib/components/graph/index.ts +3 -0
  66. package/site/src/lib/components/graph/render-graph.ts +626 -0
  67. package/site/src/lib/components/popover/index.ts +1 -0
  68. package/site/src/lib/components/popover/link-popovers.ts +233 -0
  69. package/site/src/lib/components/popover/popover.css +82 -0
  70. package/site/src/lib/components/search/SearchButton.svelte +57 -0
  71. package/site/src/lib/components/search/SearchPalette.svelte +189 -0
  72. package/site/src/lib/components/search/index.ts +3 -0
  73. package/site/src/lib/components/search/pagefind-client.svelte.ts +129 -0
  74. package/site/src/lib/components/search/pagefind-query.ts +22 -0
  75. package/site/src/lib/components/search/pagefind-url.ts +12 -0
  76. package/site/src/lib/components/search/search-state.svelte.ts +22 -0
  77. package/site/src/lib/components/ui/IconButton.svelte +30 -0
  78. package/site/src/lib/components/ui/ThemeToggle.svelte +22 -0
  79. package/site/src/lib/components/ui/index.ts +1 -0
  80. package/site/src/lib/config.ts +9 -0
  81. package/site/src/lib/content-license.ts +7 -0
  82. package/site/src/lib/file-history.ts +37 -0
  83. package/site/src/lib/file-icons.ts +27 -0
  84. package/site/src/lib/file-tabs.ts +72 -0
  85. package/site/src/lib/files.ts +73 -0
  86. package/site/src/lib/github-user.ts +70 -0
  87. package/site/src/lib/metadata.ts +166 -0
  88. package/site/src/lib/notebook-nav.ts +63 -0
  89. package/site/src/lib/publication.ts +1 -0
  90. package/site/src/lib/rmd.ts +156 -0
  91. package/site/src/lib/server/access-admin.ts +33 -0
  92. package/site/src/lib/server/content-model.ts +253 -0
  93. package/site/src/lib/server/content-provider.ts +266 -0
  94. package/site/src/lib/server/content.ts +277 -0
  95. package/site/src/lib/server/runtime-content.ts +34 -0
  96. package/site/src/lib/site-config.json +9 -0
  97. package/site/src/lib/social-image.ts +13 -0
  98. package/site/src/lib/styles/article.css +607 -0
  99. package/site/src/lib/styles/foundation.css +138 -0
  100. package/site/src/lib/styles/print.css +115 -0
  101. package/site/src/lib/styles/shell.css +182 -0
  102. package/site/src/lib/text-exports.ts +5 -0
  103. package/site/src/lib/types.ts +140 -0
  104. package/site/src/lib/utils.ts +6 -0
  105. package/site/src/routes/+error.svelte +16 -0
  106. package/site/src/routes/+layout.server.ts +10 -0
  107. package/site/src/routes/+layout.svelte +72 -0
  108. package/site/src/routes/+layout.ts +3 -0
  109. package/site/src/routes/404/+page.svelte +13 -0
  110. package/site/src/routes/[...slug]/+page.server.ts +24 -0
  111. package/site/src/routes/[...slug]/+page.svelte +180 -0
  112. package/site/src/routes/[course]/files/[...file]/+page.svelte +373 -0
  113. package/site/src/routes/[course]/files/[...file]/+page.ts +38 -0
  114. package/site/src/routes/admin/access/+page.server.ts +53 -0
  115. package/site/src/routes/admin/access/+page.svelte +118 -0
  116. package/site/src/routes/graph.json/+server.ts +7 -0
  117. package/site/src/routes/index.xml/+server.ts +7 -0
  118. package/site/src/routes/layout.css +10 -0
  119. package/site/src/routes/robots.txt/+server.ts +9 -0
  120. package/site/src/routes/sitemap.xml/+server.ts +7 -0
  121. package/site/src/routes/tags/+page.server.ts +5 -0
  122. package/site/src/routes/tags/+page.svelte +38 -0
  123. package/site/src/routes/tags/[...tag]/+page.server.ts +22 -0
  124. package/site/src/routes/tags/[...tag]/+page.svelte +52 -0
  125. package/site/static/favicon.png +0 -0
  126. package/site/static/fonts/JetBrainsMono-Italic-VF.woff2 +0 -0
  127. package/site/static/fonts/JetBrainsMono-VF.woff2 +0 -0
  128. package/site/static/fonts/OverusedGrotesk-VF.woff2 +0 -0
  129. package/site/static/fonts/OverusedGroteskItalic-VF.woff2 +0 -0
  130. package/site/static/fonts/Virgil.woff2 +0 -0
  131. package/site/svelte.config.js +71 -0
  132. package/site/tooling/LICENSE-quartz +25 -0
  133. package/site/tooling/adapter.js +55 -0
  134. package/site/tooling/assets.ts +323 -0
  135. package/site/tooling/benchmark.ts +191 -0
  136. package/site/tooling/build.ts +407 -0
  137. package/site/tooling/cache.ts +133 -0
  138. package/site/tooling/check-diagrams.ts +164 -0
  139. package/site/tooling/check-links.ts +86 -0
  140. package/site/tooling/check-targets.ts +139 -0
  141. package/site/tooling/compiler.ts +1386 -0
  142. package/site/tooling/deploy.ts +112 -0
  143. package/site/tooling/fingerprint.ts +81 -0
  144. package/site/tooling/lib/app-version.js +92 -0
  145. package/site/tooling/lib/application-asset-urls.ts +27 -0
  146. package/site/tooling/lib/application-cache.ts +269 -0
  147. package/site/tooling/lib/asset-hash-cache.ts +116 -0
  148. package/site/tooling/lib/autotag.ts +99 -0
  149. package/site/tooling/lib/build-assembly.ts +148 -0
  150. package/site/tooling/lib/build-workspace.ts +386 -0
  151. package/site/tooling/lib/cache-selection.ts +70 -0
  152. package/site/tooling/lib/cloudflare-prewarm.js +40 -0
  153. package/site/tooling/lib/comments.ts +32 -0
  154. package/site/tooling/lib/content-cache.ts +244 -0
  155. package/site/tooling/lib/content-urls.ts +50 -0
  156. package/site/tooling/lib/course-cache.ts +298 -0
  157. package/site/tooling/lib/course-files.ts +281 -0
  158. package/site/tooling/lib/currency.ts +33 -0
  159. package/site/tooling/lib/deployment-config.ts +222 -0
  160. package/site/tooling/lib/deployment-manifest.ts +274 -0
  161. package/site/tooling/lib/deployment-prepare.ts +325 -0
  162. package/site/tooling/lib/deployment-upload.ts +293 -0
  163. package/site/tooling/lib/dev-git-state.ts +130 -0
  164. package/site/tooling/lib/dev-history.ts +86 -0
  165. package/site/tooling/lib/dev-social-images.ts +38 -0
  166. package/site/tooling/lib/dev-state.ts +107 -0
  167. package/site/tooling/lib/edition-builds.ts +184 -0
  168. package/site/tooling/lib/edition-paths.js +29 -0
  169. package/site/tooling/lib/empty-tag-route.js +23 -0
  170. package/site/tooling/lib/file-history.ts +201 -0
  171. package/site/tooling/lib/file-icons.ts +93 -0
  172. package/site/tooling/lib/file-policy.ts +38 -0
  173. package/site/tooling/lib/file-search.js +13 -0
  174. package/site/tooling/lib/git-revisions.ts +41 -0
  175. package/site/tooling/lib/history-pool.ts +74 -0
  176. package/site/tooling/lib/history-worker.ts +22 -0
  177. package/site/tooling/lib/knit-rmd.R +38 -0
  178. package/site/tooling/lib/lastmod.ts +201 -0
  179. package/site/tooling/lib/markdown-files.ts +51 -0
  180. package/site/tooling/lib/ofm.ts +634 -0
  181. package/site/tooling/lib/output.ts +60 -0
  182. package/site/tooling/lib/product-cache.ts +1006 -0
  183. package/site/tooling/lib/project-config.ts +288 -0
  184. package/site/tooling/lib/public-assets.ts +38 -0
  185. package/site/tooling/lib/public-links.ts +56 -0
  186. package/site/tooling/lib/publishing.ts +156 -0
  187. package/site/tooling/lib/r-runtime.ts +257 -0
  188. package/site/tooling/lib/rebuild-queue.ts +25 -0
  189. package/site/tooling/lib/rmd-preflight.ts +166 -0
  190. package/site/tooling/lib/rmd-preview-cache.ts +93 -0
  191. package/site/tooling/lib/rmd-previews.ts +129 -0
  192. package/site/tooling/lib/runtime-data-modules.ts +30 -0
  193. package/site/tooling/lib/runtime-packaging.ts +321 -0
  194. package/site/tooling/lib/search-cache.js +165 -0
  195. package/site/tooling/lib/search-inputs.js +129 -0
  196. package/site/tooling/lib/slug.ts +377 -0
  197. package/site/tooling/lib/social-images.ts +210 -0
  198. package/site/tooling/lib/text-exports.ts +51 -0
  199. package/site/tooling/lib/visit-elements.ts +17 -0
  200. package/site/tooling/live-test.ts +138 -0
  201. package/site/tooling/pipeline.ts +103 -0
  202. package/site/tooling/prepare.ts +3 -0
  203. package/site/tooling/publish.ts +28 -0
  204. package/site/tooling/r-cache.ts +45 -0
  205. package/site/tooling/r-render-profile.json +7 -0
  206. package/site/tooling/search.js +94 -0
  207. package/site/tooling/verify-deployment.ts +109 -0
  208. package/site/tooling/vite-content.ts +240 -0
  209. package/site/tooling/wrangler-paths.js +25 -0
  210. package/site/tooling/wrangler.ts +19 -0
  211. package/site/tsconfig.json +16 -0
  212. package/site/vite.config.ts +40 -0
  213. package/site/worker/access.ts +57 -0
  214. package/site/worker/analytics.ts +100 -0
  215. package/site/worker/auth-routes.ts +78 -0
  216. package/site/worker/auth.ts +78 -0
  217. package/site/worker/content-runtime.ts +226 -0
  218. package/site/worker/framing.ts +4 -0
  219. package/site/worker/gate.ts +147 -0
  220. package/site/worker/index.ts +9 -0
  221. package/site/worker/login.ts +62 -0
  222. package/site/worker/publication.ts +44 -0
  223. package/site/worker/runtime.ts +104 -0
  224. package/site/worker/tsconfig.json +13 -0
  225. package/site/wrangler.jsonc +37 -0
@@ -0,0 +1,32 @@
1
+ name: Encrypted build products
2
+ description: Restore or save independently verified application, course and search products
3
+ inputs:
4
+ mode:
5
+ description: restore or save
6
+ required: true
7
+ namespace:
8
+ description: Production caches, or isolated caches for trusted manual benchmarks
9
+ default: production
10
+ transport:
11
+ description: Individual group caches, or one bundle of independently encrypted groups
12
+ default: groups
13
+ outputs:
14
+ requested:
15
+ description: Product groups requested
16
+ hits:
17
+ description: Encrypted archive cache hits before product verification
18
+ misses:
19
+ description: Encrypted archive cache misses
20
+ restored:
21
+ description: Product groups verified and restored
22
+ rebuild_needed:
23
+ description: Requested groups unavailable for reuse
24
+ published:
25
+ description: Verified product archives published
26
+ skipped:
27
+ description: Save operations skipped or already cached
28
+ failed:
29
+ description: Optional cache operations that failed
30
+ runs:
31
+ using: node24
32
+ main: index.mjs
@@ -0,0 +1,450 @@
1
+ import { spawn } from "node:child_process";
2
+ import {
3
+ appendFileSync,
4
+ copyFileSync,
5
+ existsSync,
6
+ lstatSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ mkdirSync,
11
+ rmSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ // A JavaScript action receives the Actions cache service credentials. Do not
18
+ // export those credentials into a workflow log or a repository artifact.
19
+ const site = process.env.TWIKI_SITE_DIR
20
+ ? path.resolve(process.env.TWIKI_SITE_DIR)
21
+ : fileURLToPath(new URL("../../../site/", import.meta.url));
22
+ if (
23
+ process.env.TWIKI_SITE_DIR &&
24
+ (!path.isAbsolute(process.env.TWIKI_SITE_DIR) ||
25
+ !lstatSync(site).isDirectory() ||
26
+ realpathSync(site) !== site)
27
+ )
28
+ throw new Error(
29
+ "Product cache requires a real absolute engine working directory",
30
+ );
31
+ const mode = process.env.INPUT_MODE;
32
+ if (!["restore", "save"].includes(mode))
33
+ throw new Error("Expected restore or save mode");
34
+ const namespace = process.env.INPUT_NAMESPACE || "production";
35
+ if (!["production", "benchmark"].includes(namespace))
36
+ throw new Error("Unsupported product cache namespace");
37
+ const transport = process.env.INPUT_TRANSPORT || "groups";
38
+ if (!["groups", "bundle"].includes(transport))
39
+ throw new Error("Unsupported product cache transport");
40
+ if (
41
+ transport === "bundle" &&
42
+ !/^[a-f0-9]{40}$/.test(process.env.GITHUB_SHA ?? "")
43
+ )
44
+ throw new Error("Bundle caches require an exact Git commit");
45
+ if (
46
+ namespace === "benchmark" &&
47
+ (process.env.GITHUB_REF !== "refs/heads/perf/build-performance" ||
48
+ process.env.GITHUB_EVENT_NAME !== "workflow_dispatch")
49
+ )
50
+ throw new Error(
51
+ "Benchmark caches require an explicit trusted manual branch run",
52
+ );
53
+ if (
54
+ mode === "save" &&
55
+ namespace === "production" &&
56
+ (process.env.GITHUB_REF !== "refs/heads/main" ||
57
+ !["push", "workflow_dispatch"].includes(process.env.GITHUB_EVENT_NAME))
58
+ )
59
+ throw new Error(
60
+ "Only trusted main builds may publish production product caches",
61
+ );
62
+ const prefix =
63
+ namespace === "benchmark"
64
+ ? "wisconsin-products-benchmark-v1-"
65
+ : "wisconsin-products-v1-";
66
+ const counts = {
67
+ requested: 0,
68
+ hits: 0,
69
+ misses: 0,
70
+ restored: 0,
71
+ rebuild_needed: 0,
72
+ published: 0,
73
+ skipped: 0,
74
+ failed: 0,
75
+ };
76
+ function groupDiagnostic(group, ready, reason) {
77
+ console.log(
78
+ "Encrypted product cache group: " +
79
+ JSON.stringify({ mode, namespace, group: group.id, ready, reason }),
80
+ );
81
+ }
82
+ if (!process.env.BUILD_CACHE_KEY) {
83
+ console.log(
84
+ "Encrypted product cache unavailable; using the normal build path",
85
+ );
86
+ } else {
87
+ // Standard package resolution also supports hoisted consumer dependencies.
88
+ let sdk;
89
+ try {
90
+ sdk = import.meta.resolve("@actions/cache");
91
+ } catch (error) {
92
+ if (error.code !== "ERR_MODULE_NOT_FOUND") throw error;
93
+ // Legacy engine development and isolated test fixtures keep site dependencies.
94
+ sdk = new URL(
95
+ "../../../site/node_modules/@actions/cache/lib/cache.js",
96
+ import.meta.url,
97
+ );
98
+ }
99
+ const { restoreCache, saveCache, isFeatureAvailable } = await import(sdk);
100
+ if (!isFeatureAvailable())
101
+ throw new Error("Actions cache service is unavailable");
102
+ mkdirSync(path.join(site, "build/generated/product-caches"), {
103
+ recursive: true,
104
+ });
105
+ const products = path.join(site, "build/generated/product-caches");
106
+ const bundleDirectory = path.join(products, "bundles", namespace);
107
+ let runtime;
108
+ let bundleLoadPromise;
109
+ let bundleAvailable = false;
110
+ let bundleGroups = new Set();
111
+ const selectedBundleGroups = new Set();
112
+ const bundleGroupId =
113
+ /^(?:application|(?:global|files-global|search)-(?:public|full)|(?:course|course-files)-(?:public|full)-[a-f0-9]{16})$/;
114
+ function regular(file) {
115
+ return lstatSync(file).isFile() && realpathSync(file) === file;
116
+ }
117
+ function cleanBundleDirectory() {
118
+ if (realpathSync(products) !== products)
119
+ throw new Error("Unsafe product cache root");
120
+ const parent = path.dirname(bundleDirectory);
121
+ mkdirSync(parent, { recursive: true });
122
+ if (realpathSync(parent) !== parent)
123
+ throw new Error("Unsafe bundle cache directory");
124
+ if (
125
+ lstatSync(bundleDirectory, { throwIfNoEntry: false }) &&
126
+ (!lstatSync(bundleDirectory).isDirectory() ||
127
+ realpathSync(bundleDirectory) !== bundleDirectory)
128
+ )
129
+ throw new Error("Unsafe bundle cache directory");
130
+ rmSync(bundleDirectory, { recursive: true, force: true });
131
+ mkdirSync(bundleDirectory);
132
+ }
133
+ function bundleKey() {
134
+ return `${prefix}bundle-${runtime}-${process.env.GITHUB_SHA}`;
135
+ }
136
+ async function loadBundle() {
137
+ if (!bundleLoadPromise) bundleLoadPromise = loadBundleOnce();
138
+ await bundleLoadPromise;
139
+ }
140
+ async function loadBundleOnce() {
141
+ try {
142
+ cleanBundleDirectory();
143
+ const hit = await restoreCache([bundleDirectory], bundleKey(), [
144
+ `${prefix}bundle-${runtime}-`,
145
+ ]);
146
+ if (!hit) return;
147
+ if (
148
+ !lstatSync(bundleDirectory).isDirectory() ||
149
+ realpathSync(bundleDirectory) !== bundleDirectory
150
+ )
151
+ throw new Error("Unsafe restored bundle directory");
152
+ const manifestFile = path.join(bundleDirectory, "bundle.json");
153
+ if (!regular(manifestFile) || lstatSync(manifestFile).size > 256 * 1024)
154
+ throw new Error("Invalid bundle metadata");
155
+ const manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
156
+ if (
157
+ manifest.schema !== 1 ||
158
+ manifest.namespace !== namespace ||
159
+ manifest.runtime !== runtime ||
160
+ !Array.isArray(manifest.groups) ||
161
+ manifest.groups.length > 4096 ||
162
+ !manifest.groups.every(
163
+ (id) => typeof id === "string" && bundleGroupId.test(id),
164
+ ) ||
165
+ new Set(manifest.groups).size !== manifest.groups.length
166
+ )
167
+ throw new Error("Invalid bundle metadata");
168
+ bundleGroups = new Set(manifest.groups);
169
+ for (const name of readdirSync(bundleDirectory)) {
170
+ const file = path.join(bundleDirectory, name);
171
+ if (
172
+ !regular(file) ||
173
+ (name !== "bundle.json" &&
174
+ (!name.endsWith(".gpg") || !bundleGroups.has(name.slice(0, -4))))
175
+ )
176
+ throw new Error("Unexpected bundle entry");
177
+ }
178
+ bundleAvailable = true;
179
+ } catch {
180
+ counts.failed++;
181
+ console.log(
182
+ "Optional encrypted bundle restore rejected; consumers will rebuild",
183
+ );
184
+ }
185
+ }
186
+ async function archiveHit(group, archive) {
187
+ if (transport === "groups")
188
+ return !!(await restoreCache(
189
+ [archive],
190
+ group.key,
191
+ group.restoreKeys ?? [],
192
+ ));
193
+ await loadBundle();
194
+ selectedBundleGroups.add(group.id);
195
+ const source = path.join(bundleDirectory, `${group.id}.gpg`);
196
+ if (!bundleAvailable || !bundleGroups.has(group.id) || !existsSync(source))
197
+ return false;
198
+ if (!regular(source)) throw new Error("Unsafe bundle archive");
199
+ mkdirSync(path.dirname(archive), { recursive: true });
200
+ if (
201
+ realpathSync(path.dirname(archive)) !== path.dirname(archive) ||
202
+ (lstatSync(archive, { throwIfNoEntry: false }) && !regular(archive))
203
+ )
204
+ throw new Error("Unsafe product archive destination");
205
+ copyFileSync(source, archive);
206
+ return true;
207
+ }
208
+ const planFile = "build/generated/product-caches/action-plan.json";
209
+ function run(args) {
210
+ return new Promise((resolve, reject) => {
211
+ const child = spawn("bun", ["tooling/cache.ts", ...args], {
212
+ cwd: site,
213
+ env: process.env,
214
+ stdio: "inherit",
215
+ });
216
+ child.once("error", reject);
217
+ child.once("close", (code) =>
218
+ code === 0
219
+ ? resolve()
220
+ : reject(new Error("Build product cache command failed")),
221
+ );
222
+ });
223
+ }
224
+ async function limited(groups, operation) {
225
+ let next = 0;
226
+ await Promise.all(
227
+ Array.from({ length: Math.min(4, groups.length) }, async () => {
228
+ while (next < groups.length) await operation(groups[next++]);
229
+ }),
230
+ );
231
+ }
232
+ async function plan(kind) {
233
+ await run([
234
+ "plan",
235
+ "--namespace",
236
+ namespace,
237
+ ...(mode === "restore" ? ["--restore"] : []),
238
+ ...(kind ? ["--kind", kind] : []),
239
+ "--plan-file",
240
+ planFile,
241
+ ]);
242
+ const value = JSON.parse(readFileSync(path.join(site, planFile), "utf8"));
243
+ if (
244
+ value.schema !== 1 ||
245
+ value.namespace !== namespace ||
246
+ !Array.isArray(value.groups)
247
+ )
248
+ throw new Error("Invalid product cache plan");
249
+ if (transport === "bundle") {
250
+ if (
251
+ !/^[a-f0-9]{64}$/.test(value.runtime ?? "") ||
252
+ (runtime && runtime !== value.runtime)
253
+ )
254
+ throw new Error("Invalid bundle runtime identity");
255
+ runtime = value.runtime;
256
+ }
257
+ for (const group of value.groups) {
258
+ if (
259
+ typeof group.id !== "string" ||
260
+ !/^[a-z0-9-]+$/.test(group.id) ||
261
+ group.namespace !== namespace ||
262
+ typeof group.key !== "string" ||
263
+ !group.key.startsWith(prefix) ||
264
+ !Array.isArray(group.restoreKeys ?? []) ||
265
+ !(group.restoreKeys ?? []).every(
266
+ (key) => typeof key === "string" && key.startsWith(prefix),
267
+ )
268
+ )
269
+ throw new Error("Product cache plan crosses its selected namespace");
270
+ if (transport === "bundle" && !bundleGroupId.test(group.id))
271
+ throw new Error("Unsupported bundle group");
272
+ const expectedArchive = path.join(
273
+ site,
274
+ "build/generated/product-caches",
275
+ ...(namespace === "benchmark" ? ["benchmark"] : []),
276
+ `${group.id}.gpg`,
277
+ );
278
+ if (
279
+ typeof group.archive !== "string" ||
280
+ path.resolve(site, group.archive) !== expectedArchive
281
+ )
282
+ throw new Error("Product archive crosses its selected namespace");
283
+ }
284
+ counts.requested += value.groups.length;
285
+ console.log(
286
+ "Encrypted product cache plan: " +
287
+ JSON.stringify({
288
+ mode,
289
+ namespace,
290
+ kind: kind ?? "all",
291
+ groups: value.groups.length,
292
+ }),
293
+ );
294
+ return value.groups;
295
+ }
296
+ async function transfer(group, operation) {
297
+ const transferFile = `build/generated/product-caches/${operation}-${namespace}-${group.id}.json`;
298
+ rmSync(path.join(site, transferFile), { force: true });
299
+ await run([
300
+ `${operation}-products`,
301
+ "--namespace",
302
+ namespace,
303
+ "--group",
304
+ group.id,
305
+ "--plan-file",
306
+ transferFile,
307
+ ]);
308
+ const value = JSON.parse(
309
+ readFileSync(path.join(site, transferFile), "utf8"),
310
+ );
311
+ const valid =
312
+ value.schema === 1 &&
313
+ value.namespace === namespace &&
314
+ Array.isArray(value.results) &&
315
+ value.results.length === 1 &&
316
+ value.results[0] !== null &&
317
+ value.results[0].id === group.id &&
318
+ typeof value.results[0].ready === "boolean";
319
+ if (!valid) return { ready: false, reason: "invalid-result" };
320
+ const result = value.results[0];
321
+ return {
322
+ ready: result.ready,
323
+ reason: result.ready
324
+ ? "ready"
325
+ : ["missing-secret", "miss", "invalid"].includes(result.reason)
326
+ ? result.reason
327
+ : "not-ready",
328
+ };
329
+ }
330
+ async function restore(kind) {
331
+ await limited(await plan(kind), async (group) => {
332
+ const archive = path.resolve(site, group.archive);
333
+ if (
334
+ !archive.startsWith(
335
+ path.join(site, "build/generated/product-caches") + path.sep,
336
+ )
337
+ )
338
+ throw new Error("Product archive escapes the cache directory");
339
+ try {
340
+ const hit = await archiveHit(group, archive);
341
+ if (!hit) {
342
+ counts.misses++;
343
+ counts.rebuild_needed++;
344
+ groupDiagnostic(group, false, "archive-miss");
345
+ } else {
346
+ counts.hits++;
347
+ const result = await transfer(group, "restore");
348
+ if (result.ready) counts.restored++;
349
+ else {
350
+ counts.rebuild_needed++;
351
+ groupDiagnostic(group, false, result.reason);
352
+ }
353
+ }
354
+ } catch {
355
+ counts.failed++;
356
+ counts.rebuild_needed++;
357
+ groupDiagnostic(group, false, "operation-error");
358
+ console.log(
359
+ "Optional encrypted product restore failed; consumers will validate or rebuild",
360
+ );
361
+ }
362
+ });
363
+ }
364
+ if (mode === "restore") {
365
+ // The compact global index supplies exact immutable course product keys.
366
+ await restore("global");
367
+ await restore("application");
368
+ await restore("search");
369
+ await restore("course");
370
+ if (
371
+ transport === "bundle" &&
372
+ bundleAvailable &&
373
+ [...bundleGroups].some((id) => !selectedBundleGroups.has(id))
374
+ ) {
375
+ counts.failed++;
376
+ console.log(
377
+ "Optional encrypted bundle contained unselected groups; consumers will validate or rebuild",
378
+ );
379
+ }
380
+ } else {
381
+ const groups = await plan();
382
+ const prepared = [];
383
+ if (transport === "bundle") cleanBundleDirectory();
384
+ await limited(groups, async (group) => {
385
+ try {
386
+ const result = await transfer(group, "save");
387
+ if (!result.ready) {
388
+ counts.skipped++;
389
+ groupDiagnostic(group, false, result.reason);
390
+ return;
391
+ }
392
+ const archive = path.resolve(site, group.archive);
393
+ if (!existsSync(archive)) {
394
+ counts.skipped++;
395
+ groupDiagnostic(group, false, "archive-missing");
396
+ } else if (!regular(archive)) throw new Error("Unsafe product archive");
397
+ else if (transport === "bundle") {
398
+ copyFileSync(archive, path.join(bundleDirectory, `${group.id}.gpg`));
399
+ prepared.push(group);
400
+ } else if ((await saveCache([archive], group.key)) >= 0)
401
+ counts.published++;
402
+ else {
403
+ counts.skipped++;
404
+ groupDiagnostic(group, true, "cache-not-created");
405
+ }
406
+ } catch {
407
+ counts.failed++;
408
+ groupDiagnostic(group, false, "operation-error");
409
+ console.log("Optional encrypted product save failed");
410
+ }
411
+ });
412
+ if (transport === "bundle" && prepared.length) {
413
+ writeFileSync(
414
+ path.join(bundleDirectory, "bundle.json"),
415
+ JSON.stringify({
416
+ schema: 1,
417
+ namespace,
418
+ runtime,
419
+ groups: prepared.map((group) => group.id).sort(),
420
+ }) + "\n",
421
+ { mode: 0o600 },
422
+ );
423
+ try {
424
+ if ((await saveCache([bundleDirectory], bundleKey())) >= 0)
425
+ counts.published += prepared.length;
426
+ else {
427
+ counts.skipped += prepared.length;
428
+ for (const group of prepared)
429
+ groupDiagnostic(group, true, "cache-not-created");
430
+ }
431
+ } catch {
432
+ counts.failed += prepared.length;
433
+ for (const group of prepared)
434
+ groupDiagnostic(group, false, "operation-error");
435
+ console.log("Optional encrypted bundle save failed");
436
+ }
437
+ }
438
+ }
439
+ }
440
+ console.log(
441
+ "Encrypted product cache results: " +
442
+ JSON.stringify({ mode, namespace, transport, ...counts }),
443
+ );
444
+ if (process.env.GITHUB_OUTPUT)
445
+ appendFileSync(
446
+ process.env.GITHUB_OUTPUT,
447
+ Object.entries(counts)
448
+ .map(([name, value]) => `${name}=${value}\n`)
449
+ .join(""),
450
+ );
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Ding
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,147 @@
1
- # Temporary Holding Version
1
+ # twiki
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An Obsidian-compatible Markdown publishing engine with wikilinks,
4
+ transclusions, search, file previews, and diagrams. Build a Cloudflare Worker
5
+ or export public pages for a static host.
6
+
7
+ Consumers keep their notes, a declarative config, and a pinned engine dependency.
8
+ Installed engine files remain read-only; generated state belongs to the consumer.
9
+
10
+ ## Start a vault
11
+
12
+ ```sh
13
+ bun add --dev --exact @twango/twiki
14
+ bunx twiki init
15
+ bunx twiki dev
16
+ ```
17
+
18
+ Use Bun 1.4+ and Node 22.12+. Git discovery needs Git. R worksheets need an
19
+ installed R renderer and their declared packages; rendering does not download
20
+ missing packages. `check` also needs Lychee 0.24.2 and Playwright Chromium
21
+ (`bunx playwright install --with-deps chromium`).
22
+
23
+ `init` creates `twiki.config.json` without overwriting an existing file:
24
+
25
+ ```json
26
+ {
27
+ "version": 1,
28
+ "contentDir": "notes",
29
+ "discovery": "filesystem",
30
+ "publication": "public",
31
+ "site": {
32
+ "name": "My notes",
33
+ "title": "My notes",
34
+ "url": "https://notes.example.com",
35
+ "description": "Notes and reference material."
36
+ }
37
+ }
38
+ ```
39
+
40
+ Without config, the current directory is the vault and publication is private.
41
+ Hidden directories, including `.obsidian` and `.twiki`, are excluded.
42
+
43
+ ## Commands
44
+
45
+ ```sh
46
+ bunx twiki dev -- --port 5173
47
+ bunx twiki check
48
+ bunx twiki build --target cloudflare
49
+ bunx twiki preview
50
+ bunx twiki deploy
51
+ bunx twiki build --target static
52
+ ```
53
+
54
+ `--project <directory>` selects a consumer root; `--config <file>` selects an
55
+ explicit JSON config. Dev and preview accept extra arguments after `--`.
56
+
57
+ Static export requires `publication: "public"`; a static host cannot protect
58
+ private files. `.twiki/output` aliases the public static tree or the complete
59
+ Cloudflare artifact. `deploy` uploads an existing Cloudflare build and requires
60
+ explicit Cloudflare configuration and credentials.
61
+
62
+ ## Cloudflare
63
+
64
+ The optional `cloudflare` object accepts Worker resource settings. Your `vars`,
65
+ D1 bindings, R2 buckets, routes, account ID, and compatibility settings are
66
+ retained; Twiki supplies generated `main` and `assets` paths.
67
+
68
+ ```json
69
+ {
70
+ "version": 1,
71
+ "contentDir": "notes",
72
+ "publication": "public",
73
+ "site": { "name": "My notes", "url": "https://notes.example.com" },
74
+ "cloudflare": {
75
+ "name": "my-notes",
76
+ "compatibility_date": "2026-06-07",
77
+ "compatibility_flags": ["nodejs_compat"],
78
+ "workers_dev": true,
79
+ "preview_urls": false,
80
+ "routes": []
81
+ }
82
+ }
83
+ ```
84
+
85
+ Provide `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` through the environment,
86
+ never through committed configuration. GitHub authentication uses a `DB` D1
87
+ binding and owner/client settings in `vars`. Without `DB`, authentication is
88
+ disabled and private assets remain unavailable. Analytics is disabled by default;
89
+ configure `analytics: {"siteId": "…", "endpoint": "https://analytics.example.com/api"}`
90
+ to use your own endpoint.
91
+
92
+ ## Git and nested courses
93
+
94
+ ```json
95
+ {
96
+ "version": 1,
97
+ "contentDir": "content",
98
+ "discovery": "git",
99
+ "publication": "policies"
100
+ }
101
+ ```
102
+
103
+ Git discovery reads recursive tracked files and declared course submodules.
104
+ Twiki does not edit notes, commit submodules, or change Git pointers.
105
+
106
+ ## CI and caches
107
+
108
+ ```sh
109
+ bunx twiki prepare --json
110
+ bunx twiki exec -- tooling/cache.ts restore
111
+ bunx twiki build --target cloudflare
112
+ bunx twiki exec -- tooling/cache.ts save
113
+ ```
114
+
115
+ The stable runtime is `<consumer>/.twiki/site`. Compiler, application, search, R,
116
+ and Wrangler caches belong there. Engine dependencies use the shipped frozen
117
+ lockfile in a separate consumer-owned `node_modules`; existing vault dependencies
118
+ cannot change the engine compiler. Repeated preparation reuses that installation.
119
+
120
+ `prepare --json` returns `projectRoot`, `workDir`, `outputDir`, and resolved
121
+ `TWIKI_*` environment paths. It does not serialize inherited credentials.
122
+ `exec` runs Bun arguments from that runtime, including cache tools, migrations,
123
+ and verification scripts.
124
+
125
+ The package exports `@twango/twiki/actions/product-cache` for Node action bootstraps,
126
+ `@twango/twiki/cli` for locating the executable, and `@twango/twiki/runtime` for Bun scripts.
127
+
128
+ ## Engine development
129
+
130
+ ```sh
131
+ bun install --frozen-lockfile
132
+ bun run test
133
+ bun run check
134
+ bun cli/twiki.ts dev --project examples/vault
135
+ ```
136
+
137
+ `site/` contains the SvelteKit engine and its focused tests. Runtime dependencies
138
+ are pinned exactly so consumers can compile without a sibling checkout. The
139
+ engine ships a matching frozen lockfile and installs its own dependencies into
140
+ `.twiki/site/node_modules` with lifecycle scripts disabled. Consumer dependency
141
+ versions do not affect the engine graph. Bun reuses its package cache; CI can
142
+ retain the runtime dependency directory for subsequent runs. The compatible
143
+ Svelte ESM printer (`esrap`) is pinned explicitly.
144
+
145
+ The stable Wrangler 4.149.0 toolchain uses its official pinned
146
+ `miniflare@5.20261006.1-alpha` implementation for local Workers. Twiki pins that
147
+ matching version rather than selecting an independent preview runtime.