pagelathe 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 (51) hide show
  1. package/LICENSE +202 -0
  2. package/dist/bin.js +10 -0
  3. package/dist/chunk-LRG7WLGI.js +1020 -0
  4. package/dist/index.js +9 -0
  5. package/dist/registry/app/astro.config.mjs +7 -0
  6. package/dist/registry/app/package.json +29 -0
  7. package/dist/registry/app/public/favicon.svg +4 -0
  8. package/dist/registry/app/scripts/vendor-sections.mjs +50 -0
  9. package/dist/registry/app/src/components/SectionRenderer.astro +23 -0
  10. package/dist/registry/app/src/components/sections/.gitkeep +0 -0
  11. package/dist/registry/app/src/content/_schema.ts +27 -0
  12. package/dist/registry/app/src/content/landing/index.yaml +135 -0
  13. package/dist/registry/app/src/content.config.ts +10 -0
  14. package/dist/registry/app/src/layouts/Base.astro +24 -0
  15. package/dist/registry/app/src/pages/index.astro +13 -0
  16. package/dist/registry/app/src/styles/global.css +20 -0
  17. package/dist/registry/app/tsconfig.json +1 -0
  18. package/dist/registry/sections/codeDemo/schema.ts +46 -0
  19. package/dist/registry/sections/codeDemo/section.astro +55 -0
  20. package/dist/registry/sections/features/schema.ts +44 -0
  21. package/dist/registry/sections/features/section.astro +25 -0
  22. package/dist/registry/sections/finalCta/schema.ts +30 -0
  23. package/dist/registry/sections/finalCta/section.astro +18 -0
  24. package/dist/registry/sections/footer/schema.ts +44 -0
  25. package/dist/registry/sections/footer/section.astro +29 -0
  26. package/dist/registry/sections/header/schema.ts +35 -0
  27. package/dist/registry/sections/header/section.astro +28 -0
  28. package/dist/registry/sections/hero/schema.ts +50 -0
  29. package/dist/registry/sections/hero/section.astro +56 -0
  30. package/dist/registry/sections/package.json +23 -0
  31. package/dist/registry/sections/pricing/schema.ts +55 -0
  32. package/dist/registry/sections/pricing/section.astro +29 -0
  33. package/dist/registry/sections/src/a11y.ts +34 -0
  34. package/dist/registry/sections/src/env.d.ts +7 -0
  35. package/dist/registry/sections/src/manifest.ts +41 -0
  36. package/dist/registry/sections/src/page.ts +48 -0
  37. package/dist/registry/sections/src/registry.ts +62 -0
  38. package/dist/registry/sections/src/render-harness.ts +11 -0
  39. package/dist/registry/sections/test/chrome.test.ts +29 -0
  40. package/dist/registry/sections/test/codeDemo.test.ts +16 -0
  41. package/dist/registry/sections/test/features.test.ts +18 -0
  42. package/dist/registry/sections/test/finalCta.test.ts +15 -0
  43. package/dist/registry/sections/test/hero.test.ts +30 -0
  44. package/dist/registry/sections/test/page.test.ts +58 -0
  45. package/dist/registry/sections/test/pricing.test.ts +22 -0
  46. package/dist/registry/sections/test/registry-complete.test.ts +38 -0
  47. package/dist/registry/sections/test/registry.test.ts +12 -0
  48. package/dist/registry/sections/test/validate.test.ts +29 -0
  49. package/dist/registry/sections/tsconfig.json +8 -0
  50. package/dist/registry/sections/vitest.config.ts +7 -0
  51. package/package.json +56 -0
