@axerity/cli 0.1.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 (186) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/axerity.default.json +135 -0
  4. package/axerity.schema.json +268 -0
  5. package/bin/axerity.js +305 -0
  6. package/mdsvex.config.js +261 -0
  7. package/package.json +103 -0
  8. package/scripts/prepare-engine.mjs +20 -0
  9. package/src/app.d.ts +17 -0
  10. package/src/app.html +39 -0
  11. package/src/content/demo/api/meta.json +5 -0
  12. package/src/content/demo/api/pet/add-pet.md +105 -0
  13. package/src/content/demo/api/pet/delete-pet.md +70 -0
  14. package/src/content/demo/api/pet/find-by-status.md +72 -0
  15. package/src/content/demo/api/pet/find-by-tags.md +64 -0
  16. package/src/content/demo/api/pet/get-pet.md +99 -0
  17. package/src/content/demo/api/pet/meta.json +15 -0
  18. package/src/content/demo/api/pet/pet-object.md +112 -0
  19. package/src/content/demo/api/pet/update-pet-with-form.md +69 -0
  20. package/src/content/demo/api/pet/update-pet.md +79 -0
  21. package/src/content/demo/api/pet/upload-image.md +79 -0
  22. package/src/content/demo/api/store/delete-order.md +62 -0
  23. package/src/content/demo/api/store/get-order.md +70 -0
  24. package/src/content/demo/api/store/inventory.md +54 -0
  25. package/src/content/demo/api/store/meta.json +5 -0
  26. package/src/content/demo/api/store/order-object.md +77 -0
  27. package/src/content/demo/api/store/place-order.md +83 -0
  28. package/src/content/demo/api/user/create-user.md +69 -0
  29. package/src/content/demo/api/user/create-with-list.md +57 -0
  30. package/src/content/demo/api/user/delete-user.md +61 -0
  31. package/src/content/demo/api/user/get-user.md +69 -0
  32. package/src/content/demo/api/user/login.md +80 -0
  33. package/src/content/demo/api/user/logout.md +45 -0
  34. package/src/content/demo/api/user/meta.json +14 -0
  35. package/src/content/demo/api/user/update-user.md +69 -0
  36. package/src/content/demo/api/user/user-object.md +85 -0
  37. package/src/content/demo/changelog.md +44 -0
  38. package/src/content/demo/components/accordion.md +70 -0
  39. package/src/content/demo/components/api.md +185 -0
  40. package/src/content/demo/components/badge.md +34 -0
  41. package/src/content/demo/components/callout.md +83 -0
  42. package/src/content/demo/components/cards.md +88 -0
  43. package/src/content/demo/components/code-group.md +55 -0
  44. package/src/content/demo/components/columns.md +42 -0
  45. package/src/content/demo/components/frame.md +51 -0
  46. package/src/content/demo/components/icon.md +54 -0
  47. package/src/content/demo/components/kbd.md +28 -0
  48. package/src/content/demo/components/meta.json +26 -0
  49. package/src/content/demo/components/roadmap.md +86 -0
  50. package/src/content/demo/components/steps.md +72 -0
  51. package/src/content/demo/components/tabs.md +146 -0
  52. package/src/content/demo/components/tooltip.md +44 -0
  53. package/src/content/demo/components/tree.md +83 -0
  54. package/src/content/demo/components/type-table.md +77 -0
  55. package/src/content/demo/components/update.md +48 -0
  56. package/src/content/demo/components/video.md +56 -0
  57. package/src/content/demo/components/webhooks.md +109 -0
  58. package/src/content/demo/components/websockets.md +101 -0
  59. package/src/content/demo/configuration/ai.md +40 -0
  60. package/src/content/demo/configuration/cli.md +46 -0
  61. package/src/content/demo/configuration/deployment.md +105 -0
  62. package/src/content/demo/configuration/index.md +92 -0
  63. package/src/content/demo/configuration/layouts.md +51 -0
  64. package/src/content/demo/configuration/meta.json +5 -0
  65. package/src/content/demo/configuration/navigation.md +167 -0
  66. package/src/content/demo/configuration/openapi.md +103 -0
  67. package/src/content/demo/configuration/search.md +36 -0
  68. package/src/content/demo/index.md +59 -0
  69. package/src/content/demo/installation.md +49 -0
  70. package/src/content/demo/meta.json +15 -0
  71. package/src/content/demo/quick-start.md +47 -0
  72. package/src/content/demo/theming/advanced.md +116 -0
  73. package/src/content/demo/theming/code.md +66 -0
  74. package/src/content/demo/theming/colors.md +103 -0
  75. package/src/content/demo/theming/index.md +88 -0
  76. package/src/content/demo/theming/layout.md +71 -0
  77. package/src/content/demo/theming/meta.json +5 -0
  78. package/src/content/demo/theming/themes.md +99 -0
  79. package/src/content/demo/theming/typography.md +83 -0
  80. package/src/content/demo/writing/code-blocks.md +154 -0
  81. package/src/content/demo/writing/diagrams.md +44 -0
  82. package/src/content/demo/writing/frontmatter.md +33 -0
  83. package/src/content/demo/writing/markdown.md +62 -0
  84. package/src/content/demo/writing/meta.json +5 -0
  85. package/src/hooks.server.ts +49 -0
  86. package/src/lib/assets/favicon.svg +1 -0
  87. package/src/lib/base.ts +12 -0
  88. package/src/lib/components/DynamicIcon.svelte +26 -0
  89. package/src/lib/components/docs/Analytics.svelte +38 -0
  90. package/src/lib/components/docs/Banner.svelte +44 -0
  91. package/src/lib/components/docs/Breadcrumbs.svelte +38 -0
  92. package/src/lib/components/docs/CopyPageMenu.svelte +119 -0
  93. package/src/lib/components/docs/DocsLayout.svelte +192 -0
  94. package/src/lib/components/docs/Footer.svelte +60 -0
  95. package/src/lib/components/docs/Mermaid.svelte +39 -0
  96. package/src/lib/components/docs/Navbar.svelte +144 -0
  97. package/src/lib/components/docs/PageMeta.svelte +35 -0
  98. package/src/lib/components/docs/PageNav.svelte +44 -0
  99. package/src/lib/components/docs/SearchDialog.svelte +182 -0
  100. package/src/lib/components/docs/Sidebar.svelte +85 -0
  101. package/src/lib/components/docs/SidebarDropdown.svelte +56 -0
  102. package/src/lib/components/docs/SidebarFooterLinks.svelte +19 -0
  103. package/src/lib/components/docs/SidebarGroup.svelte +54 -0
  104. package/src/lib/components/docs/SidebarLink.svelte +67 -0
  105. package/src/lib/components/docs/TableOfContents.svelte +77 -0
  106. package/src/lib/components/docs/ThemeToggle.svelte +19 -0
  107. package/src/lib/components/docs/VersionSwitcher.svelte +80 -0
  108. package/src/lib/components/kit/Accordion.svelte +60 -0
  109. package/src/lib/components/kit/AccordionGroup.svelte +13 -0
  110. package/src/lib/components/kit/Badge.svelte +32 -0
  111. package/src/lib/components/kit/Callout.svelte +51 -0
  112. package/src/lib/components/kit/Card.svelte +72 -0
  113. package/src/lib/components/kit/CardGroup.svelte +21 -0
  114. package/src/lib/components/kit/CodeGroup.svelte +65 -0
  115. package/src/lib/components/kit/Columns.svelte +26 -0
  116. package/src/lib/components/kit/Event.svelte +23 -0
  117. package/src/lib/components/kit/EventList.svelte +9 -0
  118. package/src/lib/components/kit/File.svelte +15 -0
  119. package/src/lib/components/kit/Folder.svelte +46 -0
  120. package/src/lib/components/kit/Frame.svelte +81 -0
  121. package/src/lib/components/kit/Icon.svelte +17 -0
  122. package/src/lib/components/kit/Kbd.svelte +11 -0
  123. package/src/lib/components/kit/Roadmap.svelte +15 -0
  124. package/src/lib/components/kit/RoadmapItem.svelte +109 -0
  125. package/src/lib/components/kit/Step.svelte +63 -0
  126. package/src/lib/components/kit/Steps.svelte +16 -0
  127. package/src/lib/components/kit/Tab.svelte +27 -0
  128. package/src/lib/components/kit/Tabs.svelte +75 -0
  129. package/src/lib/components/kit/Tooltip.svelte +33 -0
  130. package/src/lib/components/kit/Tree.svelte +11 -0
  131. package/src/lib/components/kit/TypeTable.svelte +187 -0
  132. package/src/lib/components/kit/Update.svelte +32 -0
  133. package/src/lib/components/kit/Video.svelte +64 -0
  134. package/src/lib/components/kit/accordion-context.ts +1 -0
  135. package/src/lib/components/kit/api/Api.svelte +80 -0
  136. package/src/lib/components/kit/api/ApiExamplePanel.svelte +100 -0
  137. package/src/lib/components/kit/api/ApiField.svelte +124 -0
  138. package/src/lib/components/kit/api/Channel.svelte +121 -0
  139. package/src/lib/components/kit/api/Endpoint.svelte +116 -0
  140. package/src/lib/components/kit/api/Enum.svelte +44 -0
  141. package/src/lib/components/kit/api/EnumValues.svelte +35 -0
  142. package/src/lib/components/kit/api/Expandable.svelte +70 -0
  143. package/src/lib/components/kit/api/Message.svelte +67 -0
  144. package/src/lib/components/kit/api/ObjectExample.svelte +11 -0
  145. package/src/lib/components/kit/api/RequestExample.svelte +11 -0
  146. package/src/lib/components/kit/api/ResponseExample.svelte +11 -0
  147. package/src/lib/components/kit/api/Webhook.svelte +115 -0
  148. package/src/lib/components/kit/api/api-context.ts +15 -0
  149. package/src/lib/components/kit/tabs-context.ts +8 -0
  150. package/src/lib/components/kit/tabs-store.svelte.ts +28 -0
  151. package/src/lib/config/site.ts +34 -0
  152. package/src/lib/content/index.ts +50 -0
  153. package/src/lib/content/raw.ts +21 -0
  154. package/src/lib/content/tree.ts +169 -0
  155. package/src/lib/index.ts +79 -0
  156. package/src/lib/nav-match.ts +23 -0
  157. package/src/lib/openapi/generate.ts +629 -0
  158. package/src/lib/server/og.ts +140 -0
  159. package/src/lib/state/search.svelte.ts +9 -0
  160. package/src/lib/state/theme.svelte.ts +58 -0
  161. package/src/lib/types.ts +216 -0
  162. package/src/params/docpage.ts +3 -0
  163. package/src/params/mdfile.ts +3 -0
  164. package/src/routes/+error.svelte +46 -0
  165. package/src/routes/+layout.svelte +25 -0
  166. package/src/routes/[...path=mdfile]/+server.ts +21 -0
  167. package/src/routes/[...slug=docpage]/+page.svelte +63 -0
  168. package/src/routes/[...slug=docpage]/+page.ts +44 -0
  169. package/src/routes/layout.css +897 -0
  170. package/src/routes/llms-full.txt/+server.ts +22 -0
  171. package/src/routes/llms.txt/+server.ts +20 -0
  172. package/src/routes/og/[...slug]/+server.ts +77 -0
  173. package/src/routes/rss.xml/+server.ts +65 -0
  174. package/src/routes/search.json/+server.ts +54 -0
  175. package/src/routes/sitemap.xml/+server.ts +21 -0
  176. package/static/favicon-dark.svg +6 -0
  177. package/static/favicon-light.svg +6 -0
  178. package/static/favicon.svg +14 -0
  179. package/static/fonts/geist-400.ttf +0 -0
  180. package/static/fonts/geist-600.ttf +0 -0
  181. package/static/fonts/geist-700.ttf +0 -0
  182. package/static/og-image.png +0 -0
  183. package/static/robots.txt +4 -0
  184. package/svelte.config.js +35 -0
  185. package/tsconfig.json +20 -0
  186. package/vite.config.ts +46 -0
