@kdrgny/justjson 1.4.0 → 1.7.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/README.md CHANGED
@@ -42,6 +42,9 @@ Design your schema, enter content, upload images. Everything is written to `cont
42
42
  | **Type-safe output** | Generates `types.ts` **and** a zero-dependency `content.ts` loader — `loadPosts()`, `loadSettings()` — so your content is typed in your build. |
43
43
  | **Validate in CI** | `justjson validate` checks content against the schema — broken relations, duplicate slugs, missing media, type errors. Exits non-zero on failure. |
44
44
  | **Draft / published** | Toggle an entry's status; the generated loader returns published entries by default. |
45
+ | **One-click site** | No site yet? `init --astro` (or a button in the editor) generates a working Astro site around your content. |
46
+ | **Ship it** | Setup snippet for your framework, one-click content commit, push to GitHub, then deploy links for Vercel and Netlify — using the `git`/`gh` already on your machine. No tokens stored. |
47
+ | **Astro collections** | [`@kdrgny/justjson-astro`](https://www.npmjs.com/package/@kdrgny/justjson-astro) turns your content into typed Astro content collections in one line. |
45
48
  | **English or Turkish** | The editor ships in English; switch to Turkish from the project menu any time. |
46
49
  | **Export any time** | One click downloads schema + content + types as a ZIP. Nothing is ever locked in. |
47
50
  | **The endpoint is yours** | Wherever you put the JSON becomes your API — repo raw, jsDelivr, your build. |
@@ -53,7 +56,7 @@ Field types: `text` · `richtext` · `number` · `boolean` · `date` · `select`
53
56
  | Command | What it does |
54
57
  |---|---|
55
58
  | `npx @kdrgny/justjson` (or `serve`) | Starts the local editor and opens it in your browser |
56
- | `npx @kdrgny/justjson init [template]` | Scaffolds a schema from a template (`blog`, `cv`, `portfolio`, `docs`, `changelog`, `recipe`, `event`, `catalog`) |
59
+ | `npx @kdrgny/justjson init [template]` | Scaffolds a schema from a template. `--astro` also generates a working site |
57
60
  | `npx @kdrgny/justjson types` | Generates `types.ts` + a typed `content.ts` loader from your schema |
58
61
  | `npx @kdrgny/justjson validate` | Checks content against the schema (`--json`, `--strict`) — great for CI |
59
62
  | `npx @kdrgny/justjson export` | Exports a ZIP snapshot (schema + content + types) |
package/dist/cli.js CHANGED
@@ -6,11 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/cli.ts
9
- import { Command } from "commander";
10
-
11
- // src/commands/export.ts
12
- import { writeFile as writeFile2 } from "fs/promises";
13
- import { join } from "path";
9
+ import { basename as basename3 } from "path";
14
10
 
15
11
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
16
12
  var external_exports = {};
@@ -4712,7 +4708,12 @@ function inferProject(data) {
4712
4708
  return { schema: { version: 1, collections, singletons }, entries, singletons: singletonData };
4713
4709
  }
4714
4710
 
4711
+ // src/cli.ts
4712
+ import { Command } from "commander";
4713
+
4715
4714
  // src/commands/export.ts
4715
+ import { writeFile as writeFile2 } from "fs/promises";
4716
+ import { join } from "path";
4716
4717
  import { zipSync } from "fflate";
4717
4718
 
4718
4719
  // src/fs-adapter.ts
@@ -4879,7 +4880,10 @@ var blog_default = {
4879
4880
  slug: "first-post",
4880
4881
  body: "Hello."
4881
4882
  }
4882
- ]
4883
+ ],
4884
+ settings: {
4885
+ title: "My blog"
4886
+ }
4883
4887
  }
4884
4888
  };
4885
4889
 
@@ -4960,7 +4964,11 @@ var catalog_default = {
4960
4964
  category: "Home",
4961
4965
  description: "Hand-made ceramic mug."
4962
4966
  }
4963
- ]
4967
+ ],
4968
+ store: {
4969
+ title: "My store",
4970
+ tagline: "Small batch, made well"
4971
+ }
4964
4972
  }
4965
4973
  };
4966
4974
 
@@ -5081,7 +5089,11 @@ var cv_default = {
5081
5089
  company: "Acme Inc.",
5082
5090
  summary: "..."
5083
5091
  }
5084
- ]
5092
+ ],
5093
+ profile: {
5094
+ name: "Ada Lovelace",
5095
+ headline: "Software engineer"
5096
+ }
5085
5097
  }
5086
5098
  };
5087
5099
 
@@ -5150,7 +5162,11 @@ var docs_default = {
5150
5162
  order: 1,
5151
5163
  body: "Welcome aboard."
5152
5164
  }
5153
- ]
5165
+ ],
5166
+ settings: {
5167
+ title: "Docs",
5168
+ tagline: "Everything you need to know"
5169
+ }
5154
5170
  }
5155
5171
  };
5156
5172
 
@@ -5236,7 +5252,11 @@ var event_default = {
5236
5252
  room: "Main hall",
5237
5253
  description: "Welcome to the event."
5238
5254
  }
5239
- ]
5255
+ ],
5256
+ event: {
5257
+ title: "My event",
5258
+ tagline: "One day, one track"
5259
+ }
5240
5260
  }
5241
5261
  };
5242
5262
 
@@ -5333,7 +5353,11 @@ var portfolio_default = {
5333
5353
  kind: "Web",
5334
5354
  body: "Project details go here."
5335
5355
  }
5336
- ]
5356
+ ],
5357
+ about: {
5358
+ name: "Ada Lovelace",
5359
+ bio: "I build things for the web."
5360
+ }
5337
5361
  }
5338
5362
  };
5339
5363
 
@@ -5414,7 +5438,10 @@ var recipe_default = {
5414
5438
  ingredients: "- 200g pasta\n- 2 cloves garlic\n- Olive oil",
5415
5439
  steps: "Boil the pasta. Fry the garlic in olive oil. Toss together."
5416
5440
  }
5417
- ]
5441
+ ],
5442
+ kitchen: {
5443
+ title: "My recipe box"
5444
+ }
5418
5445
  }
5419
5446
  };
5420
5447
 
@@ -5454,10 +5481,15 @@ async function applyTemplate(adapter, contentDir, template) {
5454
5481
  const schema = parseSchema(template.schema);
5455
5482
  await saveSchema(adapter, schema, contentDir);
5456
5483
  const store = new ContentStore(adapter, schema, contentDir);
5457
- for (const [collection, rows] of Object.entries(template.samples)) {
5458
- for (const row of rows) {
5484
+ const singletonNames = new Set(schema.singletons.map((s) => s.name));
5485
+ for (const [name, sample] of Object.entries(template.samples)) {
5486
+ if (!Array.isArray(sample)) {
5487
+ if (singletonNames.has(name)) await store.writeSingleton(name, sample);
5488
+ continue;
5489
+ }
5490
+ for (const row of sample) {
5459
5491
  const slug = slugify(typeof row.slug === "string" ? row.slug : String(row.title ?? "content"));
5460
- await store.writeEntry(collection, slug, row);
5492
+ await store.writeEntry(name, slug, row);
5461
5493
  }
5462
5494
  }
5463
5495
  return schema;
@@ -5566,13 +5598,314 @@ function formatJson(issues) {
5566
5598
  return JSON.stringify({ ok: errors === 0, errors, warnings, issues }, null, 2);
5567
5599
  }
5568
5600
 
5601
+ // src/scaffold.ts
5602
+ import { access, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
5603
+ import { dirname as dirname2, join as join4 } from "path";
5604
+ var ASTRO_VERSION = "^5.18.0";
5605
+ var LOADER_VERSION = "^1.7.0";
5606
+ var STYLE = ` <style is:global>
5607
+ :root { color-scheme: light dark; }
5608
+ body {
5609
+ margin: 0 auto;
5610
+ max-width: 44rem;
5611
+ padding: 3rem 1.25rem 6rem;
5612
+ font: 16px/1.65 ui-sans-serif, system-ui, sans-serif;
5613
+ }
5614
+ h1 { font-size: 1.9rem; line-height: 1.2; margin: 0 0 0.35rem; }
5615
+ h2 { font-size: 1.05rem; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.6; margin: 2.75rem 0 0.75rem; }
5616
+ ul { list-style: none; margin: 0; padding: 0; }
5617
+ li + li { margin-top: 0.6rem; }
5618
+ a { color: inherit; }
5619
+ time { opacity: 0.6; font-size: 0.9rem; }
5620
+ img { max-width: 100%; height: auto; border-radius: 8px; }
5621
+ .body { white-space: pre-wrap; }
5622
+ </style>`;
5623
+ function titleFieldOf(fields) {
5624
+ const preferred = fields.find(
5625
+ (f) => ["title", "name", "label", "role", "version"].includes(f.key)
5626
+ );
5627
+ return (preferred ?? fields.find((f) => f.type === "text"))?.key ?? "title";
5628
+ }
5629
+ function safePackageName(name) {
5630
+ return slugify(name) || "my-site";
5631
+ }
5632
+ function detailPage(collection) {
5633
+ const titleKey = titleFieldOf(collection.fields);
5634
+ const image = collection.fields.find((f) => f.type === "image");
5635
+ const rich = collection.fields.find((f) => f.type === "richtext");
5636
+ const date = collection.fields.find((f) => f.type === "date");
5637
+ const parts = [
5638
+ `<h1>{entry.data.${titleKey} ?? entry.id}</h1>`,
5639
+ date && `{entry.data.${date.key} && <time>{entry.data.${date.key}}</time>}`,
5640
+ image && `{entry.data.${image.key} && <img src={\`/media/\${entry.data.${image.key}.split('/').pop()}\`} alt="" />}`,
5641
+ rich && `{entry.data.${rich.key} && <div class="body">{entry.data.${rich.key}}</div>}`
5642
+ ].filter(Boolean);
5643
+ return `---
5644
+ import { getCollection } from 'astro:content'
5645
+
5646
+ export async function getStaticPaths() {
5647
+ const entries = await getCollection('${collection.name}')
5648
+ return entries.map((entry) => ({ params: { slug: entry.id }, props: { entry } }))
5649
+ }
5650
+
5651
+ const { entry } = Astro.props
5652
+ ---
5653
+ <html lang="en">
5654
+ <head>
5655
+ <meta charset="utf-8" />
5656
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5657
+ <title>{entry.data.${titleKey} ?? entry.id}</title>
5658
+ ${STYLE}
5659
+ </head>
5660
+ <body>
5661
+ <p><a href="/">&larr; Back</a></p>
5662
+ ${parts.map((p) => ` ${p}`).join("\n")}
5663
+ </body>
5664
+ </html>
5665
+ `;
5666
+ }
5667
+ function indexPage(schema, projectName) {
5668
+ const singleton = schema.singletons[0];
5669
+ const imports = ["getCollection", singleton && "getEntry"].filter(Boolean).join(", ");
5670
+ const loads = schema.collections.map(
5671
+ (c) => `const ${varFor(c.name)} = await getCollection('${c.name}')`
5672
+ );
5673
+ if (singleton) {
5674
+ loads.unshift(`const site = await getEntry('${singleton.name}', '${singleton.name}')`);
5675
+ }
5676
+ const heading = singleton ? `{site?.data.${titleFieldOf(singleton.fields)} ?? '${projectName}'}` : `${projectName}`;
5677
+ const sections = schema.collections.map((c) => {
5678
+ const titleKey = titleFieldOf(c.fields);
5679
+ return ` <h2>${c.label ?? c.name}</h2>
5680
+ <ul>
5681
+ {${varFor(c.name)}.map((entry) => (
5682
+ <li>
5683
+ <a href={\`/${c.name}/\${entry.id}\`}>{entry.data.${titleKey} ?? entry.id}</a>
5684
+ </li>
5685
+ ))}
5686
+ </ul>`;
5687
+ });
5688
+ const empty = ` <p>No content yet \u2014 run <code>npx @kdrgny/justjson</code> and add some.</p>`;
5689
+ return `---
5690
+ import { ${imports} } from 'astro:content'
5691
+
5692
+ ${loads.join("\n")}
5693
+ ---
5694
+ <html lang="en">
5695
+ <head>
5696
+ <meta charset="utf-8" />
5697
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5698
+ <title>${singleton ? `{site?.data.${titleFieldOf(singleton.fields)} ?? '${projectName}'}` : projectName}</title>
5699
+ ${STYLE}
5700
+ </head>
5701
+ <body>
5702
+ <h1>${heading}</h1>
5703
+ ${sections.length ? sections.join("\n") : empty}
5704
+ </body>
5705
+ </html>
5706
+ `;
5707
+ }
5708
+ function varFor(name) {
5709
+ const camel = name.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase());
5710
+ return /^[a-zA-Z_$]/.test(camel) ? camel : `c${camel}`;
5711
+ }
5712
+ function astroSiteFiles(schema, projectName) {
5713
+ const files = {
5714
+ "package.json": `${JSON.stringify(
5715
+ {
5716
+ name: safePackageName(projectName),
5717
+ type: "module",
5718
+ version: "0.0.1",
5719
+ private: true,
5720
+ scripts: { dev: "astro dev", build: "astro build", preview: "astro preview" },
5721
+ dependencies: {
5722
+ "@kdrgny/justjson-astro": LOADER_VERSION,
5723
+ astro: ASTRO_VERSION
5724
+ }
5725
+ },
5726
+ null,
5727
+ 2
5728
+ )}
5729
+ `,
5730
+ "astro.config.mjs": `import { defineConfig } from 'astro/config'
5731
+
5732
+ export default defineConfig({})
5733
+ `,
5734
+ "tsconfig.json": `${JSON.stringify({ extends: "astro/tsconfigs/strict" }, null, 2)}
5735
+ `,
5736
+ ".gitignore": `node_modules
5737
+ dist
5738
+ .astro
5739
+ `,
5740
+ "src/content.config.ts": `import { justjsonCollections } from '@kdrgny/justjson-astro'
5741
+
5742
+ // Every collection and singleton in content/_schema.json, typed from your fields.
5743
+ export const collections = await justjsonCollections()
5744
+ `,
5745
+ "src/pages/index.astro": indexPage(schema, projectName)
5746
+ };
5747
+ for (const collection of schema.collections) {
5748
+ files[`src/pages/${collection.name}/[slug].astro`] = detailPage(collection);
5749
+ }
5750
+ return files;
5751
+ }
5752
+ async function exists(path) {
5753
+ try {
5754
+ await access(path);
5755
+ return true;
5756
+ } catch {
5757
+ return false;
5758
+ }
5759
+ }
5760
+ async function writeAstroSite(root2, schema, projectName) {
5761
+ const files = astroSiteFiles(schema, projectName);
5762
+ const result = { written: [], skipped: [] };
5763
+ for (const [relative, contents] of Object.entries(files)) {
5764
+ const target = join4(root2, relative);
5765
+ if (await exists(target)) {
5766
+ result.skipped.push(relative);
5767
+ continue;
5768
+ }
5769
+ await mkdir2(dirname2(target), { recursive: true });
5770
+ await writeFile3(target, contents, "utf8");
5771
+ result.written.push(relative);
5772
+ }
5773
+ return result;
5774
+ }
5775
+
5569
5776
  // src/server.ts