@@ -0,0 +1,1020 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/version.ts
7
+ import { readFileSync } from "fs";
8
+ import { fileURLToPath } from "url";
9
+ import { dirname, join } from "path";
10
+ function getVersion() {
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+ const pkgPath = join(here, "..", "package.json");
13
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
14
+ return pkg.version;
15
+ }
16
+
17
+ // src/config/store.ts
18
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
19
+
20
+ // src/config/paths.ts
21
+ import { homedir } from "os";
22
+ import { join as join2 } from "path";
23
+ function getConfigDir() {
24
+ const override = process.env.PAGELATHE_CONFIG_DIR;
25
+ if (override && override.trim() !== "") return override;
26
+ return join2(homedir(), ".pagelathe");
27
+ }
28
+ function getConfigFile() {
29
+ return join2(getConfigDir(), "config.json");
30
+ }
31
+
32
+ // src/config/schema.ts
33
+ import { z } from "zod";
34
+ var pagelatheConfigSchema = z.object({
35
+ version: z.literal(1).default(1),
36
+ provider: z.object({
37
+ /** OpenRouter API key (may instead come from env at read time). */
38
+ openrouterKey: z.string().min(1).optional(),
39
+ /** Default model id shown first in the picker. */
40
+ defaultModel: z.string().min(1).default("anthropic/claude-3.7-sonnet")
41
+ }).default({ defaultModel: "anthropic/claude-3.7-sonnet" })
42
+ });
43
+ var ConfigError = class extends Error {
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = "ConfigError";
47
+ }
48
+ };
49
+
50
+ // src/config/store.ts
51
+ function loadConfig() {
52
+ let raw;
53
+ try {
54
+ raw = readFileSync2(getConfigFile(), "utf8");
55
+ } catch (err) {
56
+ if (err.code === "ENOENT") {
57
+ return pagelatheConfigSchema.parse({});
58
+ }
59
+ throw new ConfigError(`Could not read config: ${err.message}`);
60
+ }
61
+ let json;
62
+ try {
63
+ json = JSON.parse(raw);
64
+ } catch {
65
+ throw new ConfigError(`Config file is not valid JSON: ${getConfigFile()}`);
66
+ }
67
+ const result = pagelatheConfigSchema.safeParse(json);
68
+ if (!result.success) {
69
+ throw new ConfigError(`Config file failed validation: ${result.error.message}`);
70
+ }
71
+ return result.data;
72
+ }
73
+ function saveConfig(config) {
74
+ let validated;
75
+ try {
76
+ validated = pagelatheConfigSchema.parse(config);
77
+ } catch (err) {
78
+ throw new ConfigError(`Invalid config: ${err.message}`);
79
+ }
80
+ mkdirSync(getConfigDir(), { recursive: true, mode: 448 });
81
+ writeFileSync(getConfigFile(), `${JSON.stringify(validated, null, 2)}
82
+ `, { mode: 384 });
83
+ }
84
+
85
+ // src/config/keys.ts
86
+ var OPENROUTER_KEY_RE = /^sk-or-[A-Za-z0-9-_]{8,}$/;
87
+ var KeyError = class extends Error {
88
+ constructor(message) {
89
+ super(message);
90
+ this.name = "KeyError";
91
+ }
92
+ };
93
+ function isValidOpenRouterKeyFormat(key) {
94
+ return OPENROUTER_KEY_RE.test(key);
95
+ }
96
+ function getApiKey() {
97
+ const fromEnv = process.env.OPENROUTER_API_KEY;
98
+ if (fromEnv && fromEnv.trim() !== "") return fromEnv;
99
+ return loadConfig().provider.openrouterKey;
100
+ }
101
+ function setApiKey(key) {
102
+ if (!isValidOpenRouterKeyFormat(key)) {
103
+ throw new KeyError(
104
+ "That does not look like an OpenRouter key (expected it to start with 'sk-or-')."
105
+ );
106
+ }
107
+ const config = loadConfig();
108
+ config.provider.openrouterKey = key;
109
+ saveConfig(config);
110
+ }
111
+ function maskKey(key) {
112
+ const tail = key.slice(-4);
113
+ return `sk-or-\u2026${tail}`;
114
+ }
115
+
116
+ // src/ui/prompts.ts
117
+ import { isCancel, cancel, password, text } from "@clack/prompts";
118
+ async function promptSecret(message) {
119
+ const value = await password({ message });
120
+ if (isCancel(value)) {
121
+ cancel("Aborted.");
122
+ process.exit(0);
123
+ }
124
+ return value;
125
+ }
126
+ async function promptText(message) {
127
+ const value = await text({ message });
128
+ if (isCancel(value)) {
129
+ cancel("Aborted.");
130
+ process.exit(0);
131
+ }
132
+ return value;
133
+ }
134
+
135
+ // src/commands/config-cmd.ts
136
+ function runSetKey(key) {
137
+ setApiKey(key);
138
+ return { ok: true, masked: maskKey(key) };
139
+ }
140
+ function runShow() {
141
+ const envKey = process.env.OPENROUTER_API_KEY;
142
+ const config = loadConfig();
143
+ const key = getApiKey();
144
+ const source = envKey && envKey.trim() !== "" ? "env" : key ? "config" : "none";
145
+ return {
146
+ keySet: Boolean(key),
147
+ maskedKey: key ? maskKey(key) : null,
148
+ defaultModel: config.provider.defaultModel,
149
+ source
150
+ };
151
+ }
152
+ function registerConfigCommand(program) {
153
+ const config = program.command("config").description("manage pagelathe config and keys");
154
+ config.command("set-key [key]").description("store your OpenRouter API key locally").action(async (key) => {
155
+ const value = key ?? await promptSecret("Paste your OpenRouter API key");
156
+ const { masked } = runSetKey(value);
157
+ console.log(`\u2713 Saved OpenRouter key (${masked}).`);
158
+ });
159
+ config.command("show").description("show current config (key is masked)").action(() => {
160
+ const out = runShow();
161
+ console.log(`Key set: ${out.keySet ? "yes" : "no"}`);
162
+ console.log(`Source: ${out.source}`);
163
+ console.log(`Key: ${out.maskedKey ?? "\u2014"}`);
164
+ console.log(`Default model: ${out.defaultModel}`);
165
+ });
166
+ }
167
+
168
+ // src/commands/init-cmd.ts
169
+ import { resolve as resolve2, join as join6 } from "path";
170
+
171
+ // src/registry/read.ts
172
+ import { existsSync as existsSync2 } from "fs";
173
+ import { join as join4 } from "path";
174
+
175
+ // ../../registry/sections/src/page.ts
176
+ import { z as z2 } from "zod";
177
+ var primaryGoalSchema = z2.enum([
178
+ "signup",
179
+ "github_star",
180
+ "docs",
181
+ "contact",
182
+ "waitlist",
183
+ "purchase"
184
+ ]);
185
+ var archetypeSchema = z2.enum(["sdk-infra", "technical-app", "general"]);
186
+ var metaSchema = z2.object({
187
+ title: z2.string().min(1),
188
+ description: z2.string().min(1),
189
+ locales: z2.array(z2.string().min(2)).min(1).default(["en"]),
190
+ primaryGoal: primaryGoalSchema.default("signup")
191
+ });
192
+ var themeSchema = z2.object({
193
+ tokens: z2.object({
194
+ colorPrimary: z2.string().min(1).default("#3A6463"),
195
+ radius: z2.string().min(1).default("0.5rem"),
196
+ font: z2.string().min(1).default("Inter")
197
+ }).default({})
198
+ });
199
+ function buildPageDocumentSchema(entrySchemas2) {
200
+ const sectionUnion = entrySchemas2.length === 1 ? entrySchemas2[0] : (
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
+ z2.discriminatedUnion("type", entrySchemas2)
203
+ );
204
+ return z2.object({
205
+ meta: metaSchema,
206
+ theme: themeSchema.default({ tokens: {} }),
207
+ archetype: archetypeSchema.default("general"),
208
+ sections: z2.array(sectionUnion).min(1)
209
+ });
210
+ }
211
+
212
+ // ../../registry/sections/hero/schema.ts
213
+ import { z as z3 } from "zod";
214
+ var ctaSchema = z3.object({
215
+ label: z3.string().min(1),
216
+ href: z3.string().min(1),
217
+ style: z3.enum(["primary", "secondary", "ghost"]).default("primary")
218
+ });
219
+ var propsSchema = z3.object({
220
+ variant: z3.enum(["code-snippet", "product-ui"]).default("code-snippet"),
221
+ eyebrow: z3.string().optional(),
222
+ headline: z3.string().min(1),
223
+ subhead: z3.string().min(1),
224
+ ctas: z3.array(ctaSchema).min(1).max(3),
225
+ /** Shown when variant === "code-snippet". */
226
+ code: z3.object({ lang: z3.string().min(1), source: z3.string().min(1) }).optional(),
227
+ /** Shown when variant === "product-ui". */
228
+ image: z3.object({ src: z3.string().min(1), alt: z3.string().min(1) }).optional()
229
+ });
230
+ var entrySchema = z3.object({
231
+ type: z3.literal("hero"),
232
+ id: z3.string().min(1),
233
+ props: propsSchema
234
+ });
235
+ var manifest = {
236
+ type: "hero",
237
+ title: "Hero",
238
+ description: "Above-the-fold value statement with CTAs; adaptive code or product-UI variant.",
239
+ required: true,
240
+ archetypes: ["sdk-infra", "technical-app", "general"],
241
+ variants: ["code-snippet", "product-ui"],
242
+ componentFile: "section.astro",
243
+ defaultProps: {
244
+ variant: "code-snippet",
245
+ eyebrow: "Open source",
246
+ headline: "Postgres branching for teams",
247
+ subhead: "Spin up an isolated database branch per pull request in one command.",
248
+ ctas: [
249
+ { label: "Start free", href: "#get-started", style: "primary" },
250
+ { label: "View on GitHub", href: "https://github.com", style: "secondary" }
251
+ ],
252
+ code: {
253
+ lang: "bash",
254
+ source: "npx branchy create --from main\n# \u2192 branch ready in 1.2s"
255
+ }
256
+ }
257
+ };
258
+
259
+ // ../../registry/sections/header/schema.ts
260
+ import { z as z4 } from "zod";
261
+ var navLinkSchema = z4.object({ label: z4.string().min(1), href: z4.string().min(1) });
262
+ var propsSchema2 = z4.object({
263
+ brand: z4.string().min(1),
264
+ links: z4.array(navLinkSchema).max(6).default([]),
265
+ github: z4.object({ href: z4.string().min(1), stars: z4.string().optional() }).optional(),
266
+ cta: z4.object({ label: z4.string().min(1), href: z4.string().min(1) }).optional()
267
+ });
268
+ var entrySchema2 = z4.object({
269
+ type: z4.literal("header"),
270
+ id: z4.string().min(1),
271
+ props: propsSchema2
272
+ });
273
+ var manifest2 = {
274
+ type: "header",
275
+ title: "Header / Navbar",
276
+ description: "Top navigation with brand, links, GitHub star badge, and a co-equal docs/CTA link.",
277
+ required: true,
278
+ archetypes: ["sdk-infra", "technical-app", "general"],
279
+ componentFile: "section.astro",
280
+ defaultProps: {
281
+ brand: "Branchy",
282
+ links: [
283
+ { label: "Docs", href: "/docs" },
284
+ { label: "Pricing", href: "#pricing" }
285
+ ],
286
+ github: { href: "https://github.com", stars: "4.2k" },
287
+ cta: { label: "Start free", href: "#get-started" }
288
+ }
289
+ };
290
+
291
+ // ../../registry/sections/footer/schema.ts
292
+ import { z as z5 } from "zod";
293
+ var footerColumnSchema = z5.object({
294
+ heading: z5.string().min(1),
295
+ links: z5.array(z5.object({ label: z5.string().min(1), href: z5.string().min(1) })).min(1)
296
+ });
297
+ var propsSchema3 = z5.object({
298
+ brand: z5.string().min(1),
299
+ tagline: z5.string().optional(),
300
+ columns: z5.array(footerColumnSchema).max(4).default([]),
301
+ copyright: z5.string().min(1)
302
+ });
303
+ var entrySchema3 = z5.object({
304
+ type: z5.literal("footer"),
305
+ id: z5.string().min(1),
306
+ props: propsSchema3
307
+ });
308
+ var manifest3 = {
309
+ type: "footer",
310
+ title: "Footer",
311
+ description: "Site footer with link columns and copyright.",
312
+ required: true,
313
+ archetypes: ["sdk-infra", "technical-app", "general"],
314
+ componentFile: "section.astro",
315
+ defaultProps: {
316
+ brand: "Branchy",
317
+ tagline: "Database branching for teams.",
318
+ columns: [
319
+ {
320
+ heading: "Product",
321
+ links: [
322
+ { label: "Docs", href: "/docs" },
323
+ { label: "Pricing", href: "#pricing" }
324
+ ]
325
+ },
326
+ { heading: "Community", links: [{ label: "GitHub", href: "https://github.com" }] }
327
+ ],
328
+ copyright: "\xA9 2026 Branchy. Apache-2.0."
329
+ }
330
+ };
331
+
332
+ // ../../registry/sections/features/schema.ts
333
+ import { z as z6 } from "zod";
334
+ var featureSchema = z6.object({
335
+ title: z6.string().min(1),
336
+ body: z6.string().min(1),
337
+ /** Larger tile = more important feature (first item is the lead). */
338
+ emphasis: z6.boolean().default(false),
339
+ icon: z6.string().optional()
340
+ });
341
+ var propsSchema4 = z6.object({
342
+ heading: z6.string().min(1),
343
+ subhead: z6.string().optional(),
344
+ items: z6.array(featureSchema).min(2).max(8)
345
+ });
346
+ var entrySchema4 = z6.object({
347
+ type: z6.literal("features"),
348
+ id: z6.string().min(1),
349
+ props: propsSchema4
350
+ });
351
+ var manifest4 = {
352
+ type: "features",
353
+ title: "Features / Bento",
354
+ description: "Feature grid; the emphasized tile is the most important capability.",
355
+ required: true,
356
+ archetypes: ["sdk-infra", "technical-app", "general"],
357
+ componentFile: "section.astro",
358
+ defaultProps: {
359
+ heading: "Built for teams that ship",
360
+ subhead: "Isolated environments, instant reset, zero config.",
361
+ items: [
362
+ {
363
+ title: "Branch per PR",
364
+ body: "Every pull request gets an isolated database.",
365
+ emphasis: true
366
+ },
367
+ { title: "1-second reset", body: "Roll back to a clean state instantly." },
368
+ { title: "Self-host or cloud", body: "Run it your way; data stays yours." }
369
+ ]
370
+ }
371
+ };
372
+
373
+ // ../../registry/sections/codeDemo/schema.ts
374
+ import { z as z7 } from "zod";
375
+ var snippetSchema = z7.object({
376
+ lang: z7.string().min(1),
377
+ label: z7.string().min(1),
378
+ source: z7.string().min(1)
379
+ });
380
+ var propsSchema5 = z7.object({
381
+ heading: z7.string().min(1),
382
+ subhead: z7.string().optional(),
383
+ snippets: z7.array(snippetSchema).min(1).max(6)
384
+ });
385
+ var entrySchema5 = z7.object({
386
+ type: z7.literal("codeDemo"),
387
+ id: z7.string().min(1),
388
+ props: propsSchema5
389
+ });
390
+ var manifest5 = {
391
+ type: "codeDemo",
392
+ title: "Code / Integration demo",
393
+ description: "Multi-language code tabs with shiki highlighting and copy-to-clipboard.",
394
+ required: true,
395
+ archetypes: ["sdk-infra", "technical-app"],
396
+ componentFile: "section.astro",
397
+ defaultProps: {
398
+ heading: "Drop it into your stack",
399
+ subhead: "Same API across languages.",
400
+ snippets: [
401
+ {
402
+ lang: "python",
403
+ label: "Python",
404
+ source: 'from branchy import Client\nc = Client()\nc.create("feat-x")'
405
+ },
406
+ {
407
+ lang: "javascript",
408
+ label: "Node",
409
+ source: "import { Client } from 'branchy'\nawait new Client().create('feat-x')"
410
+ },
411
+ { lang: "go", label: "Go", source: 'client := branchy.New()\nclient.Create("feat-x")' }
412
+ ]
413
+ }
414
+ };
415
+
416
+ // ../../registry/sections/pricing/schema.ts
417
+ import { z as z8 } from "zod";
418
+ var tierSchema = z8.object({
419
+ name: z8.string().min(1),
420
+ price: z8.string().min(1),
421
+ period: z8.string().optional(),
422
+ description: z8.string().min(1),
423
+ features: z8.array(z8.string().min(1)).min(1),
424
+ cta: z8.object({ label: z8.string().min(1), href: z8.string().min(1) }),
425
+ featured: z8.boolean().default(false)
426
+ });
427
+ var propsSchema6 = z8.object({
428
+ heading: z8.string().min(1),
429
+ subhead: z8.string().optional(),
430
+ tiers: z8.array(tierSchema).min(1).max(4)
431
+ });
432
+ var entrySchema6 = z8.object({
433
+ type: z8.literal("pricing"),
434
+ id: z8.string().min(1),
435
+ props: propsSchema6
436
+ });
437
+ var manifest6 = {
438
+ type: "pricing",
439
+ title: "Pricing",
440
+ description: "Transparent pricing tiers with honest constraints (rate limits, free tier).",
441
+ required: true,
442
+ archetypes: ["sdk-infra", "technical-app", "general"],
443
+ componentFile: "section.astro",
444
+ defaultProps: {
445
+ heading: "Honest pricing",
446
+ subhead: "Start free. No credit card.",
447
+ tiers: [
448
+ {
449
+ name: "Open source",
450
+ price: "$0",
451
+ description: "Self-host, unlimited branches.",
452
+ features: ["Self-hosted", "Community support", "Apache-2.0"],
453
+ cta: { label: "Get started", href: "#get-started" }
454
+ },
455
+ {
456
+ name: "Team",
457
+ price: "$29",
458
+ period: "/mo",
459
+ description: "Managed cloud for small teams.",
460
+ features: ["10 projects", "Daily backups", "Email support"],
461
+ cta: { label: "Start trial", href: "#trial" },
462
+ featured: true
463
+ }
464
+ ]
465
+ }
466
+ };
467
+
468
+ // ../../registry/sections/finalCta/schema.ts
469
+ import { z as z9 } from "zod";
470
+ var propsSchema7 = z9.object({
471
+ headline: z9.string().min(1),
472
+ subhead: z9.string().optional(),
473
+ cta: z9.object({ label: z9.string().min(1), href: z9.string().min(1) }),
474
+ microcopy: z9.string().optional()
475
+ });
476
+ var entrySchema7 = z9.object({
477
+ type: z9.literal("finalCta"),
478
+ id: z9.string().min(1),
479
+ props: propsSchema7
480
+ });
481
+ var manifest7 = {
482
+ type: "finalCta",
483
+ title: "Final CTA",
484
+ description: "Closing call to action with action-specific label and honest microcopy.",
485
+ required: true,
486
+ archetypes: ["sdk-infra", "technical-app", "general"],
487
+ componentFile: "section.astro",
488
+ defaultProps: {
489
+ headline: "Ship your first branch today",
490
+ subhead: "Free and open source. Self-host in minutes.",
491
+ cta: { label: "Start free", href: "#get-started" },
492
+ microcopy: "No credit card \xB7 Apache-2.0"
493
+ }
494
+ };
495
+
496
+ // ../../registry/sections/src/registry.ts
497
+ var sectionModules = [
498
+ { manifest, propsSchema, entrySchema },
499
+ { manifest: manifest2, propsSchema: propsSchema2, entrySchema: entrySchema2 },
500
+ { manifest: manifest3, propsSchema: propsSchema3, entrySchema: entrySchema3 },
501
+ {
502
+ manifest: manifest4,
503
+ propsSchema: propsSchema4,
504
+ entrySchema: entrySchema4
505
+ },
506
+ {
507
+ manifest: manifest5,
508
+ propsSchema: propsSchema5,
509
+ entrySchema: entrySchema5
510
+ },
511
+ {
512
+ manifest: manifest6,
513
+ propsSchema: propsSchema6,
514
+ entrySchema: entrySchema6
515
+ },
516
+ {
517
+ manifest: manifest7,
518
+ propsSchema: propsSchema7,
519
+ entrySchema: entrySchema7
520
+ }
521
+ ];
522
+ var registry = Object.fromEntries(
523
+ sectionModules.map((m) => [m.manifest.type, m])
524
+ );
525
+ function listSections() {
526
+ return [...sectionModules];
527
+ }
528
+ function getSection(type) {
529
+ return registry[type];
530
+ }
531
+ var entrySchemas = sectionModules.map((m) => m.entrySchema);
532
+ var pageDocumentSchema = buildPageDocumentSchema(
533
+ entrySchemas
534
+ );
535
+
536
+ // src/registry/paths.ts
537
+ import { existsSync } from "fs";
538
+ import { dirname as dirname2, join as join3, resolve } from "path";
539
+ import { fileURLToPath as fileURLToPath2 } from "url";
540
+ var RegistryError = class extends Error {
541
+ constructor(message) {
542
+ super(message);
543
+ this.name = "RegistryError";
544
+ }
545
+ };
546
+ function getRegistryRoot() {
547
+ const override = process.env.PAGELATHE_REGISTRY_DIR;
548
+ if (override && override.trim() !== "") return resolve(override);
549
+ let dir = dirname2(fileURLToPath2(import.meta.url));
550
+ for (let i = 0; i < 8; i++) {
551
+ const candidate = join3(dir, "registry");
552
+ if (existsSync(join3(candidate, "sections")) && existsSync(join3(candidate, "app"))) {
553
+ return candidate;
554
+ }
555
+ const parent = dirname2(dir);
556
+ if (parent === dir) break;
557
+ dir = parent;
558
+ }
559
+ throw new RegistryError(
560
+ "Could not locate the pagelathe registry. Set PAGELATHE_REGISTRY_DIR to the registry/ directory."
561
+ );
562
+ }
563
+ function getSectionsDir() {
564
+ return join3(getRegistryRoot(), "sections");
565
+ }
566
+ function getAppDir() {
567
+ return join3(getRegistryRoot(), "app");
568
+ }
569
+
570
+ // src/registry/read.ts
571
+ function listAvailableSections() {
572
+ return listSections().map((s) => ({
573
+ type: s.manifest.type,
574
+ title: s.manifest.title,
575
+ required: s.manifest.required
576
+ }));
577
+ }
578
+ function sectionComponentPath(type) {
579
+ const known = listSections().some((s) => s.manifest.type === type);
580
+ if (!known) throw new RegistryError(`Unknown section: "${type}".`);
581
+ const path = join4(getSectionsDir(), type, "section.astro");
582
+ if (!existsSync2(path)) {
583
+ throw new RegistryError(`Section "${type}" has no section.astro at ${path}.`);
584
+ }
585
+ return path;
586
+ }
587
+ var DEFAULT_PAGE_SECTIONS = [
588
+ "header",
589
+ "hero",
590
+ "features",
591
+ "codeDemo",
592
+ "pricing",
593
+ "finalCta",
594
+ "footer"
595
+ ];
596
+
597
+ // src/fs/scaffold.ts
598
+ import {
599
+ cpSync,
600
+ existsSync as existsSync3,
601
+ mkdirSync as mkdirSync2,
602
+ readdirSync,
603
+ copyFileSync,
604
+ rmSync,
605
+ readFileSync as readFileSync3,
606
+ writeFileSync as writeFileSync2
607
+ } from "fs";
608
+ import { dirname as dirname3, join as join5 } from "path";
609
+ var COPY_SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".astro"]);
610
+ function copyAppTemplate(srcDir, destDir) {
611
+ const created = [];
612
+ cpSync(srcDir, destDir, {
613
+ recursive: true,
614
+ filter: (src) => {
615
+ const base = src.split(/[\\/]/).pop() ?? "";
616
+ if (COPY_SKIP.has(base)) return false;
617
+ if (/[\\/]src[\\/]components[\\/]sections[\\/].+\.astro$/.test(src)) return false;
618
+ return true;
619
+ }
620
+ });
621
+ created.push(destDir);
622
+ return created;
623
+ }
624
+ function isNonEmptyDir(dir) {
625
+ if (!existsSync3(dir)) return false;
626
+ return readdirSync(dir).filter((n) => n !== ".git").length > 0;
627
+ }
628
+ function copyInto(srcFile, destFile) {
629
+ mkdirSync2(dirname3(destFile), { recursive: true });
630
+ copyFileSync(srcFile, destFile);
631
+ }
632
+ function finalizeScaffold(destDir) {
633
+ rmSync(join5(destDir, "scripts"), { recursive: true, force: true });
634
+ const pkgPath = join5(destDir, "package.json");
635
+ if (!existsSync3(pkgPath)) return;
636
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
637
+ if (pkg.scripts) {
638
+ delete pkg.scripts.prebuild;
639
+ delete pkg.scripts["clean:sections"];
640
+ }
641
+ writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
642
+ }
643
+
644
+ // src/commands/init-cmd.ts
645
+ async function runInit(targetDir, opts = {}) {
646
+ const dest = resolve2(targetDir);
647
+ if (isNonEmptyDir(dest) && !opts.force) {
648
+ throw new Error(`Directory ${dest} is not empty. Use --force to scaffold anyway.`);
649
+ }
650
+ const created = copyAppTemplate(getAppDir(), dest);
651
+ for (const type of DEFAULT_PAGE_SECTIONS) {
652
+ copyInto(
653
+ sectionComponentPath(type),
654
+ join6(dest, "src", "components", "sections", `${type}.astro`)
655
+ );
656
+ }
657
+ finalizeScaffold(dest);
658
+ return { created };
659
+ }
660
+ function registerInitCommand(program) {
661
+ program.command("init [dir]").description("scaffold a new pagelathe landing project").option("-f, --force", "scaffold into a non-empty directory").action(async (dir, options) => {
662
+ const target = dir ?? ".";
663
+ await runInit(target, options);
664
+ console.log(`\u2713 Scaffolded pagelathe project in ${resolve2(target)}.`);
665
+ console.log(` Next: cd ${target} && pnpm install && pnpm dev`);
666
+ });
667
+ }
668
+
669
+ // src/commands/add-cmd.ts
670
+ import { existsSync as existsSync4 } from "fs";
671
+ import { join as join7, resolve as resolve3 } from "path";
672
+ async function runAdd(type, opts = {}) {
673
+ const src = sectionComponentPath(type);
674
+ const cwd = resolve3(opts.cwd ?? process.cwd());
675
+ const dest = join7(cwd, "src", "components", "sections", `${type}.astro`);
676
+ if (existsSync4(dest) && !opts.force) {
677
+ throw new Error(`${dest} exists. Use --force to overwrite.`);
678
+ }
679
+ copyInto(src, dest);
680
+ return { written: dest };
681
+ }
682
+ function registerAddCommand(program) {
683
+ program.command("add <section>").description("vendor a section component from the registry into this project").option("-f, --force", "overwrite an existing section file").action(async (section, options) => {
684
+ try {
685
+ const { written } = await runAdd(section, options);
686
+ console.log(`\u2713 Added ${section} \u2192 ${written}`);
687
+ } catch (err) {
688
+ const available = listAvailableSections().map((s) => s.type).join(", ");
689
+ const msg = err instanceof Error ? err.message : String(err);
690
+ throw new Error(`${msg}
691
+ Available sections: ${available}`);
692
+ }
693
+ });
694
+ }
695
+
696
+ // src/gen/provider.ts
697
+ import { zodToJsonSchema } from "zod-to-json-schema";
698
+
699
+ // src/gen/llm.ts
700
+ var LlmError = class extends Error {
701
+ constructor(message, detail) {
702
+ super(message);
703
+ this.detail = detail;
704
+ this.name = "LlmError";
705
+ }
706
+ detail;
707
+ };
708
+
709
+ // src/gen/provider.ts
710
+ var DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
711
+ function createOpenRouterClient(opts) {
712
+ const fetchImpl = opts.fetchImpl ?? fetch;
713
+ const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
714
+ async function callOnce(messages, schemaName, jsonSchema) {
715
+ const res = await fetchImpl(`${baseUrl}/chat/completions`, {
716
+ method: "POST",
717
+ headers: {
718
+ Authorization: `Bearer ${opts.apiKey}`,
719
+ "Content-Type": "application/json",
720
+ ...opts.appUrl ? { "HTTP-Referer": opts.appUrl } : {},
721
+ ...opts.appName ? { "X-Title": opts.appName } : {}
722
+ },
723
+ body: JSON.stringify({
724
+ model: opts.model,
725
+ messages,
726
+ response_format: {
727
+ type: "json_schema",
728
+ json_schema: { name: schemaName, strict: true, schema: jsonSchema }
729
+ }
730
+ })
731
+ });
732
+ if (!res.ok) {
733
+ throw new LlmError(`OpenRouter request failed (HTTP ${res.status}).`, {
734
+ status: res.status
735
+ });
736
+ }
737
+ const data = await res.json();
738
+ const content = data.choices?.[0]?.message?.content;
739
+ if (typeof content !== "string") throw new LlmError("OpenRouter returned no content.");
740
+ return content;
741
+ }
742
+ return {
743
+ async generateObject(schema, options) {
744
+ const maxRetries = options.maxRetries ?? 2;
745
+ const schemaName = options.schemaName ?? "result";
746
+ const jsonSchema = zodToJsonSchema(schema, { $refStrategy: "none" });
747
+ const messages = [
748
+ { role: "system", content: options.system },
749
+ {
750
+ role: "user",
751
+ content: `${options.prompt}
752
+
753
+ Return ONLY a JSON object that matches the schema. No prose, no markdown fences.`
754
+ }
755
+ ];
756
+ let lastErr = "";
757
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
758
+ let content;
759
+ try {
760
+ content = await callOnce(messages, schemaName, jsonSchema);
761
+ } catch (err) {
762
+ if (err instanceof LlmError && err.detail?.status !== void 0 && err.detail.status >= 500 && attempt < maxRetries) {
763
+ continue;
764
+ }
765
+ throw err instanceof LlmError ? err : new LlmError("OpenRouter request error.");
766
+ }
767
+ const parsed = safeJson(content);
768
+ const result = parsed === void 0 ? void 0 : schema.safeParse(parsed);
769
+ if (result?.success) return result.data;
770
+ lastErr = result && !result.success ? JSON.stringify(result.error.issues) : "Output was not valid JSON.";
771
+ messages.push({ role: "assistant", content });
772
+ messages.push({
773
+ role: "user",
774
+ content: `That output was invalid: ${lastErr}
775
+ Return a corrected JSON object that matches the schema exactly.`
776
+ });
777
+ }
778
+ throw new LlmError(
779
+ `Model did not produce schema-valid output after ${maxRetries + 1} attempts.`,
780
+ { attempts: maxRetries + 1 }
781
+ );
782
+ }
783
+ };
784
+ }
785
+ function safeJson(text2) {
786
+ const trimmed = text2.trim().replace(/^```(?:json)?\s*/i, "").replace(/```$/i, "");
787
+ try {
788
+ return JSON.parse(trimmed);
789
+ } catch {
790
+ return void 0;
791
+ }
792
+ }
793
+
794
+ // src/gen/generate.ts
795
+ import { join as join9 } from "path";
796
+
797
+ // src/gen/archetype.ts
798
+ import { z as z10 } from "zod";
799
+
800
+ // src/gen/prompts.ts
801
+ var BANNED_WORDS = [
802
+ "scalable",
803
+ "powerful",
804
+ "easy",
805
+ "seamless",
806
+ "robust",
807
+ "cutting-edge",
808
+ "revolutionary",
809
+ "game-changing"
810
+ ];
811
+ var BRAND_RULES = `You write landing-page copy for technical founders building developer tools.
812
+ Rules (non-negotiable):
813
+ - Anti-fluff: never use these words: ${BANNED_WORDS.join(", ")}. Be concrete and specific.
814
+ - Dark-mode-first, terse, honest. No marketing hyperbole.
815
+ - Keep terminology consistent across sections (same product nouns and verbs).
816
+ - For sdk-infra/technical-app products, lead with real code and real numbers.
817
+ - Pricing must be honest: name real constraints (rate limits, free tier, OSS).`;
818
+ var archetypeSystem = `You classify developer-product landing pages into one archetype.
819
+ ${BRAND_RULES}`;
820
+ var plannerSystem = `You plan the section order of a developer landing page.
821
+ ${BRAND_RULES}`;
822
+ var metaSystem = `You write page metadata (title, description) for a developer landing page.
823
+ ${BRAND_RULES}`;
824
+ function fillSystem(type, archetype) {
825
+ return `You write the content for the "${type}" section of a ${archetype} developer landing page.
826
+ ${BRAND_RULES}
827
+ Fill every required field of the JSON schema with concrete, on-brand content.`;
828
+ }
829
+
830
+ // src/gen/archetype.ts
831
+ var resultSchema = z10.object({
832
+ archetype: z10.enum(["sdk-infra", "technical-app", "general"]),
833
+ reason: z10.string().min(1)
834
+ });
835
+ async function classifyArchetype(description, llm) {
836
+ const { archetype } = await llm.generateObject(resultSchema, {
837
+ system: archetypeSystem,
838
+ schemaName: "archetype",
839
+ prompt: `Classify this product into one archetype.
840
+ - sdk-infra: SDKs, APIs, databases, infra, CLIs, libraries (code-forward).
841
+ - technical-app: technical SaaS / apps with a UI for technical users.
842
+ - general: anything else technical.
843
+
844
+ Product description:
845
+ ${description}`
846
+ });
847
+ return archetype;
848
+ }
849
+
850
+ // src/gen/planner.ts
851
+ import { z as z11 } from "zod";
852
+ var CANONICAL_ORDER = [
853
+ "header",
854
+ "hero",
855
+ "features",
856
+ "codeDemo",
857
+ "pricing",
858
+ "finalCta",
859
+ "footer"
860
+ ];
861
+ var REQUIRED = ["header", "hero", "features", "pricing", "finalCta", "footer"];
862
+ async function planSections(input, llm) {
863
+ const available = listSections().map((s) => s.manifest.type);
864
+ const schema = z11.object({
865
+ sections: z11.array(z11.enum(available)).min(1)
866
+ });
867
+ const { sections } = await llm.generateObject(schema, {
868
+ system: plannerSystem,
869
+ schemaName: "section_plan",
870
+ prompt: `Choose which sections this ${input.archetype} product page should include, from: ${available.join(", ")}.
871
+ Always include: ${REQUIRED.join(", ")}.
872
+ For sdk-infra/technical-app, include codeDemo.
873
+ Return them in the order they should appear.
874
+
875
+ Product description:
876
+ ${input.description}`
877
+ });
878
+ const chosen = new Set(sections);
879
+ for (const r of REQUIRED) chosen.add(r);
880
+ if (input.archetype !== "general") chosen.add("codeDemo");
881
+ return CANONICAL_ORDER.filter((t) => chosen.has(t));
882
+ }
883
+
884
+ // src/gen/fill.ts
885
+ import { z as z12 } from "zod";
886
+ async function fillSection(input, llm) {
887
+ const section = getSection(input.type);
888
+ if (!section) throw new Error(`Unknown section: ${input.type}`);
889
+ return llm.generateObject(section.propsSchema, {
890
+ system: fillSystem(input.type, input.archetype),
891
+ schemaName: `${input.type}_props`,
892
+ prompt: `Product "${input.brand}": ${input.description}
893
+
894
+ Write the props for the "${input.type}" section. Use real, specific copy consistent with the product. Reuse the brand name "${input.brand}" where a brand is needed.`
895
+ });
896
+ }
897
+ var metaResultSchema = z12.object({
898
+ title: z12.string().min(1),
899
+ description: z12.string().min(1),
900
+ primaryGoal: z12.enum(["signup", "github_star", "docs", "contact", "waitlist", "purchase"]),
901
+ brand: z12.string().min(1)
902
+ });
903
+ async function deriveMeta(input, llm) {
904
+ return llm.generateObject(metaResultSchema, {
905
+ system: metaSystem,
906
+ schemaName: "page_meta",
907
+ prompt: `From this product description, produce: a short brand name, an SEO title ("<Brand> \u2014 <value>"), a one-sentence meta description, and the primary conversion goal.
908
+
909
+ Description:
910
+ ${input.description}`
911
+ });
912
+ }
913
+
914
+ // src/gen/yaml-doc.ts
915
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, renameSync } from "fs";
916
+ import { dirname as dirname4, join as join8 } from "path";
917
+ import { stringify as yamlStringify } from "yaml";
918
+ function writeDocumentYaml(doc, destFile) {
919
+ mkdirSync3(dirname4(destFile), { recursive: true });
920
+ const tmp = join8(dirname4(destFile), `.index.yaml.tmp-${process.pid}`);
921
+ writeFileSync3(tmp, yamlStringify(doc), "utf8");
922
+ renameSync(tmp, destFile);
923
+ }
924
+
925
+ // src/gen/generate.ts
926
+ async function generate(input) {
927
+ const log = input.onProgress ?? (() => {
928
+ });
929
+ log("Classifying product\u2026");
930
+ const archetype = await classifyArchetype(input.description, input.llm);
931
+ log(`Archetype: ${archetype}`);
932
+ const meta = await deriveMeta({ description: input.description }, input.llm);
933
+ log(`Planning sections\u2026`);
934
+ const types = await planSections({ description: input.description, archetype }, input.llm);
935
+ const sections = [];
936
+ for (const type of types) {
937
+ log(`Writing ${type}\u2026`);
938
+ const props = await fillSection(
939
+ { type, description: input.description, archetype, brand: meta.brand },
940
+ input.llm
941
+ );
942
+ sections.push({ type, id: `${type}-1`, props });
943
+ }
944
+ const document = pageDocumentSchema.parse({
945
+ meta: {
946
+ title: meta.title,
947
+ description: meta.description,
948
+ locales: ["en"],
949
+ primaryGoal: meta.primaryGoal
950
+ },
951
+ theme: { tokens: {} },
952
+ archetype,
953
+ sections
954
+ });
955
+ for (const type of types) {
956
+ copyInto(
957
+ sectionComponentPath(type),
958
+ join9(input.cwd, "src", "components", "sections", `${type}.astro`)
959
+ );
960
+ }
961
+ const yamlPath = join9(input.cwd, "src", "content", "landing", "index.yaml");
962
+ writeDocumentYaml(document, yamlPath);
963
+ return { document, vendored: types, yamlPath };
964
+ }
965
+
966
+ // src/commands/generate-cmd.ts
967
+ async function runGenerate(opts) {
968
+ const description = opts.description?.trim();
969
+ if (!description) throw new Error("A product description is required.");
970
+ let llm = opts.llm;
971
+ if (!llm) {
972
+ const key = getApiKey();
973
+ if (!key) {
974
+ throw new Error(
975
+ "No OpenRouter key found. Run `pagelathe config set-key` or set OPENROUTER_API_KEY."
976
+ );
977
+ }
978
+ const model = opts.model ?? loadConfig().provider.defaultModel;
979
+ llm = createOpenRouterClient({ apiKey: key, model, appName: "pagelathe" });
980
+ }
981
+ return generate({
982
+ description,
983
+ cwd: opts.cwd ?? process.cwd(),
984
+ llm,
985
+ onProgress: opts.onProgress
986
+ });
987
+ }
988
+ function registerGenerateCommand(program) {
989
+ program.command("generate").description("describe your product and generate an on-brand landing page").option("-d, --description <text>", "product description (skips the prompt)").option("-m, --model <id>", "OpenRouter model id (defaults to your config)").action(async (options) => {
990
+ const description = options.description ?? await promptText("Describe your product (what it does, who it's for):");
991
+ const res = await runGenerate({
992
+ description,
993
+ model: options.model,
994
+ onProgress: (m) => console.log(` ${m}`)
995
+ });
996
+ console.log(`
997
+ \u2713 Generated ${res.vendored.length} sections \u2192 ${res.yamlPath}`);
998
+ console.log(` Next: pnpm dev`);
999
+ });
1000
+ }
1001
+
1002
+ // src/index.ts
1003
+ function buildProgram() {
1004
+ const program = new Command();
1005
+ program.name("pagelathe").description("AI landing-page builder for technical founders").version(getVersion(), "-v, --version", "print the pagelathe version");
1006
+ registerConfigCommand(program);
1007
+ registerInitCommand(program);
1008
+ registerAddCommand(program);
1009
+ registerGenerateCommand(program);
1010
+ return program;
1011
+ }
1012
+ async function cli(argv) {
1013
+ const program = buildProgram();
1014
+ await program.parseAsync(argv);
1015
+ }
1016
+
1017
+ export {
1018
+ buildProgram,
1019
+ cli
1020
+ };