package/bin/axerity.js ADDED
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import { fileURLToPath } from 'node:url';
4
+ import {
5
+ cpSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ rmSync,
11
+ symlinkSync,
12
+ writeFileSync
13
+ } from 'node:fs';
14
+ import { basename, dirname, join, relative, resolve } from 'node:path';
15
+
16
+ const here = dirname(fileURLToPath(import.meta.url));
17
+ const engineRoot = resolve(here, '..');
18
+ const userRoot = process.cwd();
19
+ const isEngineRepo = userRoot === engineRoot;
20
+
21
+ const symlinkType = process.platform === 'win32' ? 'junction' : 'dir';
22
+
23
+ // The engine runs in place, from its own install with its real node_modules. The
24
+ // user's project is mounted into a handful of gitignored paths for a single run,
25
+ // so nothing the engine tracks is ever touched — clean exit, Ctrl-C, or crash.
26
+ const ENGINE_DOCS = join(engineRoot, 'src', 'content', 'docs');
27
+ const ENGINE_CONFIG = join(engineRoot, 'axerity.json');
28
+ const ENGINE_STATIC = join(engineRoot, 'static');
29
+ const ENGINE_BUILD = join(engineRoot, 'build');
30
+ const SPECS_DIR = join(engineRoot, '.axerity-specs');
31
+ const ASSETS_DIR = join(engineRoot, '.axerity-assets');
32
+
33
+ function findContentDir() {
34
+ for (const candidate of ['docs', join('content', 'docs'), 'content']) {
35
+ const dir = join(userRoot, candidate);
36
+ if (existsSync(dir)) return dir;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /** Reset the engine's own demo site into the mount points (for `axerity` runs
42
+ * from inside the engine repo). `pnpm dev` does this via its pre-scripts. */
43
+ function prepareEngine() {
44
+ spawnSync(process.execPath, [join(engineRoot, 'scripts', 'prepare-engine.mjs')], {
45
+ stdio: 'inherit'
46
+ });
47
+ }
48
+
49
+ /** Copy local specs into a gitignored folder and rewrite the config to point at
50
+ * them, so the user's spec paths resolve without touching the engine tree. */
51
+ function mountSpecs(config) {
52
+ const rewrite = (source) => {
53
+ const spec = typeof source === 'string' ? source : source.spec;
54
+ if (!spec || /^https?:\/\//.test(spec) || !existsSync(join(userRoot, spec))) return source;
55
+ const local = `.axerity-specs/${basename(spec)}`;
56
+ cpSync(join(userRoot, spec), join(engineRoot, local));
57
+ return typeof source === 'string' ? local : { ...source, spec: local };
58
+ };
59
+
60
+ if (!config.openapi) return config;
61
+ mkdirSync(SPECS_DIR, { recursive: true });
62
+ const openapi = Array.isArray(config.openapi)
63
+ ? config.openapi.map(rewrite)
64
+ : rewrite(config.openapi);
65
+ return { ...config, openapi };
66
+ }
67
+
68
+ function mount(contentDir) {
69
+ // Content -> the path the engine globs, as symlinks so generated pages (an API
70
+ // reference) can sit alongside without touching the user's repo.
71
+ rmSync(ENGINE_DOCS, { recursive: true, force: true });
72
+ mkdirSync(ENGINE_DOCS, { recursive: true });
73
+ for (const entry of readdirSync(contentDir)) {
74
+ symlinkSync(join(contentDir, entry), join(ENGINE_DOCS, entry), symlinkType);
75
+ }
76
+
77
+ // Config (with local spec paths rewritten into the gitignored specs folder).
78
+ const config = JSON.parse(readFileSync(join(userRoot, 'axerity.json'), 'utf8'));
79
+ writeFileSync(ENGINE_CONFIG, JSON.stringify(mountSpecs(config), null, '\t'));
80
+
81
+ // Assets: engine defaults overlaid with the user's public/ folder.
82
+ rmSync(ASSETS_DIR, { recursive: true, force: true });
83
+ cpSync(ENGINE_STATIC, ASSETS_DIR, { recursive: true });
84
+ const userPublic = join(userRoot, 'public');
85
+ if (existsSync(userPublic)) cpSync(userPublic, ASSETS_DIR, { recursive: true });
86
+ }
87
+
88
+ function tidy() {
89
+ rmSync(SPECS_DIR, { recursive: true, force: true });
90
+ rmSync(ASSETS_DIR, { recursive: true, force: true });
91
+ rmSync(ENGINE_BUILD, { recursive: true, force: true });
92
+ }
93
+
94
+ const tty = process.stdout.isTTY;
95
+ const paint = (code) => (s) => (tty ? `\x1b[${code}m${s}\x1b[0m` : s);
96
+ const dim = paint('2');
97
+ const bold = paint('1');
98
+ const red = paint('31');
99
+ const green = paint('32');
100
+ const brand = (s) => (tty ? `\x1b[38;2;124;108;246m${s}\x1b[0m` : s);
101
+ const mark = brand('◆');
102
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
103
+
104
+ const banner = (sub) => process.stdout.write(`\n ${mark} ${bold('axerity')} ${dim(sub)}\n\n`);
105
+
106
+ const NOISE =
107
+ /^\s*(VITE v|➜|press h|ready in|Local:|Network:|\[vite\].*(optimiz|dependencies)|\[optimizer\]|Forced re-opt|watching for file changes|use --host)/i;
108
+
109
+ function streamServer(child, sub) {
110
+ let shown = false;
111
+ let buffer = '';
112
+ const onData = (chunk) => {
113
+ buffer += chunk;
114
+ const lines = buffer.split('\n');
115
+ buffer = lines.pop() ?? '';
116
+ for (const line of lines) {
117
+ const clean = strip(line);
118
+ const url = clean.match(/Local:\s*(http:\/\/\S+)/);
119
+ if (url && !shown) {
120
+ shown = true;
121
+ process.stdout.write(` ${dim('ready at')} ${brand(url[1])}\n`);
122
+ process.stdout.write(` ${dim('Ctrl+C to stop')}\n\n`);
123
+ continue;
124
+ }
125
+ if (!clean.trim() || NOISE.test(clean.trim())) continue;
126
+ process.stdout.write(` ${line}\n`);
127
+ }
128
+ };
129
+ child.stdout.on('data', onData);
130
+ child.stderr.on('data', onData);
131
+ }
132
+
133
+ /** A build: silent spinner on success, full captured output only on failure. */
134
+ function streamBuild(child) {
135
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
136
+ let i = 0;
137
+ let log = '';
138
+ child.stdout.on('data', (d) => (log += d));
139
+ child.stderr.on('data', (d) => (log += d));
140
+ const timer = tty
141
+ ? setInterval(() => {
142
+ process.stdout.write(`\r ${brand(frames[(i = ++i % frames.length)])} ${dim('building…')}`);
143
+ }, 80)
144
+ : null;
145
+ return {
146
+ stop(ok) {
147
+ if (timer) clearInterval(timer);
148
+ if (tty) process.stdout.write('\r\x1b[K');
149
+ if (!ok) process.stdout.write(log);
150
+ }
151
+ };
152
+ }
153
+
154
+ function runEngine(sub, extra, { mounted, onSuccess }) {
155
+ const viteEntry = join(engineRoot, 'node_modules', 'vite', 'bin', 'vite.js');
156
+ const child = spawn(process.execPath, [viteEntry, sub, ...extra], {
157
+ cwd: engineRoot,
158
+ stdio: ['inherit', 'pipe', 'pipe'],
159
+ env: {
160
+ ...process.env,
161
+ ...(mounted
162
+ ? { AXERITY_FS_ALLOW: userRoot, AXERITY_MOUNTED: '1', AXERITY_ASSETS: ASSETS_DIR }
163
+ : {})
164
+ }
165
+ });
166
+
167
+ banner(sub);
168
+ const build = sub === 'build' ? streamBuild(child) : (streamServer(child, sub), null);
169
+
170
+ let cleaned = false;
171
+ const cleanup = () => {
172
+ if (cleaned || !mounted) return;
173
+ cleaned = true;
174
+ tidy();
175
+ };
176
+
177
+ child.on('exit', (code) => {
178
+ const ok = code === 0;
179
+ if (build) build.stop(ok);
180
+ if (ok && onSuccess) onSuccess();
181
+ else if (!ok && sub !== 'build')
182
+ process.stdout.write(`\n ${red('✗')} exited with code ${code}\n`);
183
+ cleanup();
184
+ process.exit(code ?? 0);
185
+ });
186
+
187
+ process.on('SIGINT', () => child.kill('SIGINT'));
188
+ process.on('SIGTERM', () => child.kill('SIGINT'));
189
+ process.on('exit', cleanup);
190
+ }
191
+
192
+ function run(sub, extra) {
193
+ // Inside the engine repo: serve its own demo site.
194
+ if (isEngineRepo) {
195
+ prepareEngine();
196
+ return runEngine(sub, extra, { mounted: false });
197
+ }
198
+
199
+ if (!existsSync(join(userRoot, 'axerity.json'))) {
200
+ console.error('No axerity.json found here. Run `axerity init` to create a starter site.');
201
+ process.exit(1);
202
+ }
203
+ const contentDir = findContentDir();
204
+ if (!contentDir) {
205
+ console.error('No content found. Create a `docs/` folder with your Markdown.');
206
+ process.exit(1);
207
+ }
208
+
209
+ mount(contentDir);
210
+
211
+ if (sub === 'preview') {
212
+ const built = join(userRoot, 'build');
213
+ if (!existsSync(built)) {
214
+ tidy();
215
+ console.error('No build found. Run `axerity build` first.');
216
+ process.exit(1);
217
+ }
218
+ cpSync(built, ENGINE_BUILD, { recursive: true });
219
+ }
220
+
221
+ const onSuccess =
222
+ sub === 'build'
223
+ ? () => {
224
+ const out = join(userRoot, 'build');
225
+ rmSync(out, { recursive: true, force: true });
226
+ cpSync(ENGINE_BUILD, out, { recursive: true });
227
+ process.stdout.write(` ${green('✓')} built ${dim('→')} ./build\n\n`);
228
+ }
229
+ : undefined;
230
+
231
+ runEngine(sub, extra, { mounted: true, onSuccess });
232
+ }
233
+
234
+ function init() {
235
+ const target = process.argv[3] ? resolve(userRoot, process.argv[3]) : userRoot;
236
+ mkdirSync(join(target, 'docs'), { recursive: true });
237
+
238
+ const files = {
239
+ 'axerity.json': `${JSON.stringify(
240
+ {
241
+ $schema: 'https://axerity.com/axerity.schema.json',
242
+ name: 'My Docs',
243
+ description: 'Documentation built with Axerity.',
244
+ theme: 'neutral',
245
+ topNav: [{ title: 'Docs', href: '/' }]
246
+ },
247
+ null,
248
+ '\t'
249
+ )}\n`,
250
+ 'docs/meta.json': `${JSON.stringify(
251
+ { title: 'Getting Started', icon: 'rocket', pages: ['index', 'quickstart'] },
252
+ null,
253
+ '\t'
254
+ )}\n`,
255
+ 'docs/index.md': `---\ntitle: Introduction\ndescription: Welcome to your docs.\nicon: book-open\n---\n\n# Introduction\n\nWelcome to your new Axerity site. Edit \`docs/index.md\` to change this page.\n`,
256
+ 'docs/quickstart.md': `---\ntitle: Quick Start\ndescription: Get going in a minute.\nicon: rocket\n---\n\n# Quick Start\n\nRun \`axerity dev\` and start writing Markdown in the \`docs/\` folder.\n`,
257
+ '.gitignore': `build\n`
258
+ };
259
+
260
+ for (const [file, content] of Object.entries(files)) {
261
+ const path = join(target, file);
262
+ if (existsSync(path)) continue;
263
+ mkdirSync(dirname(path), { recursive: true });
264
+ writeFileSync(path, content);
265
+ }
266
+
267
+ const where = relative(userRoot, target) || '.';
268
+ console.log(`Created an Axerity site in ${where}.\n`);
269
+ console.log('Next:');
270
+ if (where !== '.') console.log(` cd ${where}`);
271
+ console.log(' axerity dev');
272
+ }
273
+
274
+ function help() {
275
+ const { version } = JSON.parse(readFileSync(join(engineRoot, 'package.json'), 'utf8'));
276
+ console.log(`Axerity ${version} — a documentation site generator\n`);
277
+ console.log('Usage: axerity <command>\n');
278
+ console.log('Commands:');
279
+ console.log(' init [dir] Scaffold a new docs site');
280
+ console.log(' dev Start the dev server');
281
+ console.log(' build Build the static site');
282
+ console.log(' preview Preview the production build');
283
+ console.log('\nWrite Markdown in docs/ and configure the site in axerity.json.');
284
+ }
285
+
286
+ const command = process.argv[2];
287
+ const extra = process.argv.slice(3);
288
+
289
+ switch (command) {
290
+ case 'init':
291
+ init();
292
+ break;
293
+ case 'dev':
294
+ run('dev', extra);
295
+ break;
296
+ case 'build':
297
+ run('build', extra);
298
+ break;
299
+ case 'preview':
300
+ run('preview', extra);
301
+ break;
302
+ default:
303
+ help();
304
+ process.exit(command ? 1 : 0);
305
+ }
@@ -0,0 +1,261 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { defineMDSveXConfig } from 'mdsvex';
3
+ import { escapeSvelte } from 'mdsvex';
4
+ import rehypeSlug from 'rehype-slug';
5
+ import { createHighlighter } from 'shiki';
6
+ import { transformerTwoslash } from '@shikijs/twoslash';
7
+
8
+ let basePath = '';
9
+ try {
10
+ basePath = JSON.parse(readFileSync('./axerity.json', 'utf8')).basePath ?? '';
11
+ } catch {
12
+ // no config
13
+ }
14
+
15
+ function rehypeBasePath() {
16
+ return (tree) => {
17
+ const walk = (node) => {
18
+ if (node.type === 'element' && node.tagName === 'a') {
19
+ const href = node.properties?.href;
20
+ if (
21
+ typeof href === 'string' &&
22
+ href.startsWith('/') &&
23
+ !href.startsWith('//') &&
24
+ !href.startsWith(`${basePath}/`)
25
+ ) {
26
+ node.properties.href = basePath + href;
27
+ }
28
+ }
29
+ node.children?.forEach(walk);
30
+ };
31
+ if (basePath) walk(tree);
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Wrap every `<table>` in a `<div class="table-wrapper">` so it can be a rounded,
37
+ * horizontally-scrollable card (a bare table can't clip rounded corners while
38
+ * also scrolling on overflow).
39
+ */
40
+ function rehypeTableWrapper() {
41
+ return (tree) => {
42
+ const walk = (node) => {
43
+ if (!node.children) return;
44
+ node.children = node.children.map((child) => {
45
+ if (child.type === 'element' && child.tagName === 'table') {
46
+ return {
47
+ type: 'element',
48
+ tagName: 'div',
49
+ properties: { className: ['table-wrapper'] },
50
+ children: [child]
51
+ };
52
+ }
53
+ walk(child);
54
+ return child;
55
+ });
56
+ };
57
+ walk(tree);
58
+ };
59
+ }
60
+
61
+ function rehypeExternalLinks() {
62
+ return (tree) => {
63
+ const walk = (node) => {
64
+ if (node.type === 'element' && node.tagName === 'a') {
65
+ const href = node.properties?.href;
66
+ if (typeof href === 'string' && /^https?:\/\//.test(href)) {
67
+ node.properties.target = '_blank';
68
+ node.properties.rel = 'noreferrer noopener';
69
+ }
70
+ }
71
+ node.children?.forEach(walk);
72
+ };
73
+ walk(tree);
74
+ };
75
+ }
76
+
77
+ // Dual theme: light tokens are inlined, dark tokens exposed as CSS vars that
78
+ // `layout.css` switches on under `.dark`.
79
+ const themes = { light: 'github-light', dark: 'vesper' };
80
+
81
+ // Languages preloaded into the highlighter. Anything else falls back to plain
82
+ // text (see the try/catch below).
83
+ const langs = [
84
+ 'svelte',
85
+ 'typescript',
86
+ 'javascript',
87
+ 'json',
88
+ 'yaml',
89
+ 'bash',
90
+ 'shell',
91
+ 'html',
92
+ 'css',
93
+ 'markdown'
94
+ ];
95
+
96
+ // Lazily create a single shared highlighter (loading grammars + themes is
97
+ // expensive, so do it once).
98
+ let highlighterPromise;
99
+ function getHighlighter() {
100
+ if (!highlighterPromise) {
101
+ highlighterPromise = createHighlighter({ themes: Object.values(themes), langs });
102
+ }
103
+ return highlighterPromise;
104
+ }
105
+
106
+ /** Escape HTML special chars for safe interpolation into the title bar. */
107
+ function escapeHtml(str) {
108
+ return str
109
+ .replace(/&/g, '&amp;')
110
+ .replace(/</g, '&lt;')
111
+ .replace(/>/g, '&gt;')
112
+ .replace(/"/g, '&quot;');
113
+ }
114
+
115
+ /** Pull `title="…"` (or single-quoted) out of a fence's meta string. */
116
+ function parseTitle(meta) {
117
+ const match = meta.match(/title=(?:"([^"]*)"|'([^']*)')/);
118
+ return match ? (match[1] ?? match[2]) : null;
119
+ }
120
+
121
+ /**
122
+ * Parse a `{1,3-5}` line-range expression from a fence's meta into a Set of
123
+ * 1-based line numbers to highlight.
124
+ */
125
+ function parseHighlightLines(meta) {
126
+ const match = meta.match(/\{([\d,\s-]+)\}/);
127
+ const lines = new Set();
128
+ if (!match) return lines;
129
+ for (const part of match[1].split(',')) {
130
+ const range = part.trim();
131
+ if (!range) continue;
132
+ const [start, end] = range.split('-').map((n) => parseInt(n, 10));
133
+ for (let i = start; i <= (end ?? start); i++) lines.add(i);
134
+ }
135
+ return lines;
136
+ }
137
+
138
+ /** Whether the fence asked for line numbers (```ts showLineNumbers). */
139
+ function hasLineNumbers(meta) {
140
+ return /\bshowLineNumbers\b/.test(meta);
141
+ }
142
+
143
+ /** Whether the fence asked for Twoslash type annotations (```ts twoslash). */
144
+ function hasTwoslash(meta) {
145
+ return /\btwoslash\b/.test(meta);
146
+ }
147
+
148
+ const twoslash = transformerTwoslash({ explicitTrigger: true });
149
+
150
+ /**
151
+ * The YAML grammar gives the `---` document markers no scope, so they inherit
152
+ * the bright default foreground and read as unhighlighted. Dim them to a subtle
153
+ * token that adapts to both themes.
154
+ */
155
+ function transformerDimYamlMarkers() {
156
+ return {
157
+ name: 'axerity:dim-yaml-markers',
158
+ span(node) {
159
+ const text = node.children?.[0];
160
+ if (text && text.type === 'text' && text.value.trim() === '---') {
161
+ node.properties = { ...node.properties, style: 'color:var(--fg-subtle)' };
162
+ }
163
+ }
164
+ };
165
+ }
166
+
167
+ /** Shiki transformer: tag the requested lines with a `highlighted` class. */
168
+ function transformerHighlightLines(lines) {
169
+ return {
170
+ name: 'axerity:highlight-lines',
171
+ line(node, lineNumber) {
172
+ if (lines.has(lineNumber)) this.addClassToHast(node, 'highlighted');
173
+ }
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Shiki separates lines with literal "\n" text nodes. With block-level lines
179
+ * (needed for full-width highlights) those render as blank lines, so strip
180
+ * them — the copy handler rejoins lines with newlines itself.
181
+ */
182
+ function transformerRemoveLineBreaks() {
183
+ return {
184
+ name: 'axerity:remove-line-breaks',
185
+ code(node) {
186
+ node.children = node.children.filter(
187
+ (child) => !(child.type === 'text' && child.value === '\n')
188
+ );
189
+ }
190
+ };
191
+ }
192
+
193
+ /** Shiki transformer: flag the block so CSS counters can number the lines. */
194
+ function transformerLineNumbers() {
195
+ return {
196
+ name: 'axerity:line-numbers',
197
+ pre(node) {
198
+ this.addClassToHast(node, 'has-line-numbers');
199
+ }
200
+ };
201
+ }
202
+
203
+ // Static copy button injected into every code block; clicks are handled by a
204
+ // delegated listener in DocsLayout (the markup carries no state itself).
205
+ const COPY_BUTTON =
206
+ '<button class="copy-button" type="button" aria-label="Copy code">' +
207
+ '<svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>' +
208
+ '<svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>' +
209
+ '</button>';
210
+
211
+ export default defineMDSveXConfig({
212
+ extensions: ['.svx', '.md'],
213
+ // Give every heading a stable id so the TOC can anchor + scroll-spy to it.
214
+ rehypePlugins: [rehypeSlug, rehypeTableWrapper, rehypeExternalLinks, rehypeBasePath],
215
+ highlight: {
216
+ highlighter: async (code, lang, meta) => {
217
+ if (lang === 'mermaid') {
218
+ const graph = escapeSvelte(
219
+ code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
220
+ );
221
+ return `{@html \`<pre class="mermaid">${graph}</pre>\`}`;
222
+ }
223
+
224
+ const highlighter = await getHighlighter();
225
+ const language = (lang && highlighter.getLoadedLanguages().includes(lang) && lang) || 'text';
226
+ meta = meta || '';
227
+
228
+ const title = parseTitle(meta);
229
+ const highlightLines = parseHighlightLines(meta);
230
+ const twoslashEnabled = hasTwoslash(meta);
231
+
232
+ const transformers = [];
233
+ if (twoslashEnabled) transformers.push(twoslash);
234
+ if (language === 'yaml') transformers.push(transformerDimYamlMarkers());
235
+ transformers.push(transformerRemoveLineBreaks());
236
+ if (highlightLines.size) transformers.push(transformerHighlightLines(highlightLines));
237
+ if (hasLineNumbers(meta)) transformers.push(transformerLineNumbers());
238
+
239
+ const html = highlighter.codeToHtml(code, {
240
+ lang: language,
241
+ meta: { __raw: meta },
242
+ themes,
243
+ defaultColor: 'light',
244
+ transformers
245
+ });
246
+
247
+ // With a title, the copy button lives in the header bar; otherwise it
248
+ // floats in the top-right corner of the block.
249
+ const header = title
250
+ ? `<div class="code-header"><span class="code-title">${escapeHtml(title)}</span>${COPY_BUTTON}</div>`
251
+ : '';
252
+ const blockClass =
253
+ (title ? 'code-block has-header' : 'code-block') + (twoslashEnabled ? ' has-twoslash' : '');
254
+ const floatingButton = title ? '' : COPY_BUTTON;
255
+ const wrapped = `<div class="${blockClass}">${header}${floatingButton}${html}</div>`;
256
+
257
+ // Escape so Svelte doesn't try to parse `{`, `}`, or backticks in the code.
258
+ return `{@html \`${escapeSvelte(wrapped)}\`}`;
259
+ }
260
+ }
261
+ });
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "@axerity/cli",
3
+ "version": "0.1.0",
4
+ "description": "A documentation site generator built with Svelte.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "author": "aelpxy",
11
+ "homepage": "https://axerity.com",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/axerity/axerity.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/axerity/axerity/issues"
18
+ },
19
+ "keywords": [
20
+ "documentation",
21
+ "docs",
22
+ "static-site-generator",
23
+ "ssg",
24
+ "sveltekit",
25
+ "svelte",
26
+ "markdown",
27
+ "mdsvex",
28
+ "openapi"
29
+ ],
30
+ "engines": {
31
+ "node": ">=24"
32
+ },
33
+ "bin": {
34
+ "axerity": "./bin/axerity.js"
35
+ },
36
+ "files": [
37
+ "bin",
38
+ "scripts",
39
+ "src",
40
+ "static",
41
+ "svelte.config.js",
42
+ "vite.config.ts",
43
+ "mdsvex.config.js",
44
+ "tsconfig.json",
45
+ "axerity.default.json",
46
+ "axerity.schema.json",
47
+ "README.md"
48
+ ],
49
+ "dependencies": {
50
+ "@fontsource-variable/geist": "^5.2.9",
51
+ "@fontsource-variable/geist-mono": "^5.2.8",
52
+ "@lucide/svelte": "^1.17.0",
53
+ "@orama/orama": "^3.1.18",
54
+ "@resvg/resvg-js": "^2.6.2",
55
+ "@shikijs/twoslash": "^4.1.0",
56
+ "@sveltejs/adapter-static": "^3.0.10",
57
+ "@sveltejs/kit": "^2.61.1",
58
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
59
+ "@tailwindcss/forms": "^0.5.11",
60
+ "@tailwindcss/typography": "^0.5.19",
61
+ "@tailwindcss/vite": "^4.3.0",
62
+ "mdsvex": "^0.12.7",
63
+ "mermaid": "^11.15.0",
64
+ "rehype-slug": "^6.0.0",
65
+ "satori": "^0.26.0",
66
+ "shiki": "^4.1.0",
67
+ "svelte": "^5.56.0",
68
+ "tailwindcss": "^4.3.0",
69
+ "twoslash": "^0.3.8",
70
+ "typescript": "^6.0.3",
71
+ "vite": "^8.0.14",
72
+ "yaml": "^2.9.0"
73
+ },
74
+ "devDependencies": {
75
+ "@eslint/compat": "^2.1.0",
76
+ "@eslint/js": "^10.0.1",
77
+ "@types/node": "^25.9.1",
78
+ "eslint": "^10.4.1",
79
+ "eslint-config-prettier": "^10.1.8",
80
+ "eslint-plugin-svelte": "^3.19.0",
81
+ "globals": "^17.6.0",
82
+ "prettier": "^3.8.3",
83
+ "prettier-plugin-svelte": "^4.0.1",
84
+ "prettier-plugin-tailwindcss": "^0.8.0",
85
+ "svelte-check": "^4.4.8",
86
+ "typescript-eslint": "^8.60.0"
87
+ },
88
+ "scripts": {
89
+ "axerity": "node ./bin/axerity.js",
90
+ "postversion": "git push --follow-tags",
91
+ "prepare-engine": "node scripts/prepare-engine.mjs",
92
+ "predev": "node scripts/prepare-engine.mjs",
93
+ "dev": "vite dev",
94
+ "prebuild": "node scripts/prepare-engine.mjs",
95
+ "build": "vite build",
96
+ "prepreview": "node scripts/prepare-engine.mjs",
97
+ "preview": "vite preview",
98
+ "check": "node scripts/prepare-engine.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
99
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
100
+ "lint": "prettier --check . --ignore-path .gitignore --ignore-path .prettierignore && eslint .",
101
+ "format": "prettier --write . --ignore-path .gitignore --ignore-path .prettierignore"
102
+ }
103
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, symlinkSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
7
+ const symlinkType = process.platform === 'win32' ? 'junction' : 'dir';
8
+
9
+ const demo = join(root, 'src', 'content', 'demo');
10
+ const docs = join(root, 'src', 'content', 'docs');
11
+ const defaultConfig = join(root, 'axerity.default.json');
12
+ const config = join(root, 'axerity.json');
13
+
14
+ rmSync(docs, { recursive: true, force: true });
15
+ mkdirSync(docs, { recursive: true });
16
+ for (const entry of readdirSync(demo)) {
17
+ symlinkSync(join(demo, entry), join(docs, entry), symlinkType);
18
+ }
19
+
20
+ if (existsSync(defaultConfig)) cpSync(defaultConfig, config);