5570
5777
  import { exec } from "child_process";
5571
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
5572
- import { basename as basename2, dirname as dirname2, extname, join as join4, normalize } from "path";
5778
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
5779
+ import { basename as basename2, dirname as dirname3, extname, join as join6, normalize } from "path";
5573
5780
  import { fileURLToPath } from "url";
5574
5781
  import { serve } from "@hono/node-server";
5575
5782
  import { Hono } from "hono";
5783
+
5784
+ // src/detect.ts
5785
+ import { readFile as readFile2 } from "fs/promises";
5786
+ import { join as join5 } from "path";
5787
+ var SIGNATURES = [
5788
+ ["astro", "astro"],
5789
+ ["next", "next"],
5790
+ ["nuxt", "nuxt"],
5791
+ ["sveltekit", "@sveltejs/kit"],
5792
+ ["vite", "vite"]
5793
+ ];
5794
+ async function detectFramework(root2) {
5795
+ let pkg;
5796
+ try {
5797
+ pkg = JSON.parse(await readFile2(join5(root2, "package.json"), "utf8"));
5798
+ } catch {
5799
+ return "unknown";
5800
+ }
5801
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
5802
+ for (const [framework, dependency] of SIGNATURES) {
5803
+ if (deps[dependency]) return framework;
5804
+ }
5805
+ return "node";
5806
+ }
5807
+
5808
+ // src/git.ts
5809
+ import { execFile } from "child_process";
5810
+ import { promisify } from "util";
5811
+ var run = promisify(execFile);
5812
+ function remoteWebUrl(remote) {
5813
+ if (!remote) return null;
5814
+ const ssh = remote.match(/^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/);
5815
+ if (ssh) return `https://${ssh[1]}/${ssh[2]}`;
5816
+ const https = remote.match(/^https?:\/\/(?:[^@]+@)?(.+?)(?:\.git)?$/);
5817
+ if (https) return `https://${https[1]}`;
5818
+ return null;
5819
+ }
5820
+ async function git(root2, args) {
5821
+ const { stdout } = await run("git", args, { cwd: root2 });
5822
+ return stdout.trim();
5823
+ }
5824
+ async function tryGit(root2, args) {
5825
+ try {
5826
+ return await git(root2, args);
5827
+ } catch {
5828
+ return null;
5829
+ }
5830
+ }
5831
+ async function hasGh() {
5832
+ try {
5833
+ await run("gh", ["--version"]);
5834
+ return true;
5835
+ } catch {
5836
+ return false;
5837
+ }
5838
+ }
5839
+ async function gitStatus(root2, contentDir) {
5840
+ const inside = await tryGit(root2, ["rev-parse", "--is-inside-work-tree"]);
5841
+ if (inside !== "true") {
5842
+ return {
5843
+ isRepo: false,
5844
+ branch: null,
5845
+ hasRemote: false,
5846
+ remoteUrl: null,
5847
+ remoteWebUrl: null,
5848
+ pendingFiles: 0,
5849
+ hasGh: await hasGh()
5850
+ };
5851
+ }
5852
+ const branch = await tryGit(root2, ["branch", "--show-current"]);
5853
+ const remoteUrl = await tryGit(root2, ["remote", "get-url", "origin"]);
5854
+ const porcelain = await tryGit(root2, ["status", "--porcelain", "-uall", "--", contentDir]) ?? "";
5855
+ return {
5856
+ isRepo: true,
5857
+ branch: branch || null,
5858
+ hasRemote: remoteUrl !== null,
5859
+ remoteUrl,
5860
+ remoteWebUrl: remoteWebUrl(remoteUrl),
5861
+ pendingFiles: porcelain ? porcelain.split("\n").filter(Boolean).length : 0,
5862
+ hasGh: await hasGh()
5863
+ };
5864
+ }
5865
+ async function commitContent(root2, contentDir, message) {
5866
+ const inside = await tryGit(root2, ["rev-parse", "--is-inside-work-tree"]);
5867
+ if (inside !== "true") throw new Error("Not a git repository \u2014 run `git init` first.");
5868
+ await git(root2, ["add", "--", contentDir]);
5869
+ const staged = await tryGit(root2, ["diff", "--cached", "--name-only", "--", contentDir]);
5870
+ if (!staged) return { committed: false, count: 0 };
5871
+ await git(root2, ["commit", "-m", message, "--", contentDir]);
5872
+ return { committed: true, count: staged.split("\n").filter(Boolean).length };
5873
+ }
5874
+ async function pushContent(root2) {
5875
+ const branch = await tryGit(root2, ["branch", "--show-current"]);
5876
+ if (!branch || branch === "HEAD") throw new Error("No branch to push \u2014 commit something first.");
5877
+ const hasUpstream = await tryGit(root2, ["rev-parse", "--abbrev-ref", `${branch}@{upstream}`]);
5878
+ const args = hasUpstream ? ["push"] : ["push", "-u", "origin", branch];
5879
+ await git(root2, args);
5880
+ return { branch };
5881
+ }
5882
+ async function createGitHubRepo(root2, options) {
5883
+ if (!await hasGh()) {
5884
+ throw new Error("GitHub CLI (gh) not found \u2014 install it, or add a remote yourself.");
5885
+ }
5886
+ const inside = await tryGit(root2, ["rev-parse", "--is-inside-work-tree"]);
5887
+ if (inside !== "true") {
5888
+ await git(root2, ["init", "-b", "main"]);
5889
+ }
5890
+ await run(
5891
+ "gh",
5892
+ [
5893
+ "repo",
5894
+ "create",
5895
+ options.name,
5896
+ options.private ? "--private" : "--public",
5897
+ "--source",
5898
+ ".",
5899
+ "--push"
5900
+ ],
5901
+ {
5902
+ cwd: root2
5903
+ }
5904
+ );
5905
+ return { name: options.name };
5906
+ }
5907
+
5908
+ // src/server.ts
5576
5909
  var editorDir = fileURLToPath(new URL("./editor", import.meta.url));
