@aravindc26/velu 0.8.0 → 0.9.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.
package/src/build.ts CHANGED
@@ -1,18 +1,27 @@
1
- import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, rmSync } from "node:fs";
2
- import { resolve, join, dirname } from "node:path";
3
- import { generateThemeCss, type ThemeConfig, type VeluColors, type VeluStyling } from "./themes.js";
1
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSync, rmSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { generateThemeCss, type VeluColors, type VeluStyling } from "./themes.js";
5
+
6
+ // ── Engine directory (shipped with the CLI package) ──────────────────────────
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const ENGINE_DIR = join(__dirname, "engine");
4
10
 
5
11
  // ── Types (used only by build.ts for page copying) ─────────────────────────────
6
12
 
7
13
  interface VeluGroup {
8
14
  group: string;
9
15
  slug: string;
16
+ icon?: string;
17
+ expanded?: boolean;
10
18
  pages: (string | VeluGroup)[];
11
19
  }
12
20
 
13
21
  interface VeluTab {
14
22
  tab: string;
15
23
  slug: string;
24
+ icon?: string;
16
25
  href?: string;
17
26
  pages?: string[];
18
27
  groups?: VeluGroup[];
@@ -45,54 +54,101 @@ function pageBasename(page: string): string {
45
54
  return page.split("/").pop()!;
46
55
  }
47
56
 
48
- function collectPagesFromGroup(group: VeluGroup): string[] {
49
- const pages: string[] = [];
50
- for (const item of group.pages) {
51
- if (typeof item === "string") pages.push(item);
52
- else pages.push(...collectPagesFromGroup(item));
53
- }
54
- return pages;
55
- }
56
-
57
57
  interface PageMapping {
58
58
  src: string; // original page reference (file path without .md)
59
- dest: string; // slug-based destination path (slug/basename)
59
+ dest: string; // destination path under content/docs (without extension)
60
60
  }
61
61
 
62
- function buildPageMap(config: VeluConfig): PageMapping[] {
63
- const mappings: PageMapping[] = [];
62
+ interface MetaFile {
63
+ dir: string;
64
+ data: Record<string, unknown>;
65
+ }
66
+
67
+ interface BuildArtifacts {
68
+ pageMap: PageMapping[];
69
+ metaFiles: MetaFile[];
70
+ firstPage: string;
71
+ }
72
+
73
+ function buildArtifacts(config: VeluConfig): BuildArtifacts {
74
+ const pageMap: PageMapping[] = [];
75
+ const metaFiles: MetaFile[] = [];
76
+ const rootTabs = config.navigation.tabs.filter((tab) => !tab.href);
77
+ const rootPages = rootTabs.map((tab) => tab.slug);
78
+ let firstPage = "quickstart";
79
+ let hasFirstPage = false;
80
+
81
+ function trackFirstPage(dest: string) {
82
+ if (!hasFirstPage) {
83
+ firstPage = dest;
84
+ hasFirstPage = true;
85
+ }
86
+ }
87
+
88
+ function addGroup(group: VeluGroup, parentDir: string) {
89
+ const groupDir = `${parentDir}/${group.slug}`;
90
+ const pages: string[] = [];
64
91
 
65
- function addPagesFromGroup(group: VeluGroup, tabSlug: string) {
66
92
  for (const item of group.pages) {
67
93
  if (typeof item === "string") {
68
- mappings.push({ src: item, dest: `${tabSlug}/${group.slug}/${pageBasename(item)}` });
94
+ const basename = pageBasename(item);
95
+ const dest = `${groupDir}/${basename}`;
96
+ pageMap.push({ src: item, dest });
97
+ pages.push(basename);
98
+ trackFirstPage(dest);
69
99
  } else {
70
- addPagesFromGroup(item, tabSlug);
100
+ addGroup(item, groupDir);
101
+ pages.push(item.slug);
71
102
  }
72
103
  }
104
+
105
+ const groupMeta: Record<string, unknown> = {
106
+ title: group.group,
107
+ pages,
108
+ defaultOpen: group.expanded !== false,
109
+ };
110
+
111
+ if (group.icon) groupMeta.icon = group.icon;
112
+
113
+ metaFiles.push({ dir: groupDir, data: groupMeta });
73
114
  }
74
115
 
75
- for (const tab of config.navigation.tabs) {
76
- if (tab.href) continue;
77
- // Direct pages in tab use tab slug
78
- if (tab.pages) {
79
- for (const page of tab.pages) {
80
- mappings.push({ src: page, dest: `${tab.slug}/${pageBasename(page)}` });
81
- }
82
- }
83
- // Groups inside tab: <tab-slug>/<group-slug>/<page-basename>
116
+ for (const tab of rootTabs) {
117
+ const tabPages: string[] = [];
118
+
84
119
  if (tab.groups) {
85
120
  for (const group of tab.groups) {
86
- addPagesFromGroup(group, tab.slug);
121
+ addGroup(group, tab.slug);
122
+ tabPages.push(group.slug);
123
+ }
124
+ }
125
+
126
+ if (tab.pages) {
127
+ for (const page of tab.pages) {
128
+ const basename = pageBasename(page);
129
+ const dest = `${tab.slug}/${basename}`;
130
+ pageMap.push({ src: page, dest });
131
+ tabPages.push(basename);
132
+ trackFirstPage(dest);
87
133
  }
88
134
  }
135
+
136
+ const tabMeta: Record<string, unknown> = {
137
+ title: tab.tab,
138
+ root: true,
139
+ pages: tabPages,
140
+ };
141
+
142
+ if (tab.icon) tabMeta.icon = tab.icon;
143
+
144
+ metaFiles.push({ dir: tab.slug, data: tabMeta });
89
145
  }
90
146
 
91
- return mappings;
92
- }
147
+ if (rootPages.length > 0) {
148
+ metaFiles.push({ dir: "", data: { pages: rootPages } });
149
+ }
93
150
 
94
- function collectAllPages(config: VeluConfig): string[] {
95
- return buildPageMap(config).map(m => m.src);
151
+ return { pageMap, metaFiles, firstPage };
96
152
  }
97
153
 
98
154
  // ── Build ──────────────────────────────────────────────────────────────────────
@@ -105,22 +161,34 @@ function build(docsDir: string, outDir: string) {
105
161
  rmSync(outDir, { recursive: true, force: true });
106
162
  }
107
163
 
108
- // Create directories
109
- mkdirSync(join(outDir, "src", "content", "docs"), { recursive: true });
110
- mkdirSync(join(outDir, "src", "components"), { recursive: true });
111
- mkdirSync(join(outDir, "src", "lib"), { recursive: true });
112
- mkdirSync(join(outDir, "src", "styles"), { recursive: true });
164
+ // ── 1. Copy engine static files ──────────────────────────────────────────
165
+ cpSync(ENGINE_DIR, outDir, { recursive: true });
166
+ // Remove legacy Astro template leftovers if present in the packaged engine.
167
+ rmSync(join(outDir, "src"), { recursive: true, force: true });
168
+ console.log("📦 Copied engine files");
169
+
170
+ // ── 2. Create additional directories ─────────────────────────────────────
171
+ mkdirSync(join(outDir, "content", "docs"), { recursive: true });
113
172
  mkdirSync(join(outDir, "public"), { recursive: true });
114
173
 
115
- // ── 1. Copy velu.json into the Astro project ─────────────────────────────
174
+ // ── 3. Copy velu.json into the generated project ─────────────────────────
116
175
  copyFileSync(join(docsDir, "velu.json"), join(outDir, "velu.json"));
117
176
  console.log("📋 Copied velu.json");
118
177
 
119
- // ── 2. Copy all referenced .md files (slug-based destinations) ───────────
120
- const pageMap = buildPageMap(config);
178
+ // ── 4. Build content + metadata artifacts ────────────────────────────────
179
+ const { pageMap, metaFiles, firstPage } = buildArtifacts(config);
180
+
181
+ // 4a) Write folder meta.json files (tabs/groups ordering & labels)
182
+ for (const meta of metaFiles) {
183
+ const metaPath = join(outDir, "content", "docs", meta.dir, "meta.json");
184
+ mkdirSync(dirname(metaPath), { recursive: true });
185
+ writeFileSync(metaPath, JSON.stringify(meta.data, null, 2) + "\n", "utf-8");
186
+ }
187
+
188
+ // 4b) Copy all referenced .md files (slug-based destinations)
121
189
  for (const { src, dest } of pageMap) {
122
190
  const srcPath = join(docsDir, `${src}.md`);
123
- const destPath = join(outDir, "src", "content", "docs", `${dest}.md`);
191
+ const destPath = join(outDir, "content", "docs", `${dest}.mdx`);
124
192
 
125
193
  if (!existsSync(srcPath)) {
126
194
  console.warn(`⚠️ Missing: ${srcPath}`);
@@ -141,1483 +209,33 @@ function build(docsDir: string, outDir: string) {
141
209
 
142
210
  writeFileSync(destPath, content, "utf-8");
143
211
  }
144
- console.log(`📄 Copied ${pageMap.length} pages`);
145
-
146
- // ── 3. Generate src/lib/velu.ts — the single source of truth ──────────────
147
- // This module reads velu.json at Astro build/render time. No hardcoded data.
148
- const veluLib = `import { readFileSync } from 'node:fs';
149
- import { resolve } from 'node:path';
150
-
151
- // ── Types ───────────────────────────────────────────────────────────────────
152
-
153
- export interface VeluGroup {
154
- group: string;
155
- slug: string;
156
- icon?: string;
157
- tag?: string;
158
- expanded?: boolean;
159
- pages: (string | VeluGroup)[];
160
- }
161
-
162
- export interface VeluTab {
163
- tab: string;
164
- slug: string;
165
- icon?: string;
166
- href?: string;
167
- pages?: string[];
168
- groups?: VeluGroup[];
169
- }
170
-
171
- export interface VeluConfig {
172
- $schema?: string;
173
- theme?: string;
174
- colors?: { primary?: string; light?: string; dark?: string };
175
- appearance?: 'system' | 'light' | 'dark';
176
- styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
177
- navigation: {
178
- tabs: VeluTab[];
179
- };
180
- }
181
-
182
- export interface TabMeta {
183
- label: string;
184
- icon?: string;
185
- href?: string;
186
- slugs: string[];
187
- firstPage?: string;
188
- }
189
-
190
- // ── Load config ─────────────────────────────────────────────────────────────
191
-
192
- let _cachedConfig: VeluConfig | null = null;
193
-
194
- export function loadVeluConfig(): VeluConfig {
195
- if (_cachedConfig) return _cachedConfig;
196
- const configPath = resolve(process.cwd(), 'velu.json');
197
- const raw = readFileSync(configPath, 'utf-8');
198
- _cachedConfig = JSON.parse(raw);
199
- return _cachedConfig!;
200
- }
201
-
202
- // ── Helpers ─────────────────────────────────────────────────────────────────
203
-
204
- function pageBasename(page: string): string {
205
- return page.split('/').pop()!;
206
- }
207
-
208
- /** Convert a group to a Starlight sidebar entry, using slug-based page paths */
209
- function veluGroupToSidebar(group: VeluGroup, tabSlug: string): any {
210
- const items: any[] = [];
211
- for (const item of group.pages) {
212
- if (typeof item === 'string') {
213
- items.push(tabSlug + '/' + group.slug + '/' + pageBasename(item));
214
- } else {
215
- items.push(veluGroupToSidebar(item, tabSlug));
216
- }
217
- }
218
- const result: any = { label: group.group, items };
219
- if (group.tag) result.badge = group.tag;
220
- if (group.expanded === false) result.collapsed = true;
221
- return result;
222
- }
223
-
224
- /** Get the first page dest path for a tab */
225
- function firstTabPage(tab: VeluTab): string | undefined {
226
- if (tab.pages && tab.pages.length > 0) {
227
- return tab.slug + '/' + pageBasename(tab.pages[0]);
228
- }
229
- if (tab.groups) {
230
- for (const g of tab.groups) {
231
- const first = firstGroupPage(g, tab.slug);
232
- if (first) return first;
233
- }
234
- }
235
- return undefined;
236
- }
237
-
238
- function firstGroupPage(group: VeluGroup, tabSlug: string): string | undefined {
239
- for (const item of group.pages) {
240
- if (typeof item === 'string') return tabSlug + '/' + group.slug + '/' + pageBasename(item);
241
- const nested = firstGroupPage(item, tabSlug);
242
- if (nested) return nested;
243
- }
244
- return undefined;
245
- }
246
-
247
- // ── Public API ──────────────────────────────────────────────────────────────
248
-
249
- /** Build the full Starlight sidebar array from velu.json */
250
- export function getSidebar(): any[] {
251
- const config = loadVeluConfig();
252
- const sidebar: any[] = [];
253
-
254
- for (const tab of config.navigation.tabs) {
255
- if (tab.href) continue;
256
- const items: any[] = [];
257
- if (tab.groups) for (const g of tab.groups) items.push(veluGroupToSidebar(g, tab.slug));
258
- if (tab.pages) {
259
- for (const p of tab.pages) items.push(tab.slug + '/' + pageBasename(p));
260
- }
261
- sidebar.push({ label: tab.tab, items });
262
- }
263
-
264
- return sidebar;
265
- }
266
-
267
- /** Get tab metadata for the header navigation */
268
- export function getTabs(): TabMeta[] {
269
- const config = loadVeluConfig();
270
- const tabs: TabMeta[] = [];
271
-
272
- for (const tab of config.navigation.tabs) {
273
- if (tab.href) {
274
- tabs.push({ label: tab.tab, icon: tab.icon, href: tab.href, slugs: [] });
275
- } else {
276
- tabs.push({
277
- label: tab.tab,
278
- icon: tab.icon,
279
- slugs: [tab.slug],
280
- firstPage: firstTabPage(tab),
281
- });
282
- }
283
- }
284
-
285
- return tabs;
286
- }
287
-
288
- /** Get the mapping of slug → sidebar group labels for filtering.
289
- * Maps every slug (tab, group, nested group) to the labels that should be visible. */
290
- export function getTabSidebarMap(): Record<string, string[]> {
291
- const config = loadVeluConfig();
292
- const map: Record<string, string[]> = {};
293
-
294
- for (const tab of config.navigation.tabs) {
295
- if (tab.href) continue;
296
- map[tab.slug] = [tab.tab];
297
- }
212
+ console.log(`📄 Generated ${pageMap.length} pages + ${metaFiles.length} navigation meta files`);
298
213
 
299
- return map;
300
- }
301
- `;
302
- writeFileSync(join(outDir, "src", "lib", "velu.ts"), veluLib, "utf-8");
303
- console.log("📚 Generated config module");
304
-
305
- // ── 4. Generate theme CSS ─────────────────────────────────────────────────
214
+ // ── 5. Generate theme CSS (dynamic — depends on user config) ─────────────
306
215
  const themeCss = generateThemeCss({
307
216
  theme: config.theme,
308
217
  colors: config.colors,
309
218
  appearance: config.appearance,
310
219
  styling: config.styling,
311
220
  });
312
- writeFileSync(join(outDir, "src", "styles", "velu-theme.css"), themeCss, "utf-8");
221
+ writeFileSync(join(outDir, "app", "velu-theme.css"), themeCss, "utf-8");
313
222
  console.log(`🎨 Generated theme: ${config.theme || "mint"}`);
314
223
 
315
- // ── 5. Generate site config ────────────────────────────────────────────────
316
- // Build expressiveCode config for code block themes
317
- let expressiveCodeConfig = "";
318
- if (config.styling?.codeblocks?.theme) {
319
- const cbt = config.styling.codeblocks.theme;
320
- if (typeof cbt === "string") {
321
- expressiveCodeConfig = `\n expressiveCode: { themes: ['${cbt}'] },`;
322
- } else {
323
- expressiveCodeConfig = `\n expressiveCode: { themes: ['${cbt.dark}', '${cbt.light}'] },`;
324
- }
325
- }
326
-
327
- const astroConfig = `import { defineConfig } from 'astro/config';
328
- import starlight from '@astrojs/starlight';
329
- import { ion } from 'starlight-ion-theme';
330
- import { getSidebar } from './src/lib/velu.ts';
331
-
332
- export default defineConfig({
333
- devToolbar: { enabled: false },
334
- integrations: [
335
- starlight({
336
- title: 'Velu Docs',
337
- plugins: [ion()],
338
- components: {
339
- Sidebar: './src/components/Sidebar.astro',
340
- PageTitle: './src/components/PageTitle.astro',
341
- Footer: './src/components/Footer.astro',
342
- },
343
- customCss: ['./src/styles/velu-theme.css', './src/styles/tabs.css', './src/styles/assistant.css'],${expressiveCodeConfig}
344
- sidebar: getSidebar(),
345
- }),
346
- ],
347
- });
348
- `;
349
- writeFileSync(join(outDir, "_config.mjs"), astroConfig, "utf-8");
350
- console.log("⚙️ Generated site config");
351
-
352
- // ── 6. Generate Sidebar.astro — tabs at top + filtered content ─────────────
353
- const sidebarComponent = `---
354
- import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter';
355
- import SidebarSublist from '@astrojs/starlight/components/SidebarSublist.astro';
356
- import { getTabs, getTabSidebarMap } from '../lib/velu.ts';
357
-
358
- const tabs = getTabs();
359
- const tabSidebarMap = getTabSidebarMap();
360
- const currentPath = Astro.url.pathname;
361
-
362
- function isTabActive(tab: any, path: string): boolean {
363
- if (tab.href) return false;
364
- if (!tab.slugs || tab.slugs.length === 0) return false;
365
- return tab.slugs.some((s: string) => path.startsWith('/' + s + '/'));
366
- }
367
-
368
- function getActiveTabSlug(path: string): string {
369
- const allPrefixes = Object.keys(tabSidebarMap);
370
- for (const prefix of allPrefixes) {
371
- if (path.startsWith('/' + prefix + '/')) return prefix;
372
- }
373
- return '';
374
- }
375
-
376
- const activeSlug = getActiveTabSlug(currentPath);
377
- const visibleLabels = new Set(tabSidebarMap[activeSlug] || []);
378
-
379
- const { sidebar } = Astro.locals.starlightRoute;
380
- const filteredSidebar = sidebar.filter((entry: any) => {
381
- if (entry.type === 'group') return visibleLabels.has(entry.label);
382
- return false;
383
- });
384
- ---
385
-
386
- <div class="velu-sidebar-tabs">
387
- {tabs.map((tab) => {
388
- if (tab.href) {
389
- return (
390
- <a href={tab.href} class="velu-sidebar-tab" target="_blank" rel="noopener noreferrer">
391
- {tab.icon && <span class="velu-sidebar-tab-icon" data-icon={tab.icon} />}
392
- <span>{tab.label}</span>
393
- <svg class="velu-external-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 17L17 7M17 7H7M17 7V17"/></svg>
394
- </a>
395
- );
396
- }
397
- const active = isTabActive(tab, currentPath);
398
- const href = tab.firstPage ? '/' + tab.firstPage + '/' : '/';
399
- return (
400
- <a href={href} class:list={['velu-sidebar-tab', { active }]}>
401
- {tab.icon && <span class="velu-sidebar-tab-icon" data-icon={tab.icon} />}
402
- <span>{tab.label}</span>
403
- </a>
404
- );
405
- })}
406
- </div>
407
-
408
- <SidebarSublist sublist={filteredSidebar} />
409
-
410
- <div class="md:sl-hidden">
411
- <MobileMenuFooter />
412
- </div>
413
- `;
414
- writeFileSync(join(outDir, "src", "components", "Sidebar.astro"), sidebarComponent, "utf-8");
415
- console.log("📋 Generated sidebar component");
416
-
417
- // ── 7. Generate PageTitle.astro — title row with copy page button ────────
418
- const pageTitleComponent = `---
419
- const currentUrl = Astro.url.href;
420
- const title = Astro.locals.starlightRoute.entry.data.title;
421
- ---
422
-
423
- <div class="velu-title-row">
424
- <h1 id="_top">{title}</h1>
425
- <div class="velu-copy-page-container">
426
- <div class="velu-copy-split-btn">
427
- <button class="velu-copy-main-btn" data-action="direct-copy">
428
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
429
- <span class="velu-copy-label">Copy page</span>
430
- </button>
431
- <span class="velu-copy-sep"></span>
432
- <button class="velu-copy-caret-btn" aria-expanded="false" aria-haspopup="true">
433
- <svg class="velu-copy-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
434
- </button>
435
- </div>
436
- <div class="velu-copy-dropdown" hidden>
437
- <button class="velu-copy-option" data-action="copy">
438
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
439
- <div>
440
- <div class="velu-copy-option-title">Copy page</div>
441
- <div class="velu-copy-option-desc">Copy page as Markdown for LLMs</div>
442
- </div>
443
- </button>
444
- <a class="velu-copy-option" href={\`https://chatgpt.com/?prompt=Read+from+\${encodeURIComponent(currentUrl)}+so+I+can+ask+questions+about+it.\`} target="_blank" rel="noopener noreferrer">
445
- <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z"/></svg>
446
- <div>
447
- <div class="velu-copy-option-title">Open in ChatGPT <span class="velu-external-arrow">&nearr;</span></div>
448
- <div class="velu-copy-option-desc">Ask questions about this page</div>
449
- </div>
450
- </a>
451
- <a class="velu-copy-option" href={\`https://claude.ai/new?q=Read+from+\${encodeURIComponent(currentUrl)}+so+I+can+ask+questions+about+it.\`} target="_blank" rel="noopener noreferrer">
452
- <svg width="18" height="18" viewBox="0 0 200 200" style="overflow:visible" fill="currentColor"><path d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"/></svg>
453
- <div>
454
- <div class="velu-copy-option-title">Open in Claude <span class="velu-external-arrow">&nearr;</span></div>
455
- <div class="velu-copy-option-desc">Ask questions about this page</div>
456
- </div>
457
- </a>
458
- </div>
459
- </div>
460
- </div>
461
-
462
- <script is:inline>
463
- (function init() {
464
- var caretBtn = document.querySelector('.velu-copy-caret-btn');
465
- var mainBtn = document.querySelector('.velu-copy-main-btn');
466
- var dropdown = document.querySelector('.velu-copy-dropdown');
467
- var label = document.querySelector('.velu-copy-label');
468
- if (!caretBtn || !mainBtn || !dropdown) return;
469
-
470
- function doCopy() {
471
- if (label) label.textContent = 'Copying...';
472
- var titleEl = document.querySelector('#_top');
473
- var article = document.querySelector('.sl-markdown-content') || document.querySelector('.content-panel') || document.querySelector('main');
474
- var text = '';
475
- if (titleEl) text = '# ' + titleEl.textContent + '\\n\\n';
476
- if (article) text += article.innerText;
477
- if (text) {
478
- navigator.clipboard.writeText(text).then(function() {
479
- if (label) label.textContent = 'Copied!';
480
- setTimeout(function() { if (label) label.textContent = 'Copy page'; }, 1500);
481
- });
482
- }
483
- dropdown.hidden = true;
484
- caretBtn.setAttribute('aria-expanded', 'false');
485
- }
486
-
487
- mainBtn.onclick = function(e) { e.stopPropagation(); doCopy(); };
488
-
489
- caretBtn.onclick = function(e) {
490
- e.stopPropagation();
491
- var open = dropdown.hidden;
492
- dropdown.hidden = !open;
493
- caretBtn.setAttribute('aria-expanded', String(open));
494
- };
495
-
496
- document.addEventListener('click', function() {
497
- dropdown.hidden = true;
498
- caretBtn.setAttribute('aria-expanded', 'false');
499
- });
500
-
501
- dropdown.onclick = function(e) { e.stopPropagation(); };
502
-
503
- var copyOpt = dropdown.querySelector('[data-action="copy"]');
504
- if (copyOpt) {
505
- copyOpt.onclick = function() { doCopy(); };
506
- }
507
- })();
508
- </script>
509
-
510
- <!-- AI Assistant Widget -->
511
- <div class="velu-ask-bar" id="veluAskBar">
512
- <div class="velu-ask-bar-inner">
513
- <input type="text" class="velu-ask-input" id="veluAskInput" placeholder="Ask a question..." autocomplete="off" />
514
- <button class="velu-ask-submit" id="veluAskSubmit" aria-label="Send">
515
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
516
- </button>
517
- </div>
518
- </div>
519
-
520
- <div class="velu-assistant-panel velu-panel-closed" id="veluAssistantPanel">
521
- <div class="velu-assistant-header">
522
- <span class="velu-assistant-title">Assistant</span>
523
- <div class="velu-assistant-actions">
524
- <button class="velu-assistant-action" data-velu-action="expand" title="Expand" aria-label="Expand assistant" type="button">
525
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
526
- </button>
527
- <button class="velu-assistant-action" data-velu-action="reset" title="New chat" aria-label="New chat" type="button">
528
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
529
- </button>
530
- <button class="velu-assistant-action" data-velu-action="close" title="Close" aria-label="Close assistant" type="button">
531
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
532
- </button>
533
- </div>
534
- </div>
535
- <div class="velu-assistant-messages" id="veluAssistantMessages"></div>
536
- <div class="velu-assistant-input-area">
537
- <input type="text" class="velu-assistant-chat-input" id="veluAssistantChatInput" placeholder="Ask a question..." autocomplete="off" />
538
- <button class="velu-assistant-send" id="veluAssistantSend" aria-label="Send">
539
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94l18-8.5a.75.75 0 000-1.38l-18-8.5z"/></svg>
540
- </button>
541
- </div>
542
- </div>
543
-
544
- <script is:inline>
545
- (function veluAssistant() {
546
- var API_BASE = 'https://api.getvelu.com/api/v1/public/ai-assistant';
547
- var state = {
548
- conversationId: null,
549
- conversationToken: null,
550
- lastSeq: 0,
551
- eventSource: null,
552
- expanded: false,
553
- bootstrapped: false
554
- };
555
-
556
- var askBar = document.getElementById('veluAskBar');
557
- var askInput = document.getElementById('veluAskInput');
558
- var askSubmit = document.getElementById('veluAskSubmit');
559
- var panel = document.getElementById('veluAssistantPanel');
560
- var messagesEl = document.getElementById('veluAssistantMessages');
561
- var chatInput = document.getElementById('veluAssistantChatInput');
562
- var sendBtn = document.getElementById('veluAssistantSend');
563
- var closeBtn = document.getElementById('veluAssistantClose');
564
- var expandBtn = document.getElementById('veluAssistantExpand');
565
- var newChatBtn = document.getElementById('veluAssistantNewChat');
566
-
567
- if (!askBar || !panel) return;
568
-
569
- if (panel.parentElement !== document.body) {
570
- document.body.appendChild(panel);
571
- }
572
- if (askBar.parentElement !== document.body) {
573
- document.body.appendChild(askBar);
574
- }
575
-
576
- function saveState() {
577
- try {
578
- sessionStorage.setItem('velu-panel-open', isPanelOpen() ? '1' : '');
579
- sessionStorage.setItem('velu-panel-expanded', state.expanded ? '1' : '');
580
- sessionStorage.setItem('velu-panel-messages', messagesEl.innerHTML);
581
- sessionStorage.setItem('velu-conv-id', state.conversationId || '');
582
- sessionStorage.setItem('velu-conv-token', state.conversationToken || '');
583
- sessionStorage.setItem('velu-last-seq', String(state.lastSeq));
584
- } catch(e) {}
585
- }
586
-
587
- function openPanel() {
588
- panel.classList.remove('velu-panel-closed');
589
- askBar.classList.add('velu-ask-bar-hidden');
590
- document.documentElement.classList.add('velu-assistant-open');
591
- chatInput.focus();
592
- saveState();
593
- }
594
-
595
- function closePanel() {
596
- panel.classList.add('velu-panel-closed');
597
- askBar.classList.remove('velu-ask-bar-hidden');
598
- document.documentElement.classList.remove('velu-assistant-open');
599
- document.documentElement.classList.remove('velu-assistant-wide');
600
- if (state.eventSource) { state.eventSource.close(); state.eventSource = null; }
601
- saveState();
602
- }
603
-
604
- function resetChat() {
605
- state.conversationId = null;
606
- state.conversationToken = null;
607
- state.lastSeq = 0;
608
- if (state.eventSource) { state.eventSource.close(); state.eventSource = null; }
609
- messagesEl.innerHTML = '';
610
- chatInput.value = '';
611
- chatInput.focus();
612
- saveState();
613
- }
614
-
615
- function toggleExpand() {
616
- state.expanded = !state.expanded;
617
- panel.classList.toggle('velu-assistant-expanded', state.expanded);
618
- document.documentElement.classList.toggle('velu-assistant-wide', state.expanded);
619
- saveState();
620
- }
621
-
622
- // Expose to inline onclick handlers
623
- window._veluClosePanel = closePanel;
624
- window._veluResetChat = resetChat;
625
- window._veluToggleExpand = toggleExpand;
626
-
627
- function bootstrap() {
628
- if (state.bootstrapped) return Promise.resolve();
629
- return fetch(API_BASE + '/bootstrap', { credentials: 'include' })
630
- .then(function(r) { return r.json(); })
631
- .then(function(d) { state.bootstrapped = true; })
632
- .catch(function() {});
633
- }
634
-
635
- function isPanelOpen() {
636
- return !panel.classList.contains('velu-panel-closed');
637
- }
638
-
639
- function addMessage(role, content, citations) {
640
- var msgDiv = document.createElement('div');
641
- msgDiv.className = 'velu-msg velu-msg-' + role;
642
- var bubble = document.createElement('div');
643
- bubble.className = 'velu-msg-bubble velu-msg-bubble-' + role;
644
- bubble.innerHTML = formatContent(content, citations || []);
645
- msgDiv.appendChild(bubble);
646
-
647
- if (role === 'assistant' && citations && citations.length > 0) {
648
- var citDiv = document.createElement('div');
649
- citDiv.className = 'velu-msg-citations';
650
- citations.forEach(function(c, i) {
651
- var a = document.createElement('a');
652
- a.href = c.url || c.route_path || '#';
653
- a.className = 'velu-citation-link';
654
- a.textContent = '[' + (i + 1) + '] ' + (c.title || c.route_path || 'Source');
655
- a.target = '_blank';
656
- citDiv.appendChild(a);
657
- });
658
- msgDiv.appendChild(citDiv);
659
- }
660
-
661
- if (role === 'assistant') {
662
- var actions = document.createElement('div');
663
- actions.className = 'velu-msg-actions';
664
- actions.innerHTML = '<button class="velu-msg-action" title="Like"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg></button>'
665
- + '<button class="velu-msg-action" title="Dislike"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/></svg></button>'
666
- + '<button class="velu-msg-action velu-msg-copy" title="Copy"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
667
- + '<button class="velu-msg-action velu-msg-retry" title="Retry"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg></button>';
668
- msgDiv.appendChild(actions);
669
-
670
- var copyBtn = actions.querySelector('.velu-msg-copy');
671
- if (copyBtn) {
672
- copyBtn.onclick = function() {
673
- navigator.clipboard.writeText(content);
674
- copyBtn.title = 'Copied!';
675
- setTimeout(function() { copyBtn.title = 'Copy'; }, 1500);
676
- };
677
- }
678
- }
679
-
680
- messagesEl.appendChild(msgDiv);
681
- messagesEl.scrollTop = messagesEl.scrollHeight;
682
- saveState();
683
- return bubble;
684
- }
685
-
686
- function formatContent(text, citations) {
687
- var html = text
688
- .replace(/&/g, '&amp;')
689
- .replace(/</g, '&lt;')
690
- .replace(/>/g, '&gt;')
691
- .replace(/\\n/g, '<br>')
692
- .replace(/\`([^\`]+)\`/g, '<code>$1</code>')
693
- .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
694
- html = html.replace(/\\[(\\d+)\\]/g, function(m, n) {
695
- var idx = parseInt(n) - 1;
696
- var c = citations[idx];
697
- if (c) {
698
- return '<a href="' + (c.url || c.route_path || '#') + '" class="velu-citation-ref" target="_blank">[' + n + ']</a>';
699
- }
700
- return m;
701
- });
702
- return html;
703
- }
704
-
705
- function addThinking() {
706
- var div = document.createElement('div');
707
- div.className = 'velu-msg velu-msg-assistant';
708
- div.id = 'veluThinking';
709
- div.innerHTML = '<div class="velu-msg-bubble velu-msg-bubble-assistant"><span class="velu-thinking-dots"><span></span><span></span><span></span></span></div>';
710
- messagesEl.appendChild(div);
711
- messagesEl.scrollTop = messagesEl.scrollHeight;
712
- }
713
-
714
- function removeThinking() {
715
- var el = document.getElementById('veluThinking');
716
- if (el) el.remove();
717
- }
718
-
719
- function connectSSE() {
720
- if (state.eventSource) state.eventSource.close();
721
- var url = API_BASE + '/conversations/' + state.conversationId + '/events?after_seq=' + state.lastSeq + '&token=' + encodeURIComponent(state.conversationToken || '');
722
- state.eventSource = new EventSource(url);
723
-
724
- state.eventSource.addEventListener('assistant.completed', function(e) {
725
- removeThinking();
726
- try {
727
- var data = JSON.parse(e.data);
728
- var msg = data.message || data;
729
- if (msg.seq) state.lastSeq = msg.seq;
730
- addMessage('assistant', msg.content || '', msg.citations || []);
731
- } catch(err) {}
732
- });
733
-
734
- state.eventSource.addEventListener('assistant.error', function(e) {
735
- removeThinking();
736
- try {
737
- var data = JSON.parse(e.data);
738
- addMessage('assistant', data.error || 'Something went wrong. Please try again.', []);
739
- } catch(err) {
740
- addMessage('assistant', 'Something went wrong. Please try again.', []);
741
- }
742
- });
743
-
744
- state.eventSource.onerror = function() {};
745
- }
746
-
747
- function sendMessage(text) {
748
- if (!text.trim()) return;
749
- addMessage('user', text);
750
- addThinking();
751
-
752
- bootstrap().then(function() {
753
- return fetch(API_BASE + '/messages', {
754
- method: 'POST',
755
- headers: { 'Content-Type': 'application/json' },
756
- credentials: 'include',
757
- body: JSON.stringify({
758
- message: text,
759
- conversation_id: state.conversationId
760
- })
761
- });
762
- }).then(function(r) {
763
- if (r.status === 429) { removeThinking(); addMessage('assistant', 'Rate limited. Please wait a moment and try again.', []); return; }
764
- return r.json();
765
- }).then(function(data) {
766
- if (!data) return;
767
- if (data.conversation_id) state.conversationId = data.conversation_id;
768
- if (data.conversation_token) state.conversationToken = data.conversation_token;
769
- saveState();
770
- if (!state.eventSource || state.eventSource.readyState === 2) {
771
- connectSSE();
772
- }
773
- }).catch(function() {
774
- removeThinking();
775
- addMessage('assistant', 'Failed to connect. Please try again.', []);
776
- });
777
- }
778
-
779
- function handleAskSubmit() {
780
- var text = askInput.value.trim();
781
- if (!text) return;
782
- askInput.value = '';
783
- openPanel();
784
- sendMessage(text);
785
- }
786
-
787
- function handleChatSubmit() {
788
- var text = chatInput.value.trim();
789
- if (!text) return;
790
- chatInput.value = '';
791
- sendMessage(text);
792
- }
793
-
794
- askInput.onkeydown = function(e) { if (e.key === 'Enter') handleAskSubmit(); };
795
- askSubmit.onclick = handleAskSubmit;
796
- chatInput.onkeydown = function(e) { if (e.key === 'Enter') handleChatSubmit(); };
797
- sendBtn.onclick = handleChatSubmit;
798
-
799
- panel.addEventListener('click', function(e) {
800
- var actionBtn = e.target.closest('[data-velu-action]');
801
- if (!actionBtn) return;
802
- var action = actionBtn.getAttribute('data-velu-action');
803
- if (action === 'close') {
804
- closePanel();
805
- } else if (action === 'expand') {
806
- toggleExpand();
807
- } else if (action === 'reset') {
808
- resetChat();
809
- }
810
- });
811
-
812
- document.addEventListener('click', function(e) {
813
- var actionBtn = e.target.closest('[data-velu-action]');
814
- if (!actionBtn) return;
815
- var action = actionBtn.getAttribute('data-velu-action');
816
- if (action === 'close') {
817
- closePanel();
818
- } else if (action === 'expand') {
819
- toggleExpand();
820
- } else if (action === 'reset') {
821
- resetChat();
822
- }
823
- }, true);
824
-
825
- // Hide ask bar only when user scrolls to the very bottom
826
- window.addEventListener('scroll', function() {
827
- if (isPanelOpen()) return;
828
- var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
829
- var docHeight = document.documentElement.scrollHeight;
830
- var winHeight = window.innerHeight;
831
- if (docHeight <= winHeight + 10) return; // short pages: always show
832
- if (docHeight - scrollTop - winHeight < 60) {
833
- askBar.classList.add('velu-ask-bar-hidden');
834
- } else {
835
- askBar.classList.remove('velu-ask-bar-hidden');
836
- }
837
- }, { passive: true });
838
-
839
- document.onkeydown = function(e) {
840
- if (e.key === 'Escape' && isPanelOpen()) { closePanel(); }
841
- };
842
-
843
- // Restore panel state from sessionStorage on page load
844
- try {
845
- var savedOpen = sessionStorage.getItem('velu-panel-open');
846
- var savedExpanded = sessionStorage.getItem('velu-panel-expanded');
847
- var savedMessages = sessionStorage.getItem('velu-panel-messages');
848
- var savedConvId = sessionStorage.getItem('velu-conv-id');
849
- var savedConvToken = sessionStorage.getItem('velu-conv-token');
850
- var savedSeq = sessionStorage.getItem('velu-last-seq');
851
- if (savedConvId) state.conversationId = savedConvId;
852
- if (savedConvToken) state.conversationToken = savedConvToken;
853
- if (savedSeq) state.lastSeq = parseInt(savedSeq, 10) || 0;
854
- if (savedMessages) messagesEl.innerHTML = savedMessages;
855
- if (savedExpanded === '1') {
856
- state.expanded = true;
857
- panel.classList.add('velu-assistant-expanded');
858
- document.documentElement.classList.add('velu-assistant-wide');
859
- }
860
- if (savedOpen === '1') {
861
- openPanel();
862
- if (state.conversationId) connectSSE();
863
- }
864
- } catch(e) {}
865
-
866
- bootstrap();
867
- })();
868
- </script>
869
- `;
870
- writeFileSync(join(outDir, "src", "components", "PageTitle.astro"), pageTitleComponent, "utf-8");
871
- console.log("📋 Generated page title component");
872
-
873
- // ── 7b. Generate Footer.astro — Powered by Velu ────────────────────────
874
- const footerComponent = `---
875
- import EditLink from 'virtual:starlight/components/EditLink';
876
- import LastUpdated from 'virtual:starlight/components/LastUpdated';
877
- import Pagination from 'virtual:starlight/components/Pagination';
878
- ---
879
-
880
- <footer class="sl-flex">
881
- <div class="meta sl-flex">
882
- <EditLink />
883
- <LastUpdated />
884
- </div>
885
- <Pagination />
886
- <div class="velu-powered-by">
887
- <a href="https://getvelu.com" target="_blank" rel="noopener noreferrer">Powered by Velu</a>
888
- </div>
889
- </footer>
890
-
891
- <style>
892
- footer {
893
- flex-direction: column;
894
- gap: 1.5rem;
895
- }
896
- .meta {
897
- gap: 0.75rem;
898
- align-items: center;
899
- flex-wrap: wrap;
900
- justify-content: space-between;
901
- }
902
- .velu-powered-by {
903
- text-align: right;
904
- padding: 1rem 2rem 0.5rem 0;
905
- }
906
- .velu-powered-by a {
907
- font-size: 1.4rem;
908
- font-weight: 500;
909
- letter-spacing: 0.02em;
910
- color: rgba(160, 165, 180, 0.45);
911
- text-decoration: none;
912
- transition: color 0.25s ease;
913
- }
914
- .velu-powered-by a:hover {
915
- color: rgba(220, 225, 240, 0.95);
916
- }
917
- </style>
918
- `;
919
- writeFileSync(join(outDir, "src", "components", "Footer.astro"), footerComponent, "utf-8");
920
- console.log("📋 Generated footer component");
921
-
922
- // ── 8. Generate tabs.css ──────────────────────────────────────────────────
923
- const tabsCss = `/* ── Velu sidebar tabs ─────────────────────────────────────────────────── */
924
-
925
- :root {
926
- --sl-sidebar-width: 16rem;
927
- }
928
-
929
- .velu-sidebar-tabs {
930
- display: flex;
931
- flex-direction: column;
932
- gap: 0.15rem;
933
- padding: 0.35rem;
934
- margin-bottom: 1rem;
935
- background-color: var(--sl-color-bg);
936
- border: 1px solid var(--sl-color-gray-5);
937
- border-radius: 0.5rem;
938
- }
939
-
940
- .velu-sidebar-tab {
941
- display: flex;
942
- align-items: center;
943
- gap: 0.5rem;
944
- padding: 0.45rem 0.65rem;
945
- font-size: var(--sl-text-sm);
946
- font-weight: 600;
947
- color: var(--sl-color-gray-3);
948
- text-decoration: none;
949
- border-radius: 0.375rem;
950
- transition: color 0.15s, background-color 0.15s;
951
- }
952
-
953
- .velu-sidebar-tab:hover {
954
- color: var(--sl-color-white);
955
- background-color: var(--sl-color-gray-6);
956
- }
957
-
958
- .velu-sidebar-tab.active {
959
- color: var(--sl-color-white);
960
- background-color: var(--sl-color-gray-6);
961
- }
962
-
963
- :root[data-theme='light'] .velu-sidebar-tab.active {
964
- color: var(--sl-color-white);
965
- background-color: var(--sl-color-gray-7);
966
- }
967
-
968
- .velu-external-icon {
969
- opacity: 0.4;
970
- flex-shrink: 0;
971
- margin-inline-start: auto;
972
- }
973
-
974
- /* ── Copy page button ─────────────────────────────────────────────────── */
975
-
976
- .velu-title-row {
977
- display: flex;
978
- align-items: flex-start;
979
- justify-content: space-between;
980
- gap: 1rem;
981
- }
982
-
983
- .velu-title-row h1 {
984
- margin: 0;
985
- }
986
-
987
- .velu-copy-page-container {
988
- position: relative;
989
- flex-shrink: 0;
990
- margin-top: 0.35rem;
991
- }
992
-
993
- .velu-copy-split-btn {
994
- display: inline-flex;
995
- align-items: center;
996
- border: 1px solid var(--sl-color-gray-5);
997
- border-radius: 999px;
998
- background: var(--sl-color-bg-nav);
999
- }
1000
-
1001
- .velu-copy-main-btn {
1002
- display: inline-flex;
1003
- align-items: center;
1004
- gap: 0.4rem;
1005
- padding: 0.35rem 0.5rem 0.35rem 0.75rem;
1006
- font-size: var(--sl-text-xs);
1007
- font-weight: 500;
1008
- color: var(--sl-color-gray-3);
1009
- background: none;
1010
- border: none;
1011
- cursor: pointer;
1012
- transition: color 0.15s;
1013
- }
1014
-
1015
- .velu-copy-main-btn:hover {
1016
- color: var(--sl-color-white);
1017
- }
1018
-
1019
- .velu-copy-sep {
1020
- width: 1px;
1021
- height: 14px;
1022
- background-color: var(--sl-color-gray-5);
1023
- flex-shrink: 0;
1024
- }
1025
-
1026
- .velu-copy-caret-btn {
1027
- display: inline-flex;
1028
- align-items: center;
1029
- padding: 0.35rem 0.5rem;
1030
- background: none;
1031
- border: none;
1032
- color: var(--sl-color-gray-3);
1033
- cursor: pointer;
1034
- transition: color 0.15s;
1035
- }
1036
-
1037
- .velu-copy-caret-btn:hover {
1038
- color: var(--sl-color-white);
1039
- }
1040
-
1041
- .velu-copy-chevron {
1042
- transition: transform 0.15s;
1043
- }
1044
-
1045
- .velu-copy-caret-btn[aria-expanded='true'] .velu-copy-chevron {
1046
- transform: rotate(180deg);
1047
- }
1048
-
1049
- .velu-copy-dropdown {
1050
- position: absolute;
1051
- right: 0;
1052
- top: calc(100% + 0.35rem);
1053
- z-index: 100;
1054
- min-width: 16rem;
1055
- padding: 0.35rem;
1056
- background: var(--sl-color-bg-nav);
1057
- border: 1px solid var(--sl-color-gray-5);
1058
- border-radius: 0.5rem;
1059
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
1060
- }
1061
-
1062
- .velu-copy-option {
1063
- display: flex;
1064
- align-items: flex-start;
1065
- gap: 0.5rem;
1066
- width: 100%;
1067
- padding: 0.5rem 0.6rem;
1068
- font: inherit;
1069
- font-size: var(--sl-text-sm);
1070
- color: var(--sl-color-gray-2);
1071
- text-align: left;
1072
- text-decoration: none;
1073
- background: none;
1074
- border: none;
1075
- border-radius: 0.35rem;
1076
- cursor: pointer;
1077
- transition: background-color 0.15s;
1078
- }
1079
-
1080
- .velu-copy-option:hover {
1081
- background-color: var(--sl-color-gray-6);
1082
- }
1083
-
1084
- .velu-copy-option svg {
1085
- flex-shrink: 0;
1086
- opacity: 0.7;
1087
- margin-top: 0.15rem;
1088
- overflow: visible;
1089
- }
1090
-
1091
- .velu-copy-option-title {
1092
- font-weight: 500;
1093
- line-height: 1.3;
1094
- }
1095
-
1096
- .velu-copy-option-desc {
1097
- font-size: var(--sl-text-xs);
1098
- color: var(--sl-color-gray-3);
1099
- line-height: 1.3;
1100
- }
1101
-
1102
- .velu-external-arrow {
1103
- font-size: 0.75em;
1104
- opacity: 0.5;
1105
- }
1106
- `;
1107
- writeFileSync(join(outDir, "src", "styles", "tabs.css"), tabsCss, "utf-8");
1108
- console.log("🎨 Generated tabs.css");
1109
-
1110
- // ── 9. Generate assistant.css ───────────────────────────────────────────
1111
- const assistantCss = `/* ── Velu AI Assistant ─────────────────────────────────────────────────── */
1112
-
1113
- /* Fixed bottom ask bar */
1114
- .velu-ask-bar {
1115
- position: fixed;
1116
- bottom: 1.5rem;
1117
- left: 50%;
1118
- transform: translateX(-50%);
1119
- z-index: 200;
1120
- width: 100%;
1121
- max-width: 36rem;
1122
- padding: 0 1rem;
1123
- transition: opacity 0.2s, transform 0.2s;
1124
- }
1125
-
1126
- .velu-ask-bar-hidden {
1127
- opacity: 0;
1128
- pointer-events: none;
1129
- transform: translateX(-50%) translateY(1rem);
1130
- }
1131
-
1132
- .velu-ask-bar-inner {
1133
- display: flex;
1134
- align-items: center;
1135
- gap: 0.5rem;
1136
- padding: 0.5rem 0.75rem;
1137
- background: var(--sl-color-bg-nav);
1138
- border: 1px solid var(--sl-color-gray-5);
1139
- border-radius: 0.75rem;
1140
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
1141
- }
1142
-
1143
- .velu-ask-icon {
1144
- flex-shrink: 0;
1145
- color: var(--sl-color-gray-3);
1146
- }
1147
-
1148
- .velu-ask-input {
1149
- flex: 1;
1150
- background: none;
1151
- border: none;
1152
- outline: none;
1153
- font: inherit;
1154
- font-size: var(--sl-text-sm);
1155
- color: var(--sl-color-white);
1156
- }
1157
-
1158
- .velu-ask-input::placeholder {
1159
- color: var(--sl-color-gray-3);
1160
- }
1161
-
1162
-
1163
- .velu-ask-submit {
1164
- flex-shrink: 0;
1165
- display: flex;
1166
- align-items: center;
1167
- justify-content: center;
1168
- width: 28px;
1169
- height: 28px;
1170
- background: var(--sl-color-accent);
1171
- color: var(--sl-color-accent-high);
1172
- border: none;
1173
- border-radius: 50%;
1174
- cursor: pointer;
1175
- transition: opacity 0.15s;
1176
- }
1177
-
1178
- .velu-ask-submit:hover { opacity: 0.85; }
1179
-
1180
- /* Right-side assistant panel */
1181
- .velu-assistant-panel {
1182
- position: fixed;
1183
- top: var(--sl-nav-height, 3.5rem);
1184
- right: 0;
1185
- bottom: 0;
1186
- width: 22rem;
1187
- z-index: 50;
1188
- pointer-events: auto;
1189
- display: flex;
1190
- flex-direction: column;
1191
- background: var(--sl-color-bg);
1192
- border-left: 1px solid var(--sl-color-gray-5);
1193
- box-shadow: -4px 0 24px rgba(0, 0, 0, 0.2);
1194
- transition: width 0.2s;
1195
- }
1196
-
1197
- .velu-panel-closed { display: none !important; }
1198
-
1199
- .velu-assistant-expanded { width: 40rem; }
1200
-
1201
- .velu-assistant-header {
1202
- display: flex;
1203
- align-items: center;
1204
- justify-content: space-between;
1205
- padding: 0.75rem 1rem;
1206
- border-bottom: 1px solid var(--sl-color-gray-5);
1207
- flex-shrink: 0;
1208
- }
1209
-
1210
- .velu-assistant-title {
1211
- font-weight: 600;
1212
- font-size: var(--sl-text-base);
1213
- color: var(--sl-color-white);
1214
- }
1215
-
1216
- .velu-assistant-actions {
1217
- display: flex;
1218
- gap: 0.25rem;
1219
- }
1220
-
1221
- .velu-assistant-action {
1222
- display: flex;
1223
- align-items: center;
1224
- justify-content: center;
1225
- width: 28px;
1226
- height: 28px;
1227
- background: none;
1228
- border: none;
1229
- border-radius: 0.25rem;
1230
- color: var(--sl-color-gray-3);
1231
- cursor: pointer;
1232
- pointer-events: auto;
1233
- transition: color 0.15s, background-color 0.15s;
1234
- }
1235
-
1236
- .velu-assistant-action:hover {
1237
- color: var(--sl-color-white);
1238
- background-color: var(--sl-color-gray-6);
1239
- }
1240
-
1241
- /* Messages area */
1242
- .velu-assistant-messages {
1243
- flex: 1;
1244
- overflow-y: auto;
1245
- padding: 1rem;
1246
- display: flex;
1247
- flex-direction: column;
1248
- gap: 0.75rem;
1249
- }
1250
-
1251
- .velu-msg {
1252
- display: flex;
1253
- flex-direction: column;
1254
- gap: 0.35rem;
1255
- }
1256
-
1257
- .velu-msg-user { align-items: flex-end; }
1258
- .velu-msg-assistant { align-items: flex-start; }
1259
-
1260
- .velu-msg-bubble {
1261
- max-width: 85%;
1262
- padding: 0.6rem 0.85rem;
1263
- border-radius: 0.75rem;
1264
- font-size: var(--sl-text-sm);
1265
- line-height: 1.55;
1266
- word-break: break-word;
1267
- }
1268
-
1269
- .velu-msg-bubble code {
1270
- background: var(--sl-color-gray-6);
1271
- padding: 0.1rem 0.3rem;
1272
- border-radius: 0.2rem;
1273
- font-size: 0.85em;
1274
- }
1275
-
1276
- .velu-msg-bubble-user {
1277
- background: var(--sl-color-accent);
1278
- color: var(--sl-color-accent-high);
1279
- border-bottom-right-radius: 0.2rem;
1280
- }
1281
-
1282
- .velu-msg-bubble-assistant {
1283
- background: var(--sl-color-gray-6);
1284
- color: var(--sl-color-white);
1285
- border-bottom-left-radius: 0.2rem;
1286
- }
1287
-
1288
- /* Citations */
1289
- .velu-msg-citations {
1290
- display: flex;
1291
- flex-wrap: wrap;
1292
- gap: 0.35rem;
1293
- padding-left: 0.25rem;
1294
- }
1295
-
1296
- .velu-citation-link {
1297
- font-size: var(--sl-text-xs);
1298
- color: var(--sl-color-accent);
1299
- text-decoration: none;
1300
- padding: 0.15rem 0.4rem;
1301
- background: var(--sl-color-gray-6);
1302
- border-radius: 0.25rem;
1303
- transition: background-color 0.15s;
1304
- }
1305
-
1306
- .velu-citation-link:hover {
1307
- background: var(--sl-color-gray-5);
1308
- }
1309
-
1310
- .velu-citation-ref {
1311
- color: var(--sl-color-accent);
1312
- text-decoration: none;
1313
- font-weight: 600;
1314
- font-size: 0.8em;
1315
- vertical-align: super;
1316
- }
1317
-
1318
- /* Message actions */
1319
- .velu-msg-actions {
1320
- display: flex;
1321
- gap: 0.15rem;
1322
- padding-left: 0.25rem;
1323
- }
1324
-
1325
- .velu-msg-action {
1326
- display: flex;
1327
- align-items: center;
1328
- justify-content: center;
1329
- width: 24px;
1330
- height: 24px;
1331
- background: none;
1332
- border: none;
1333
- border-radius: 0.25rem;
1334
- color: var(--sl-color-gray-4);
1335
- cursor: pointer;
1336
- transition: color 0.15s, background-color 0.15s;
1337
- }
1338
-
1339
- .velu-msg-action:hover {
1340
- color: var(--sl-color-white);
1341
- background-color: var(--sl-color-gray-6);
1342
- }
1343
-
1344
- /* Thinking dots */
1345
- .velu-thinking-dots {
1346
- display: inline-flex;
1347
- gap: 0.3rem;
1348
- padding: 0.2rem 0;
1349
- }
1350
-
1351
- .velu-thinking-dots span {
1352
- width: 6px;
1353
- height: 6px;
1354
- border-radius: 50%;
1355
- background: var(--sl-color-gray-3);
1356
- animation: veluDotPulse 1.2s infinite;
1357
- }
1358
-
1359
- .velu-thinking-dots span:nth-child(2) { animation-delay: 0.2s; }
1360
- .velu-thinking-dots span:nth-child(3) { animation-delay: 0.4s; }
1361
-
1362
- @keyframes veluDotPulse {
1363
- 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
1364
- 40% { opacity: 1; transform: scale(1); }
1365
- }
1366
-
1367
- /* Chat input area */
1368
- .velu-assistant-input-area {
1369
- display: flex;
1370
- align-items: center;
1371
- gap: 0.5rem;
1372
- padding: 0.75rem 1rem;
1373
- border-top: 1px solid var(--sl-color-gray-5);
1374
- flex-shrink: 0;
1375
- }
1376
-
1377
- .velu-assistant-chat-input {
1378
- flex: 1;
1379
- background: var(--sl-color-gray-6);
1380
- border: 1px solid var(--sl-color-gray-5);
1381
- border-radius: 0.5rem;
1382
- padding: 0.5rem 0.75rem;
1383
- font: inherit;
1384
- font-size: var(--sl-text-sm);
1385
- color: var(--sl-color-white);
1386
- outline: none;
1387
- transition: border-color 0.15s;
1388
- }
1389
-
1390
- .velu-assistant-chat-input:focus {
1391
- border-color: var(--sl-color-accent);
1392
- }
1393
-
1394
- .velu-assistant-chat-input::placeholder {
1395
- color: var(--sl-color-gray-3);
1396
- }
1397
-
1398
- .velu-assistant-send {
1399
- flex-shrink: 0;
1400
- display: flex;
1401
- align-items: center;
1402
- justify-content: center;
1403
- width: 32px;
1404
- height: 32px;
1405
- background: var(--sl-color-accent);
1406
- color: var(--sl-color-accent-high);
1407
- border: none;
1408
- border-radius: 50%;
1409
- cursor: pointer;
1410
- transition: opacity 0.15s;
1411
- }
1412
-
1413
- .velu-assistant-send:hover { opacity: 0.85; }
1414
-
1415
- /* Squeeze page layout when panel is open */
1416
- html.velu-assistant-open body {
1417
- margin-right: 22rem;
1418
- transition: margin-right 0.25s ease;
1419
- }
1420
-
1421
- html.velu-assistant-wide body {
1422
- margin-right: 40rem;
1423
- }
1424
-
1425
- html.velu-assistant-open .header {
1426
- padding-right: 22rem;
1427
- transition: padding-right 0.25s ease;
1428
- }
1429
-
1430
- html.velu-assistant-wide .header {
1431
- padding-right: 40rem;
1432
- }
1433
-
1434
- html.velu-assistant-open .velu-ask-bar {
1435
- right: 22rem;
1436
- left: auto;
1437
- transform: none;
1438
- }
1439
-
1440
- html.velu-assistant-wide .velu-ask-bar {
1441
- right: 40rem;
1442
- }
1443
-
1444
- /* Responsive */
1445
- @media (max-width: 50rem) {
1446
- .velu-assistant-panel {
1447
- width: 100%;
1448
- }
1449
- .velu-assistant-expanded {
1450
- width: 100%;
1451
- }
1452
- .velu-ask-bar {
1453
- max-width: calc(100% - 2rem);
1454
- }
1455
- html.velu-assistant-open body {
1456
- margin-right: 0;
1457
- }
1458
- html.velu-assistant-wide body {
1459
- margin-right: 0;
1460
- }
1461
- }
1462
- `;
1463
- writeFileSync(join(outDir, "src", "styles", "assistant.css"), assistantCss, "utf-8");
1464
- console.log("🤖 Generated assistant.css");
224
+ // ── 6. Generate index.mdx (dynamic — references first page) ──────────────
225
+ writeFileSync(
226
+ join(outDir, "content", "docs", "index.mdx"),
227
+ `---\ntitle: "Overview"\ndescription: Documentation powered by Velu + Fumadocs\n---\n\nimport { Card, Cards } from "fumadocs-ui/components/card"\nimport { Callout } from "fumadocs-ui/components/callout"\n\n<Callout type="info">\n This site is powered by Velu + Fumadocs.\n</Callout>\n\n## Start here\n\n<Cards>\n <Card\n title="Read the docs"\n href="/${firstPage}/"\n description="Begin with the first page in your configured navigation."\n />\n</Cards>\n`,
228
+ "utf-8"
229
+ );
1465
230
 
1466
- // ── 10. Static boilerplate ─────────────────────────────────────────────────
1467
- const astroPkg = {
231
+ // ── 7. Generate minimal package.json (type: module, no local deps) ───────
232
+ const sitePkg = {
1468
233
  name: "velu-docs-site",
1469
234
  version: "0.0.1",
1470
235
  private: true,
1471
236
  type: "module",
1472
- scripts: {
1473
- dev: "astro dev",
1474
- build: "astro build",
1475
- preview: "astro preview",
1476
- },
1477
- dependencies: {
1478
- astro: "^5.12.0",
1479
- "@astrojs/starlight": "^0.35.0",
1480
- sharp: "^0.33.0",
1481
- "starlight-ion-theme": "^2.3.0",
1482
- },
1483
237
  };
1484
- writeFileSync(join(outDir, "package.json"), JSON.stringify(astroPkg, null, 2) + "\n", "utf-8");
1485
-
1486
- writeFileSync(
1487
- join(outDir, "tsconfig.json"),
1488
- JSON.stringify({ extends: "astro/tsconfigs/strict" }, null, 2) + "\n",
1489
- "utf-8"
1490
- );
1491
-
1492
- const firstPage = pageMap[0]?.dest || "quickstart";
1493
- writeFileSync(
1494
- join(outDir, "src", "content", "docs", "index.mdx"),
1495
- `---\ntitle: "Welcome to Velu Docs"\ndescription: Documentation powered by Velu\n---\n\nWelcome to the documentation. Head over to the [Quickstart](/${firstPage}/) to get started.\n`,
1496
- "utf-8"
1497
- );
1498
-
1499
- writeFileSync(
1500
- join(outDir, "src", "content.config.ts"),
1501
- `import { defineCollection } from 'astro:content';\nimport { docsSchema } from '@astrojs/starlight/schema';\n\nexport const collections = {\n docs: defineCollection({ schema: docsSchema() }),\n};\n`,
1502
- "utf-8"
1503
- );
1504
-
1505
- // ── 10. Generate _server.mjs — programmatic dev/build/preview ───────────────
1506
- const serverScript = `import { dev, build, preview } from 'astro';
1507
- import { watch } from 'node:fs';
1508
- import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from 'node:fs';
1509
- import { resolve, dirname, relative, extname, join } from 'node:path';
1510
-
1511
- // ── Docs directory (parent of .velu-out) ────────────────────────────────────
1512
- const docsDir = resolve('..');
1513
- const contentDir = resolve('src', 'content', 'docs');
1514
-
1515
- // ── Page processing (mirrors build.ts logic) ────────────────────────────────
1516
- function pageLabelFromSlug(slug) {
1517
- const last = slug.split('/').pop() || slug;
1518
- return last.replace(/[-_]/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase());
1519
- }
1520
-
1521
- function processPage(srcPath, destPath, slug) {
1522
- let content = readFileSync(srcPath, 'utf-8');
1523
- if (!content.startsWith('---')) {
1524
- const titleMatch = content.match(/^#\\s+(.+)$/m);
1525
- const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
1526
- if (titleMatch) {
1527
- content = content.replace(/^#\\s+.+$/m, '').trimStart();
1528
- }
1529
- content = '---\\ntitle: "' + title + '"\\n---\\n\\n' + content;
1530
- }
1531
- mkdirSync(dirname(destPath), { recursive: true });
1532
- writeFileSync(destPath, content, 'utf-8');
1533
- }
1534
-
1535
- function startWatcher() {
1536
- const debounce = new Map();
1537
-
1538
- watch(docsDir, { recursive: true }, (eventType, filename) => {
1539
- if (!filename) return;
1540
- // Ignore changes inside .velu-out itself
1541
- if (filename.startsWith('.velu-out')) return;
1542
- // Ignore node_modules, hidden dirs
1543
- if (filename.includes('node_modules') || filename.startsWith('.')) return;
1544
-
1545
- // Debounce — avoid duplicate events
1546
- if (debounce.has(filename)) clearTimeout(debounce.get(filename));
1547
- debounce.set(filename, setTimeout(() => {
1548
- debounce.delete(filename);
1549
- const srcPath = join(docsDir, filename);
1550
- if (!existsSync(srcPath)) return;
1551
-
1552
- if (filename === 'velu.json') {
1553
- copyFileSync(srcPath, resolve('velu.json'));
1554
- console.log(' \\x1b[32m↻\\x1b[0m velu.json updated');
1555
- return;
1556
- }
1557
-
1558
- if (extname(filename) === '.md') {
1559
- const slug = filename.replace(/\\\\/g, '/').replace(/\\.md$/, '');
1560
- const destPath = join(contentDir, slug + '.md');
1561
- try {
1562
- processPage(srcPath, destPath, slug);
1563
- console.log(' \\x1b[32m↻\\x1b[0m ' + slug);
1564
- } catch (e) {
1565
- console.error(' \\x1b[31m✗\\x1b[0m Failed to sync ' + filename + ': ' + e.message);
1566
- }
1567
- }
1568
- }, 100));
1569
- });
1570
- }
1571
-
1572
- // ── CLI ──────────────────────────────────────────────────────────────────────
1573
- const args = process.argv.slice(2);
1574
- const command = args[0] || 'dev';
1575
- const portIdx = args.indexOf('--port');
1576
- const port = portIdx !== -1 ? parseInt(args[portIdx + 1]) : 4321;
1577
-
1578
- if (command === 'dev') {
1579
- const server = await dev({
1580
- root: '.',
1581
- configFile: './_config.mjs',
1582
- server: { port },
1583
- logLevel: 'silent',
1584
- });
1585
- const addr = server.address;
1586
- console.log('');
1587
- console.log(' \\x1b[36mvelu\\x1b[0m v0.1.0 ready');
1588
- console.log('');
1589
- console.log(' ┃ Local \\x1b[36mhttp://localhost:' + addr.port + '/\\x1b[0m');
1590
- console.log(' ┃ Network use --host to expose');
1591
- console.log('');
1592
- console.log(' watching for file changes...');
1593
- startWatcher();
1594
- } else if (command === 'build') {
1595
- console.log('\\n Building site...\\n');
1596
- await build({ root: '.', configFile: './_config.mjs', logLevel: 'warn' });
1597
- console.log('\\n ✅ Site built successfully.\\n');
1598
- } else if (command === 'preview') {
1599
- const server = await preview({
1600
- root: '.',
1601
- configFile: './_config.mjs',
1602
- server: { port },
1603
- logLevel: 'silent',
1604
- });
1605
- const addr = server.address;
1606
- console.log('');
1607
- console.log(' \\x1b[36mvelu\\x1b[0m preview');
1608
- console.log('');
1609
- console.log(' ┃ Local \\x1b[36mhttp://localhost:' + addr.port + '/\\x1b[0m');
1610
- console.log('');
1611
- }
1612
- `;
1613
- writeFileSync(join(outDir, "_server.mjs"), serverScript, "utf-8");
1614
-
1615
- // ── 11. Generate .gitignore ──────────────────────────────────────────────
1616
- writeFileSync(
1617
- join(outDir, ".gitignore"),
1618
- `.astro/\nnode_modules/\ndist/\n`,
1619
- "utf-8"
1620
- );
238
+ writeFileSync(join(outDir, "package.json"), JSON.stringify(sitePkg, null, 2) + "\n", "utf-8");
1621
239
 
1622
240
  console.log("📦 Generated boilerplate");
1623
241
  console.log(`\n✅ Site generated at: ${outDir}`);