@twango/twiki 0.0.0-stage → 0.1.1

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
package/cli/runtime.ts ADDED
@@ -0,0 +1,385 @@
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ realpathSync,
8
+ renameSync,
9
+ rmSync,
10
+ symlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import path from "node:path";
14
+ import { spawnSync } from "node:child_process";
15
+ import { fileURLToPath } from "node:url";
16
+ import {
17
+ parseProjectConfig,
18
+ resolveProject,
19
+ writeProjectSiteConfig,
20
+ type ResolvedProject,
21
+ } from "../site/tooling/lib/project-config";
22
+
23
+ export interface PreparedProject {
24
+ projectRoot: string;
25
+ workDir: string;
26
+ outputDir: string;
27
+ env: Record<string, string>;
28
+ }
29
+ export interface PrepareOptions {
30
+ projectRoot?: string;
31
+ configFile?: string;
32
+ environment?: NodeJS.ProcessEnv;
33
+ /** Override only for package integration fixtures. */
34
+ engineDirectory?: string;
35
+ /** Override only for isolated installer fixtures. */
36
+ installDependencies?: (workDir: string) => void;
37
+ }
38
+ const packageRoot = fileURLToPath(new URL("../", import.meta.url));
39
+ const engineDirectories = [
40
+ "src",
41
+ "tooling",
42
+ "worker",
43
+ "assets",
44
+ "static",
45
+ "migrations",
46
+ "database",
47
+ ];
48
+ const engineFiles = [
49
+ "package.json",
50
+ "bun.lock",
51
+ "svelte.config.js",
52
+ "vite.config.ts",
53
+ "tsconfig.json",
54
+ "wrangler.jsonc",
55
+ "_headers",
56
+ "_redirects",
57
+ "drizzle.config.ts",
58
+ ];
59
+ const safeRelative = (file: string) =>
60
+ file !== "" &&
61
+ !path.isAbsolute(file) &&
62
+ !file.includes("\\") &&
63
+ file.split("/").every((part) => part !== "" && part !== "." && part !== "..");
64
+ function directory(root: string, target: string) {
65
+ const relative = path.relative(root, target);
66
+ if (
67
+ relative === ".." ||
68
+ relative.startsWith(".." + path.sep) ||
69
+ path.isAbsolute(relative)
70
+ )
71
+ throw new Error("Runtime path escapes its consumer directory");
72
+ for (const part of relative.split(path.sep).filter(Boolean)) {
73
+ root = path.join(root, part);
74
+ const stat = lstatSync(root, { throwIfNoEntry: false });
75
+ if (stat && (!stat.isDirectory() || realpathSync(root) !== root))
76
+ throw new Error("Runtime directories must not be symlinks");
77
+ if (!stat) mkdirSync(root);
78
+ }
79
+ }
80
+ function writeRuntime(root: string, file: string, bytes: string | Uint8Array) {
81
+ if (!safeRelative(file)) throw new Error("Unsafe runtime filename");
82
+ const target = path.join(root, file);
83
+ directory(root, path.dirname(target));
84
+ const stat = lstatSync(target, { throwIfNoEntry: false });
85
+ if (stat && (!stat.isFile() || realpathSync(target) !== target))
86
+ throw new Error("Runtime files must be regular");
87
+ const value = Buffer.from(bytes);
88
+ if (stat?.size === value.length && readFileSync(target).equals(value)) return;
89
+ const temporary = target + ".twiki-tmp-" + process.pid;
90
+ try {
91
+ writeFileSync(temporary, value);
92
+ renameSync(temporary, target);
93
+ } finally {
94
+ rmSync(temporary, { force: true });
95
+ }
96
+ }
97
+ function inventory(source: string) {
98
+ const files = engineFiles.filter((file) =>
99
+ existsSync(path.join(source, file)),
100
+ );
101
+ const walk = (relative: string) => {
102
+ const absolute = path.join(source, relative);
103
+ if (!existsSync(absolute)) return;
104
+ for (const entry of readdirSync(absolute, { withFileTypes: true }).sort(
105
+ (a, b) => a.name.localeCompare(b.name),
106
+ )) {
107
+ if (entry.name === "node_modules" || entry.name === ".DS_Store") continue;
108
+ const file = relative + "/" + entry.name;
109
+ if (
110
+ file === "src/lib/generated" ||
111
+ file.startsWith("static/_") ||
112
+ file === "static/.wrangler"
113
+ )
114
+ continue;
115
+ if (entry.isSymbolicLink())
116
+ throw new Error("Packaged engine files must not be symlinks");
117
+ if (entry.isDirectory()) walk(file);
118
+ else if (entry.isFile()) files.push(file);
119
+ }
120
+ };
121
+ for (const folder of engineDirectories) walk(folder);
122
+ return [...new Set(files)].sort();
123
+ }
124
+ function installEngineDependencies(
125
+ root: string,
126
+ environment: NodeJS.ProcessEnv,
127
+ ) {
128
+ const modules = path.join(root, "node_modules");
129
+ directory(root, modules);
130
+ const contained = (file: string) => {
131
+ const relative = path.relative(modules, file);
132
+ return (
133
+ relative !== ".." &&
134
+ !relative.startsWith(".." + path.sep) &&
135
+ !path.isAbsolute(relative)
136
+ );
137
+ };
138
+ // Retire links from older CLI staging without following or mutating their targets.
139
+ // Existing consumer-owned Vite caches and isolated package installs stay intact.
140
+ const retireLinks = (folder: string, nested = false) => {
141
+ for (const entry of readdirSync(folder, { withFileTypes: true })) {
142
+ const file = path.join(folder, entry.name);
143
+ if (entry.isSymbolicLink()) {
144
+ if (!existsSync(file) || !contained(realpathSync(file))) rmSync(file);
145
+ } else if (
146
+ !nested &&
147
+ entry.isDirectory() &&
148
+ (entry.name.startsWith("@") || entry.name === ".bin")
149
+ )
150
+ retireLinks(file, true);
151
+ }
152
+ };
153
+ retireLinks(modules);
154
+ const installed = spawnSync(
155
+ process.execPath,
156
+ [
157
+ "install",
158
+ "--frozen-lockfile",
159
+ "--ignore-scripts",
160
+ "--backend",
161
+ "copyfile",
162
+ "--silent",
163
+ ],
164
+ {
165
+ cwd: root,
166
+ env: { ...process.env, ...environment },
167
+ encoding: "utf8",
168
+ maxBuffer: 16 * 1024 * 1024,
169
+ },
170
+ );
171
+ if (installed.error)
172
+ throw new Error(
173
+ `Unable to install the frozen Twiki engine: ${installed.error.message}`,
174
+ );
175
+ if (installed.status !== 0)
176
+ throw new Error(
177
+ `Frozen Twiki engine dependency installation failed: ${installed.stderr.trim() || "bun install exited unsuccessfully"}`,
178
+ );
179
+ }
180
+ export function wranglerConfiguration(project: ResolvedProject) {
181
+ const supplied = project.cloudflare ?? {};
182
+ const rawVars = supplied.vars;
183
+ if (
184
+ rawVars !== undefined &&
185
+ (!rawVars || typeof rawVars !== "object" || Array.isArray(rawVars))
186
+ )
187
+ throw new Error("cloudflare.vars must be an object");
188
+ const bindings = Array.isArray(supplied.d1_databases)
189
+ ? supplied.d1_databases
190
+ : [];
191
+ const vars = {
192
+ ORIGIN: project.site.url,
193
+ TWIKI_SITE_NAME: project.site.name,
194
+ TWIKI_SITE_DESCRIPTION: project.site.description,
195
+ TWIKI_AUTH_MODE: bindings.some(
196
+ (binding) =>
197
+ binding &&
198
+ typeof binding === "object" &&
199
+ (binding as { binding?: string }).binding === "DB",
200
+ )
201
+ ? "github"
202
+ : "none",
203
+ DISABLE_ANALYTICS: project.analytics ? "false" : "true",
204
+ ...(project.analytics
205
+ ? {
206
+ TWIKI_ANALYTICS_SITE_ID: project.analytics.siteId,
207
+ TWIKI_ANALYTICS_ENDPOINT: project.analytics.endpoint ?? "",
208
+ }
209
+ : {}),
210
+ ...(rawVars as Record<string, string> | undefined),
211
+ };
212
+ return {
213
+ name: "twiki",
214
+ compatibility_date: "2026-06-07",
215
+ compatibility_flags: ["nodejs_compat"],
216
+ d1_databases: [],
217
+ workers_dev: vars.TWIKI_AUTH_MODE === "none",
218
+ preview_urls: false,
219
+ routes: [],
220
+ ...supplied,
221
+ vars,
222
+ main: "build/.svelte-kit/worker.js",
223
+ assets: {
224
+ binding: "ASSETS",
225
+ directory: "build/.svelte-kit/cloudflare",
226
+ html_handling: "drop-trailing-slash",
227
+ not_found_handling: "404-page",
228
+ run_worker_first: true,
229
+ },
230
+ };
231
+ }
232
+ /** Copies maintained engine code; package installation and source vault remain read-only. */
233
+ export function prepareProject(options: PrepareOptions = {}): PreparedProject {
234
+ const environment = options.environment ?? process.env;
235
+ const projectRoot = realpathSync(
236
+ path.resolve(
237
+ options.projectRoot ?? environment.TWIKI_PROJECT_ROOT ?? process.cwd(),
238
+ ),
239
+ );
240
+ const state = path.join(projectRoot, ".twiki");
241
+ const workDir = path.join(state, "site");
242
+ const requested = options.configFile ?? environment.TWIKI_CONFIG_FILE;
243
+ const configFile = requested
244
+ ? path.resolve(projectRoot, requested)
245
+ : path.join(projectRoot, "twiki.config.json");
246
+ const raw = existsSync(configFile)
247
+ ? parseProjectConfig(JSON.parse(readFileSync(configFile, "utf8")))
248
+ : { version: 1 as const };
249
+ if (requested && !existsSync(configFile))
250
+ throw new Error("The requested Twiki config does not exist");
251
+ const inputEnvironment = {
252
+ ...environment,
253
+ TWIKI_PROJECT_ROOT: projectRoot,
254
+ TWIKI_CONFIG_FILE: existsSync(configFile) ? configFile : undefined,
255
+ };
256
+ const project = resolveProject(workDir, projectRoot, inputEnvironment);
257
+ if (
258
+ !existsSync(project.contentDir) ||
259
+ !lstatSync(project.contentDir).isDirectory()
260
+ )
261
+ throw new Error("The content directory does not exist");
262
+ directory(projectRoot, state);
263
+ const lock = path.join(state, "prepare.lock");
264
+ try {
265
+ mkdirSync(lock);
266
+ } catch (error) {
267
+ if ((error as NodeJS.ErrnoException).code === "EEXIST")
268
+ throw new Error(
269
+ "Another Twiki command is preparing this project; retry when it finishes",
270
+ );
271
+ throw error;
272
+ }
273
+ try {
274
+ const source = realpathSync(
275
+ options.engineDirectory ?? path.join(packageRoot, "site"),
276
+ );
277
+ const files = inventory(source);
278
+ directory(projectRoot, workDir);
279
+ const previousFile = path.join(state, "engine.json");
280
+ let previous: string[] = [];
281
+ if (existsSync(previousFile)) {
282
+ const saved = JSON.parse(readFileSync(previousFile, "utf8"));
283
+ if (
284
+ !Array.isArray(saved.files) ||
285
+ !saved.files.every(
286
+ (file: unknown) => typeof file === "string" && safeRelative(file),
287
+ )
288
+ )
289
+ throw new Error("Invalid runtime source inventory");
290
+ previous = saved.files;
291
+ }
292
+ for (const file of files)
293
+ if (
294
+ ![
295
+ "src/lib/site-config.json",
296
+ "src/lib/analytics-config.json",
297
+ "src/lib/auth-config.json",
298
+ "wrangler.jsonc",
299
+ ].includes(file)
300
+ )
301
+ writeRuntime(workDir, file, readFileSync(path.join(source, file)));
302
+ for (const file of previous)
303
+ if (!files.includes(file)) {
304
+ directory(workDir, path.dirname(path.join(workDir, file)));
305
+ rmSync(path.join(workDir, file), { force: true });
306
+ }
307
+ (
308
+ options.installDependencies ??
309
+ ((runtime) => installEngineDependencies(runtime, environment))
310
+ )(workDir);
311
+ const modules = path.join(workDir, "node_modules");
312
+ directory(workDir, modules);
313
+ writeRuntime(state, "config.json", JSON.stringify(raw, null, 2) + "\n");
314
+ writeProjectSiteConfig(workDir, project);
315
+ writeRuntime(
316
+ workDir,
317
+ "src/lib/analytics-config.json",
318
+ JSON.stringify(project.analytics ?? null, null, 2) + "\n",
319
+ );
320
+ const wrangler = wranglerConfiguration(project);
321
+ writeRuntime(
322
+ workDir,
323
+ "src/lib/auth-config.json",
324
+ JSON.stringify(
325
+ { enabled: wrangler.vars.TWIKI_AUTH_MODE === "github" },
326
+ null,
327
+ 2,
328
+ ) + "\n",
329
+ );
330
+ writeRuntime(
331
+ workDir,
332
+ "wrangler.jsonc",
333
+ JSON.stringify(wrangler, null, 2) + "\n",
334
+ );
335
+ writeRuntime(
336
+ state,
337
+ "engine.json",
338
+ JSON.stringify({ schema: 1, files }, null, 2) + "\n",
339
+ );
340
+ return {
341
+ projectRoot,
342
+ workDir,
343
+ outputDir: path.join(state, "output"),
344
+ env: {
345
+ TWIKI_PROJECT_ROOT: projectRoot,
346
+ TWIKI_CONFIG_FILE: path.join(state, "config.json"),
347
+ TWIKI_CONTENT_DIR: project.contentDir,
348
+ TWIKI_SITE_DIR: workDir,
349
+ TWIKI_DEPENDENCY_DIR: modules,
350
+ TWIKI_DEPENDENCY_PATHS: JSON.stringify([modules]),
351
+ },
352
+ };
353
+ } finally {
354
+ rmSync(lock, { recursive: true, force: true });
355
+ }
356
+ }
357
+ /** A stable output alias avoids copying every asset after an already successful build. */
358
+ export function publishOutput(
359
+ project: PreparedProject,
360
+ target: "cloudflare" | "static",
361
+ ) {
362
+ const source = path.join(
363
+ project.workDir,
364
+ "build/.svelte-kit",
365
+ ...(target === "static" ? ["cloudflare", "_published"] : []),
366
+ );
367
+ if (!existsSync(source))
368
+ throw new Error("The build did not produce the requested output");
369
+ const stat = lstatSync(project.outputDir, { throwIfNoEntry: false });
370
+ if (stat && !stat.isSymbolicLink())
371
+ throw new Error(
372
+ "Refusing to replace an existing non-symlink output directory",
373
+ );
374
+ const temporary = project.outputDir + ".tmp-" + process.pid;
375
+ try {
376
+ symlinkSync(
377
+ path.relative(path.dirname(project.outputDir), source),
378
+ temporary,
379
+ "dir",
380
+ );
381
+ renameSync(temporary, project.outputDir);
382
+ } finally {
383
+ rmSync(temporary, { force: true });
384
+ }
385
+ }
package/cli/twiki.ts ADDED
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env bun
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { prepareProject, publishOutput, type PreparedProject } from "./runtime";
6
+ import { resolveProject } from "../site/tooling/lib/project-config";
7
+
8
+ export const help = `twiki <command> [options]
9
+
10
+ dev Start the development server
11
+ check Check types, links, targets, and diagrams
12
+ build --target cloudflare Build a complete Cloudflare Worker (default)
13
+ build --target static Export public static pages
14
+ preview Preview the Cloudflare build locally
15
+ deploy Upload the existing Cloudflare build
16
+ prepare [--json] Prepare the consumer-owned .twiki/site runtime
17
+ exec -- <Bun arguments> Run engine tooling with the resolved configuration
18
+ init Create twiki.config.json (never overwrites)
19
+
20
+ --project <directory> Consumer root (default: current directory)
21
+ --config <file> Explicit declarative JSON configuration
22
+ -- Pass remaining arguments to dev/preview/exec
23
+ `;
24
+ export function argumentsFor(argv: string[]): {
25
+ command: string;
26
+ projectRoot?: string;
27
+ configFile?: string;
28
+ target: "cloudflare" | "static";
29
+ json: boolean;
30
+ extra: string[];
31
+ } {
32
+ const command = argv[0] ?? "help";
33
+ if (["--help", "-h", "help"].includes(command))
34
+ return {
35
+ command: "help",
36
+ target: "cloudflare" as const,
37
+ json: false,
38
+ extra: [] as string[],
39
+ };
40
+ if (["--version", "-v"].includes(command))
41
+ return {
42
+ command: "version",
43
+ target: "cloudflare" as const,
44
+ json: false,
45
+ extra: [] as string[],
46
+ };
47
+ if (
48
+ ![
49
+ "dev",
50
+ "check",
51
+ "build",
52
+ "preview",
53
+ "deploy",
54
+ "prepare",
55
+ "exec",
56
+ "init",
57
+ ].includes(command)
58
+ )
59
+ throw new Error("Unknown command; run twiki --help");
60
+ let projectRoot: string | undefined;
61
+ let configFile: string | undefined;
62
+ let target: "cloudflare" | "static" = "cloudflare";
63
+ let json = false;
64
+ const extra: string[] = [];
65
+ for (let i = 1; i < argv.length; i++) {
66
+ const arg = argv[i];
67
+ if (arg === "--") {
68
+ extra.push(...argv.slice(i + 1));
69
+ break;
70
+ }
71
+ if (arg === "--json" && command === "prepare") json = true;
72
+ else if (["--project", "--config", "--target"].includes(arg)) {
73
+ const value = argv[++i];
74
+ if (!value || value.startsWith("--"))
75
+ throw new Error("Missing option value");
76
+ if (arg === "--project") projectRoot = value;
77
+ else if (arg === "--config") configFile = value;
78
+ else {
79
+ if (command !== "build" || !["cloudflare", "static"].includes(value))
80
+ throw new Error("Build target must be cloudflare or static");
81
+ target = value as "cloudflare" | "static";
82
+ }
83
+ } else if (command === "dev" || command === "preview") extra.push(arg);
84
+ else throw new Error("Unsupported argument; run twiki --help");
85
+ }
86
+ if (extra.length && !["dev", "preview", "exec"].includes(command))
87
+ throw new Error("This command does not accept passthrough arguments");
88
+ if (command === "exec" && !extra.length)
89
+ throw new Error("Use twiki exec -- <Bun arguments>");
90
+ return { command, projectRoot, configFile, target, json, extra };
91
+ }
92
+ export function invocation(
93
+ command: string,
94
+ project: PreparedProject,
95
+ extra: string[] = [],
96
+ ) {
97
+ const bin = (name: string) =>
98
+ path.join(project.workDir, "node_modules", ".bin", name);
99
+ if (command === "dev") return [bin("vite"), "dev", ...extra];
100
+ if (command === "check") return [process.execPath, "run", "check"];
101
+ if (command === "build") return [process.execPath, "tooling/build.ts"];
102
+ if (command === "preview") {
103
+ if (
104
+ extra.some(
105
+ (argument) =>
106
+ argument === "-r" ||
107
+ argument.startsWith("-r=") ||
108
+ argument === "--remote" ||
109
+ argument.startsWith("--remote=") ||
110
+ argument === "--local=false",
111
+ )
112
+ )
113
+ throw new Error("Preview supports local Workers only");
114
+ const option = (name: string) => {
115
+ const index = extra.findIndex(
116
+ (argument) => argument === name || argument.startsWith(`${name}=`),
117
+ );
118
+ if (index < 0) return undefined;
119
+ const value = extra[index].startsWith(`${name}=`)
120
+ ? extra[index].slice(name.length + 1)
121
+ : extra[index + 1];
122
+ if (!value || value.startsWith("--"))
123
+ throw new Error(`Missing ${name} value`);
124
+ return value;
125
+ };
126
+ const port = option("--port") ?? "4173";
127
+ if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535)
128
+ throw new Error("Preview port must be between 1 and 65535");
129
+ const ip = option("--ip") ?? "127.0.0.1";
130
+ const protocol = option("--local-protocol") ?? "http";
131
+ if (protocol !== "http" && protocol !== "https")
132
+ throw new Error("Preview protocol must be http or https");
133
+ const host =
134
+ ip === "0.0.0.0"
135
+ ? "127.0.0.1"
136
+ : ip === "::"
137
+ ? "[::1]"
138
+ : ip.includes(":") && !ip.startsWith("[")
139
+ ? `[${ip}]`
140
+ : ip;
141
+ let origin: URL;
142
+ try {
143
+ origin = new URL(`${protocol}://${host}:${port}`);
144
+ } catch {
145
+ throw new Error("Invalid preview IP or hostname");
146
+ }
147
+ if (
148
+ origin.username ||
149
+ origin.password ||
150
+ origin.pathname !== "/" ||
151
+ origin.search ||
152
+ origin.hash
153
+ )
154
+ throw new Error("Invalid preview IP or hostname");
155
+ let explicitOrigin = false,
156
+ inVariables = false;
157
+ for (const argument of extra) {
158
+ if (argument === "--var") {
159
+ inVariables = true;
160
+ continue;
161
+ }
162
+ if (argument.startsWith("--var=")) {
163
+ explicitOrigin ||= argument.startsWith("--var=ORIGIN:");
164
+ inVariables = true;
165
+ continue;
166
+ }
167
+ if (argument.startsWith("-")) inVariables = false;
168
+ if (inVariables && argument.startsWith("ORIGIN:")) explicitOrigin = true;
169
+ }
170
+ return [
171
+ process.execPath,
172
+ "tooling/wrangler.ts",
173
+ "dev",
174
+ ...(option("--persist-to")
175
+ ? []
176
+ : ["--persist-to", "build/.wrangler/state"]),
177
+ ...(option("--port") ? [] : ["--port", port]),
178
+ ...(option("--ip") ? [] : ["--ip", ip]),
179
+ ...(!explicitOrigin ? ["--var", `ORIGIN:${origin.origin}`] : []),
180
+ ...extra,
181
+ ];
182
+ }
183
+ if (command === "deploy")
184
+ return [process.execPath, "tooling/deploy.ts", "--upload"];
185
+ if (command === "exec") return [process.execPath, ...extra];
186
+ throw new Error("Unsupported engine invocation");
187
+ }
188
+ async function run(binary: string, args: string[], project: PreparedProject) {
189
+ const child = spawn(binary, args, {
190
+ cwd: project.workDir,
191
+ stdio: "inherit",
192
+ env: {
193
+ ...process.env,
194
+ ...project.env,
195
+ PATH: [
196
+ path.join(project.workDir, "node_modules/.bin"),
197
+ path.dirname(process.execPath),
198
+ process.env.PATH,
199
+ ]
200
+ .filter(Boolean)
201
+ .join(path.delimiter),
202
+ },
203
+ });
204
+ const interrupt = (signal: NodeJS.Signals) => child.kill(signal);
205
+ const sigint = () => interrupt("SIGINT");
206
+ const sigterm = () => interrupt("SIGTERM");
207
+ process.on("SIGINT", sigint);
208
+ process.on("SIGTERM", sigterm);
209
+ try {
210
+ await new Promise<void>((resolve, reject) => {
211
+ child.once("error", reject);
212
+ child.once("close", (code, signal) =>
213
+ code === 0
214
+ ? resolve()
215
+ : reject(
216
+ new Error("Engine command failed (" + (code ?? signal) + ")"),
217
+ ),
218
+ );
219
+ });
220
+ } finally {
221
+ process.off("SIGINT", sigint);
222
+ process.off("SIGTERM", sigterm);
223
+ }
224
+ }
225
+ export async function main(argv = process.argv.slice(2)) {
226
+ const options = argumentsFor(argv);
227
+ if (options.command === "help") {
228
+ console.log(help);
229
+ return;
230
+ }
231
+ if (options.command === "version") {
232
+ console.log(
233
+ JSON.parse(
234
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
235
+ ).version,
236
+ );
237
+ return;
238
+ }
239
+ if (options.command === "init") {
240
+ const file = path.resolve(
241
+ options.projectRoot ?? process.cwd(),
242
+ options.configFile ?? "twiki.config.json",
243
+ );
244
+ writeFileSync(
245
+ file,
246
+ JSON.stringify(
247
+ {
248
+ version: 1,
249
+ contentDir: ".",
250
+ discovery: "filesystem",
251
+ publication: "private",
252
+ site: { name: "My notes", title: "My notes" },
253
+ },
254
+ null,
255
+ 2,
256
+ ) + "\n",
257
+ { flag: "wx" },
258
+ );
259
+ console.log("Created twiki.config.json");
260
+ return;
261
+ }
262
+ const project = prepareProject(options);
263
+ if (options.command === "prepare") {
264
+ console.log(
265
+ options.json
266
+ ? JSON.stringify(project)
267
+ : "Twiki runtime ready: " +
268
+ path.relative(process.cwd(), project.workDir),
269
+ );
270
+ return;
271
+ }
272
+ const resolved = resolveProject(project.workDir, project.projectRoot, {
273
+ ...process.env,
274
+ ...project.env,
275
+ });
276
+ if (
277
+ options.command === "build" &&
278
+ options.target === "static" &&
279
+ resolved.publication !== "public"
280
+ )
281
+ throw new Error(
282
+ 'Static export requires publication: "public"; private content needs Cloudflare authentication',
283
+ );
284
+ if (options.command === "deploy" && !resolved.cloudflare)
285
+ throw new Error("Deployment requires explicit cloudflare configuration");
286
+ const [binary, ...args] = invocation(options.command, project, options.extra);
287
+ if (options.command === "build" && options.target === "static")
288
+ args.push("--static");
289
+ await run(binary, args, project);
290
+ if (options.command === "build") {
291
+ publishOutput(project, options.target);
292
+ console.log(
293
+ "Output: " + path.relative(project.projectRoot, project.outputDir),
294
+ );
295
+ }
296
+ }
297
+ if (import.meta.main)
298
+ try {
299
+ await main();
300
+ } catch (error) {
301
+ console.error(
302
+ "twiki: " + (error instanceof Error ? error.message : "command failed"),
303
+ );
304
+ process.exitCode = 1;
305
+ }