5577
5910
  var PROVIDER_BASE_URLS = {
5578
5911
  groq: "https://api.groq.com/openai/v1",
@@ -5669,6 +6002,55 @@ async function createServer(root2) {
5669
6002
  singletons: schema.singletons.length
5670
6003
  })
5671
6004
  );
6005
+ app.post("/api/_ship/scaffold", async (c) => {
6006
+ if (schema.collections.length === 0 && schema.singletons.length === 0) {
6007
+ return c.json({ error: "Pick a template or build a schema first." }, 400);
6008
+ }
6009
+ return c.json(await writeAstroSite(root2, schema, basename2(root2) || "my-site"));
6010
+ });
6011
+ app.get(
6012
+ "/api/_ship",
6013
+ async (c) => c.json({
6014
+ framework: await detectFramework(root2),
6015
+ git: await gitStatus(root2, contentDir)
6016
+ })
6017
+ );
6018
+ app.post("/api/_ship/commit", async (c) => {
6019
+ const { message } = await c.req.json();
6020
+ try {
6021
+ return c.json(await commitContent(root2, contentDir, message?.trim() || "content: update"));
6022
+ } catch (e) {
6023
+ return c.json({ error: e.message }, 400);
6024
+ }
6025
+ });
6026
+ app.post("/api/_ship/publish", async (c) => {
6027
+ const { message } = await c.req.json().catch(() => ({}));
6028
+ try {
6029
+ const commit = await commitContent(root2, contentDir, message?.trim() || "content: update");
6030
+ const { branch } = await pushContent(root2);
6031
+ return c.json({ committed: commit.committed, count: commit.count, branch });
6032
+ } catch (e) {
6033
+ return c.json({ error: e.message }, 400);
6034
+ }
6035
+ });
6036
+ app.post("/api/_ship/push", async (c) => {
6037
+ try {
6038
+ return c.json(await pushContent(root2));
6039
+ } catch (e) {
6040
+ return c.json({ error: e.message }, 400);
6041
+ }
6042
+ });
6043
+ app.post("/api/_ship/repo", async (c) => {
6044
+ const { name, private: isPrivate } = await c.req.json();
6045
+ if (!name?.trim()) return c.json({ error: "A repository name is required." }, 400);
6046
+ try {
6047
+ return c.json(
6048
+ await createGitHubRepo(root2, { name: name.trim(), private: isPrivate !== false })
6049
+ );
6050
+ } catch (e) {
6051
+ return c.json({ error: e.message }, 400);
6052
+ }
6053
+ });
5672
6054
  app.get("/api/_templates", (c) => c.json({ items: templateList() }));
5673
6055
  app.post("/api/_init", async (c) => {
5674
6056
  const { template: id } = await c.req.json();
@@ -5752,9 +6134,9 @@ async function createServer(root2) {
5752
6134
  const base = slugify((body.filename ?? "gorsel").replace(/\.[^.]+$/, "")) || "gorsel";
5753
6135
  const name = `${base}-${Date.now().toString(36)}.webp`;
5754
6136
  const rel = `${contentDir}/media/${name}`;
5755
- const abs = join4(root2, rel);
5756
- await mkdir2(dirname2(abs), { recursive: true });
5757
- await writeFile3(abs, Buffer.from(body.dataBase64, "base64"));
6137
+ const abs = join6(root2, rel);
6138
+ await mkdir3(dirname3(abs), { recursive: true });
6139
+ await writeFile4(abs, Buffer.from(body.dataBase64, "base64"));
5758
6140
  return c.json({ path: rel });
5759
6141
  });
5760
6142
  app.post("/api/_ai/models", async (c) => {
@@ -5796,7 +6178,7 @@ async function createServer(root2) {
5796
6178
  return c.text("forbidden", 400);
5797
6179
  }
5798
6180
  try {
5799
- const bytes = await readFile2(join4(root2, contentDir, "media", file));
6181
+ const bytes = await readFile3(join6(root2, contentDir, "media", file));
5800
6182
  return new Response(bytes, { headers: { "content-type": "image/webp" } });
5801
6183
  } catch {
5802
6184
  return c.text("not found", 404);
@@ -5823,14 +6205,14 @@ async function createServer(root2) {
5823
6205
  app.get("/*", async (c) => {
5824
6206
  const urlPath = c.req.path === "/" ? "/index.html" : c.req.path;
5825
6207
  const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
5826
- const file = join4(editorDir, rel);
6208
+ const file = join6(editorDir, rel);
5827
6209
  if (!file.startsWith(editorDir)) return c.text("forbidden", 403);
5828
6210
  const type = MIME[extname(file)] ?? "application/octet-stream";
5829
6211
  try {
5830
- return new Response(await readFile2(file), { headers: { "content-type": type } });
6212
+ return new Response(await readFile3(file), { headers: { "content-type": type } });
5831
6213
  } catch {
5832
6214
  try {
5833
- const html = await readFile2(join4(editorDir, "index.html"));
6215
+ const html = await readFile3(join6(editorDir, "index.html"));
5834
6216
  return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
5835
6217
  } catch {
5836
6218
  return c.text("Editor UI not found (run the justjson build first).", 404);
@@ -5863,9 +6245,16 @@ function readVersion() {
5863
6245
  var program = new Command();
5864
6246
  var root = process.cwd();
5865
6247
  program.name("justjson").description("A tiny local-first CMS that keeps your content as plain JSON").version(readVersion());
5866
- program.command("init").description("Scaffold a project from a template").argument("[template]", `template name (${listTemplates().join(", ")})`, "blog").action(async (template) => {
6248
+ program.command("init").description("Scaffold a project from a template").argument("[template]", `template name (${listTemplates().join(", ")})`, "blog").option("--astro", "also generate a working Astro site around the content").action(async (template, opts) => {
5867
6249
  await initProject(root, template);
5868
6250
  console.log(`Scaffolded from the '${template}' template.`);
6251
+ if (!opts.astro) return;
6252
+ const schema = await loadSchema(new FsAdapter(root), await resolveContentDir(root));
6253
+ if (!schema) return;
6254
+ const { written, skipped } = await writeAstroSite(root, schema, basename3(root) || "my-site");
6255
+ console.log(`Wrote ${written.length} site file(s).`);
6256
+ if (skipped.length > 0) console.log(`Kept your existing: ${skipped.join(", ")}`);
6257
+ console.log("\nNext:\n npm install\n npm run dev");
5869
6258
  });
5870
6259
  program.command("types").description("Generate types.ts and a typed content.ts loader from your schema").action(async () => {
5871
6260
  const out = await generateTypesFile(root);