@kdrgny/justjson 1.2.0 → 1.6.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/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 = {};
@@ -4054,6 +4050,20 @@ var coerce = {
4054
4050
  var NEVER = INVALID;
4055
4051
 
4056
4052
  // ../core/dist/index.js
4053
+ var JustJsonError = class extends Error {
4054
+ constructor(message) {
4055
+ super(message);
4056
+ this.name = new.target.name;
4057
+ }
4058
+ };
4059
+ var NotFoundError = class extends JustJsonError {
4060
+ };
4061
+ var UnsafeSlugError = class extends JustJsonError {
4062
+ };
4063
+ var PathEscapeError = class extends JustJsonError {
4064
+ };
4065
+ var SchemaError = class extends JustJsonError {
4066
+ };
4057
4067
  var fieldTypes = [
4058
4068
  "text",
4059
4069
  "richtext",
@@ -4062,23 +4072,34 @@ var fieldTypes = [
4062
4072
  "date",
4063
4073
  "select",
4064
4074
  "relation",
4065
- "image"
4075
+ "image",
4076
+ "url",
4077
+ "email",
4078
+ "list",
4079
+ "color",
4080
+ "group"
4066
4081
  ];
4067
- var zField = external_exports.object({
4068
- key: external_exports.string().min(1),
4069
- label: external_exports.string().optional(),
4070
- type: external_exports.enum(fieldTypes),
4071
- required: external_exports.boolean().optional(),
4072
- options: external_exports.array(external_exports.string()).optional(),
4073
- to: external_exports.string().optional()
4074
- }).superRefine((field, ctx) => {
4075
- if (field.type === "select" && (!field.options || field.options.length === 0)) {
4076
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: "select alan\u0131 options gerektirir" });
4077
- }
4078
- if (field.type === "relation" && !field.to) {
4079
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: 'relation alan\u0131 "to" gerektirir' });
4080
- }
4081
- });
4082
+ var zField = external_exports.lazy(
4083
+ () => external_exports.object({
4084
+ key: external_exports.string().min(1),
4085
+ label: external_exports.string().optional(),
4086
+ type: external_exports.enum(fieldTypes),
4087
+ required: external_exports.boolean().optional(),
4088
+ options: external_exports.array(external_exports.string()).optional(),
4089
+ to: external_exports.string().optional(),
4090
+ fields: external_exports.array(zField).optional()
4091
+ }).superRefine((field, ctx) => {
4092
+ if (field.type === "select" && (!field.options || field.options.length === 0)) {
4093
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: "a select field requires options" });
4094
+ }
4095
+ if (field.type === "relation" && !field.to) {
4096
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: 'a relation field requires "to"' });
4097
+ }
4098
+ if (field.type === "group" && (!field.fields || field.fields.length === 0)) {
4099
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: "a group field requires fields" });
4100
+ }
4101
+ })
4102
+ );
4082
4103
  var zCollection = external_exports.object({
4083
4104
  name: external_exports.string().min(1),
4084
4105
  label: external_exports.string().optional(),
@@ -4102,30 +4123,38 @@ var zSchema = external_exports.object({
4102
4123
  const containers = [...schema.collections, ...schema.singletons];
4103
4124
  for (const c of containers) {
4104
4125
  if (names.has(c.name)) {
4105
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `tekrar eden ad: ${c.name}` });
4126
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `duplicate name: ${c.name}` });
4106
4127
  }
4107
4128
  names.add(c.name);
4108
4129
  if (c.path.includes("..") || c.path.startsWith("/") || c.path.includes("\\")) {
4109
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `g\xFCvensiz path: ${c.path}` });
4130
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `unsafe path: ${c.path}` });
4110
4131
  }
4111
4132
  if (paths.has(c.path)) {
4112
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `tekrar eden path: ${c.path}` });
4133
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `duplicate path: ${c.path}` });
4113
4134
  }
4114
4135
  paths.add(c.path);
4115
4136
  const keys = /* @__PURE__ */ new Set();
4116
4137
  for (const f of c.fields) {
4117
4138
  if (keys.has(f.key)) {
4118
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `tekrar eden field key: ${f.key}` });
4139
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `duplicate field key: ${f.key}` });
4119
4140
  }
4120
4141
  keys.add(f.key);
4121
4142
  if (f.type === "relation" && f.to && !collectionNames.has(f.to)) {
4122
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `relation hedefi yok: ${f.to}` });
4143
+ ctx.addIssue({
4144
+ code: external_exports.ZodIssueCode.custom,
4145
+ message: `relation target does not exist: ${f.to}`
4146
+ });
4123
4147
  }
4124
4148
  }
4125
4149
  }
4126
4150
  });
4127
4151
  function parseSchema(input) {
4128
- return zSchema.parse(input);
4152
+ const result = zSchema.safeParse(input);
4153
+ if (result.success) return result.data;
4154
+ const lines = result.error.issues.map(
4155
+ (i) => i.path.length ? `${i.path.join(".")}: ${i.message}` : i.message
4156
+ );
4157
+ throw new SchemaError(lines.join("\n"));
4129
4158
  }
4130
4159
  function serializeSchema(schema) {
4131
4160
  return `${JSON.stringify(schema, null, 2)}
@@ -4149,7 +4178,7 @@ var TR = {
4149
4178
  function slugify(input) {
4150
4179
  const mapped = input.replace(/[çğıöşüÇĞİÖŞÜI]/g, (c) => TR[c] ?? c);
4151
4180
  const slug = mapped.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4152
- return slug || "icerik";
4181
+ return slug || "content";
4153
4182
  }
4154
4183
  var TITLE_KEYS = ["title", "name", "baslik", "ba\u015Fl\u0131k", "ad", "label", "heading", "isim"];
4155
4184
  function entryTitle(fields, data) {
@@ -4171,7 +4200,7 @@ function schemaPath(contentDir) {
4171
4200
  }
4172
4201
  function assertSafeSlug(slug) {
4173
4202
  if (slug.length === 0 || slug.includes("/") || slug.includes("\\") || slug.includes("..")) {
4174
- throw new Error(`G\xFCvensiz slug: ${slug}`);
4203
+ throw new UnsafeSlugError(`Unsafe slug: ${slug}`);
4175
4204
  }
4176
4205
  }
4177
4206
  async function loadSchema(adapter, contentDir = "content") {
@@ -4193,12 +4222,12 @@ var ContentStore = class {
4193
4222
  contentDir;
4194
4223
  collection(name) {
4195
4224
  const col = this.schema.collections.find((c) => c.name === name);
4196
- if (!col) throw new Error(`Bilinmeyen koleksiyon: ${name}`);
4225
+ if (!col) throw new NotFoundError(`Bilinmeyen koleksiyon: ${name}`);
4197
4226
  return col;
4198
4227
  }
4199
4228
  singleton(name) {
4200
4229
  const s = this.schema.singletons.find((x) => x.name === name);
4201
- if (!s) throw new Error(`Bilinmeyen singleton: ${name}`);
4230
+ if (!s) throw new NotFoundError(`Bilinmeyen singleton: ${name}`);
4202
4231
  return s;
4203
4232
  }
4204
4233
  entryPath(col, slug) {
@@ -4251,6 +4280,206 @@ var ContentStore = class {
4251
4280
  `);
4252
4281
  }
4253
4282
  };
4283
+ function isEmpty(value) {
4284
+ return value === void 0 || value === null || value === "";
4285
+ }
4286
+ var URL_RE = /^[a-z][a-z0-9+.-]*:\/\/.+/i;
4287
+ var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
4288
+ var COLOR_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
4289
+ function typeError(field, value) {
4290
+ switch (field.type) {
4291
+ case "text":
4292
+ case "richtext":
4293
+ case "date":
4294
+ case "image":
4295
+ return typeof value === "string" ? null : "expected text";
4296
+ case "url":
4297
+ if (typeof value !== "string") return "expected text";
4298
+ return URL_RE.test(value) ? null : "expected a valid URL";
4299
+ case "email":
4300
+ if (typeof value !== "string") return "expected text";
4301
+ return EMAIL_RE.test(value) ? null : "expected a valid email address";
4302
+ case "color":
4303
+ if (typeof value !== "string") return "expected text";
4304
+ return COLOR_RE.test(value) ? null : "expected a hex color (e.g. #ff0000)";
4305
+ case "list":
4306
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
4307
+ return "expected a list of text";
4308
+ }
4309
+ return null;
4310
+ case "relation":
4311
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
4312
+ return "expected a list of slugs";
4313
+ }
4314
+ return null;
4315
+ case "group":
4316
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4317
+ return "expected an object";
4318
+ }
4319
+ return null;
4320
+ case "number":
4321
+ return typeof value === "number" ? null : "expected a number";
4322
+ case "boolean":
4323
+ return typeof value === "boolean" ? null : "expected true or false";
4324
+ case "select":
4325
+ if (typeof value !== "string") return "expected text";
4326
+ return field.options?.includes(value) ? null : "not one of the options";
4327
+ }
4328
+ }
4329
+ function validateEntry(fields, data) {
4330
+ const issues = [];
4331
+ const known = new Set(fields.map((f) => f.key));
4332
+ for (const field of fields) {
4333
+ const value = data[field.key];
4334
+ if (isEmpty(value)) {
4335
+ if (field.required) {
4336
+ issues.push({
4337
+ key: field.key,
4338
+ level: "warning",
4339
+ kind: "required",
4340
+ message: "required field is empty"
4341
+ });
4342
+ }
4343
+ continue;
4344
+ }
4345
+ const err = typeError(field, value);
4346
+ if (err) issues.push({ key: field.key, level: "error", kind: "type", message: err });
4347
+ }
4348
+ for (const key of Object.keys(data)) {
4349
+ if (key.startsWith("_")) continue;
4350
+ if (!known.has(key)) {
4351
+ issues.push({
4352
+ key,
4353
+ level: "warning",
4354
+ kind: "unknown-key",
4355
+ message: "key is not in the schema"
4356
+ });
4357
+ }
4358
+ }
4359
+ return { ok: issues.every((i) => i.level !== "error"), issues };
4360
+ }
4361
+ function basename(path) {
4362
+ return path.split(/[/\\]/).pop() ?? path;
4363
+ }
4364
+ function imageFields(fields) {
4365
+ return fields.filter((f) => f.type === "image");
4366
+ }
4367
+ function relationFields(fields) {
4368
+ return fields.filter((f) => f.type === "relation");
4369
+ }
4370
+ function validateProject(schema, content) {
4371
+ const issues = [];
4372
+ const mediaNames = content.media ? new Set(content.media.map(basename)) : null;
4373
+ const schemaCollectionNames = new Set(schema.collections.map((c) => c.name));
4374
+ for (const name of Object.keys(content.collections)) {
4375
+ if (!schemaCollectionNames.has(name)) {
4376
+ issues.push({
4377
+ level: "warning",
4378
+ kind: "schema-content-mismatch",
4379
+ collection: name,
4380
+ message: `Collection folder '${name}' is not in the schema`
4381
+ });
4382
+ }
4383
+ }
4384
+ const slugsByCollection = {};
4385
+ for (const [name, entries] of Object.entries(content.collections)) {
4386
+ slugsByCollection[name] = new Set(entries.map((e) => e.slug));
4387
+ }
4388
+ for (const col of schema.collections) {
4389
+ const entries = content.collections[col.name] ?? [];
4390
+ const seen = /* @__PURE__ */ new Set();
4391
+ const reported = /* @__PURE__ */ new Set();
4392
+ for (const { slug } of entries) {
4393
+ if (seen.has(slug) && !reported.has(slug)) {
4394
+ reported.add(slug);
4395
+ issues.push({
4396
+ level: "error",
4397
+ kind: "duplicate-slug",
4398
+ collection: col.name,
4399
+ slug,
4400
+ message: `Duplicate slug in '${col.name}': ${slug}`
4401
+ });
4402
+ }
4403
+ seen.add(slug);
4404
+ }
4405
+ const rels = relationFields(col.fields);
4406
+ const imgs = imageFields(col.fields);
4407
+ for (const { slug, data } of entries) {
4408
+ for (const issue of validateEntry(col.fields, data).issues) {
4409
+ issues.push({
4410
+ level: issue.level,
4411
+ kind: issue.kind,
4412
+ collection: col.name,
4413
+ slug,
4414
+ field: issue.key,
4415
+ message: issue.message
4416
+ });
4417
+ }
4418
+ for (const field of rels) {
4419
+ const value = data[field.key];
4420
+ if (!Array.isArray(value)) continue;
4421
+ const target = field.to ? slugsByCollection[field.to] : void 0;
4422
+ for (const ref of value) {
4423
+ if (typeof ref !== "string") continue;
4424
+ if (!target || !target.has(ref)) {
4425
+ issues.push({
4426
+ level: "error",
4427
+ kind: "broken-relation",
4428
+ collection: col.name,
4429
+ slug,
4430
+ field: field.key,
4431
+ message: field.to ? `Broken relation: '${ref}' does not exist in '${field.to}'` : `Relation field '${field.key}' has no target collection`
4432
+ });
4433
+ }
4434
+ }
4435
+ }
4436
+ if (mediaNames) {
4437
+ for (const field of imgs) {
4438
+ const value = data[field.key];
4439
+ if (typeof value !== "string" || value === "") continue;
4440
+ if (!mediaNames.has(basename(value))) {
4441
+ issues.push({
4442
+ level: "error",
4443
+ kind: "missing-media",
4444
+ collection: col.name,
4445
+ slug,
4446
+ field: field.key,
4447
+ message: `Missing media: ${value}`
4448
+ });
4449
+ }
4450
+ }
4451
+ }
4452
+ }
4453
+ }
4454
+ for (const s of schema.singletons) {
4455
+ const data = content.singletons[s.name] ?? {};
4456
+ for (const issue of validateEntry(s.fields, data).issues) {
4457
+ issues.push({
4458
+ level: issue.level,
4459
+ kind: issue.kind,
4460
+ singleton: s.name,
4461
+ field: issue.key,
4462
+ message: issue.message
4463
+ });
4464
+ }
4465
+ if (mediaNames) {
4466
+ for (const field of imageFields(s.fields)) {
4467
+ const value = data[field.key];
4468
+ if (typeof value !== "string" || value === "") continue;
4469
+ if (!mediaNames.has(basename(value))) {
4470
+ issues.push({
4471
+ level: "error",
4472
+ kind: "missing-media",
4473
+ singleton: s.name,
4474
+ field: field.key,
4475
+ message: `Missing media: ${value}`
4476
+ });
4477
+ }
4478
+ }
4479
+ }
4480
+ }
4481
+ return issues;
4482
+ }
4254
4483
  function pascalCase(name) {
4255
4484
  return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
4256
4485
  }
@@ -4263,7 +4492,14 @@ function fieldTsType(field) {
4263
4492
  case "select":
4264
4493
  return (field.options ?? []).map((o) => `'${o}'`).join(" | ") || "string";
4265
4494
  case "relation":
4495
+ case "list":
4266
4496
  return "string[]";
4497
+ case "group": {
4498
+ const subs = field.fields ?? [];
4499
+ if (subs.length === 0) return "Record<string, unknown>";
4500
+ const inner = subs.map((f) => `${f.key}${f.required ? "" : "?"}: ${fieldTsType(f)}`).join("; ");
4501
+ return `{ ${inner} }`;
4502
+ }
4267
4503
  default:
4268
4504
  return "string";
4269
4505
  }
@@ -4278,7 +4514,7 @@ ${lines.join("\n")}
4278
4514
  }`;
4279
4515
  }
4280
4516
  function generateTypes(schema) {
4281
- const blocks = ["// JustJSON taraf\u0131ndan \xFCretildi \u2014 elle d\xFCzenlemeyin."];
4517
+ const blocks = ["// Generated by JustJSON \u2014 do not edit by hand."];
4282
4518
  for (const col of schema.collections) {
4283
4519
  const name = pascalCase(col.name);
4284
4520
  blocks.push(emitInterface(name, col.fields));
@@ -4290,6 +4526,71 @@ function generateTypes(schema) {
4290
4526
  return `${blocks.join("\n\n")}
4291
4527
  `;
4292
4528
  }
4529
+ function pascalCase2(name) {
4530
+ return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
4531
+ }
4532
+ var IMPORTS = `import { readFile, readdir } from 'node:fs/promises'
4533
+ import { dirname, join } from 'node:path'
4534
+ import { fileURLToPath } from 'node:url'`;
4535
+ var READERS = `export type WithSlug<T> = T & { slug: string }
4536
+
4537
+ export interface LoadOptions {
4538
+ /** true ise _status: 'draft' olan kay\u0131tlar da dahil edilir (varsay\u0131lan: yaln\u0131zca yay\u0131nlananlar). */
4539
+ drafts?: boolean
4540
+ }
4541
+
4542
+ async function readCollection<T>(dir: string, opts?: LoadOptions): Promise<WithSlug<T>[]> {
4543
+ let files: string[]
4544
+ try {
4545
+ files = (await readdir(join(contentDir, dir))).filter((f) => f.endsWith('.json'))
4546
+ } catch {
4547
+ return []
4548
+ }
4549
+ const out: WithSlug<T>[] = []
4550
+ for (const file of files.sort()) {
4551
+ const raw = await readFile(join(contentDir, dir, file), 'utf8')
4552
+ const data = JSON.parse(raw) as T & { _status?: string }
4553
+ if (!opts?.drafts && data._status === 'draft') continue
4554
+ out.push({ ...data, slug: file.slice(0, -'.json'.length) })
4555
+ }
4556
+ return out
4557
+ }
4558
+
4559
+ async function readSingleton<T>(file: string): Promise<T | null> {
4560
+ try {
4561
+ return JSON.parse(await readFile(join(contentDir, file), 'utf8')) as T
4562
+ } catch {
4563
+ return null
4564
+ }
4565
+ }`;
4566
+ function generateLoader(schema, contentDir) {
4567
+ const typeNames = [
4568
+ ...schema.collections.map((c) => pascalCase2(c.name)),
4569
+ ...schema.singletons.map((s) => pascalCase2(s.name))
4570
+ ];
4571
+ const header = [
4572
+ "// Generated by JustJSON \u2014 do not edit by hand.",
4573
+ IMPORTS,
4574
+ `import type { ${typeNames.join(", ")} } from './types'`
4575
+ ].join("\n");
4576
+ const q = (s) => `'${s.replace(/'/g, "\\'")}'`;
4577
+ const contentDirLine = `const contentDir = join(dirname(fileURLToPath(import.meta.url)), ${q(contentDir)})`;
4578
+ const loaders = [];
4579
+ for (const col of schema.collections) {
4580
+ const name = pascalCase2(col.name);
4581
+ loaders.push(
4582
+ `export const load${name} = (opts?: LoadOptions): Promise<WithSlug<${name}>[]> => readCollection<${name}>(${q(col.path)}, opts)`
4583
+ );
4584
+ }
4585
+ for (const s of schema.singletons) {
4586
+ const name = pascalCase2(s.name);
4587
+ loaders.push(
4588
+ `export const load${name} = (): Promise<${name} | null> => readSingleton<${name}>(${q(s.path)})`
4589
+ );
4590
+ }
4591
+ return `${[header, contentDirLine, READERS, loaders.join("\n")].join("\n\n")}
4592
+ `;
4593
+ }
4293
4594
  function json(data) {
4294
4595
  return `${JSON.stringify(data, null, 2)}
4295
4596
  `;
@@ -4342,7 +4643,7 @@ function fieldsFromRows(rows) {
4342
4643
  return order.map((key) => ({ key, label: key, type: types.get(key) }));
4343
4644
  }
4344
4645
  function inferProject(data) {
4345
- if (!isPlainObject(data)) throw new Error("\u0130\xE7e aktar\u0131lan JSON bir nesne olmal\u0131.");
4646
+ if (!isPlainObject(data)) throw new Error("The imported JSON must be an object.");
4346
4647
  const collections = [];
4347
4648
  const singletons = [];
4348
4649
  const entries = {};
@@ -4351,7 +4652,7 @@ function inferProject(data) {
4351
4652
  const generalData = {};
4352
4653
  const usedNames = /* @__PURE__ */ new Set();
4353
4654
  const uniqueName = (base) => {
4354
- let name = base || "alan";
4655
+ let name = base || "field";
4355
4656
  let n = 2;
4356
4657
  while (usedNames.has(name)) name = `${base}-${n++}`;
4357
4658
  usedNames.add(name);
@@ -4400,14 +4701,19 @@ function inferProject(data) {
4400
4701
  }
4401
4702
  }
4402
4703
  if (generalFields.length > 0) {
4403
- const name = uniqueName("genel");
4404
- singletons.push({ name, label: "Genel", path: `${name}.json`, fields: generalFields });
4704
+ const name = uniqueName("general");
4705
+ singletons.push({ name, label: "General", path: `${name}.json`, fields: generalFields });
4405
4706
  singletonData[name] = generalData;
4406
4707
  }
4407
4708
  return { schema: { version: 1, collections, singletons }, entries, singletons: singletonData };
4408
4709
  }
4409
4710
 
4711
+ // src/cli.ts
4712
+ import { Command } from "commander";
4713
+
4410
4714
  // src/commands/export.ts
4715
+ import { writeFile as writeFile2 } from "fs/promises";
4716
+ import { join } from "path";
4411
4717
  import { zipSync } from "fflate";
4412
4718
 
4413
4719
  // src/fs-adapter.ts
@@ -4422,7 +4728,7 @@ var FsAdapter = class {
4422
4728
  const rootAbs = resolve(this.root);
4423
4729
  const full = resolve(rootAbs, path);
4424
4730
  if (full !== rootAbs && !full.startsWith(rootAbs + sep)) {
4425
- throw new Error(`Yol k\xF6k d\u0131\u015F\u0131na \xE7\u0131k\u0131yor: ${path}`);
4731
+ throw new PathEscapeError(`Path escapes the project root: ${path}`);
4426
4732
  }
4427
4733
  return full;
4428
4734
  }
@@ -4481,7 +4787,7 @@ async function resolveContentDir(root2) {
4481
4787
  // src/commands/export.ts
4482
4788
  async function collectExportZip(adapter, contentDir) {
4483
4789
  const schema = await loadSchema(adapter, contentDir);
4484
- if (!schema) throw new Error("\u015Eema bulunamad\u0131. \xD6nce `justjson init` \xE7al\u0131\u015Ft\u0131r\u0131n.");
4790
+ if (!schema) throw new Error("No schema found. Run `justjson init` first.");
4485
4791
  const store = new ContentStore(adapter, schema, contentDir);
4486
4792
  const entries = {};
4487
4793
  for (const col of schema.collections) {
@@ -4518,109 +4824,194 @@ async function exportZip(root2, outFile = "justjson-export.zip") {
4518
4824
  // src/templates/blog.json
4519
4825
  var blog_default = {
4520
4826
  title: "Blog",
4521
- description: "Rich-text i\xE7erikli yaz\u0131lar ve site ad\u0131 i\xE7in bir Ayarlar.",
4827
+ description: "Posts with rich-text content, plus a Settings singleton for the site name.",
4522
4828
  schema: {
4523
4829
  version: 1,
4524
4830
  collections: [
4525
4831
  {
4526
4832
  name: "posts",
4527
- label: "Yaz\u0131lar",
4833
+ label: "Posts",
4528
4834
  path: "posts",
4529
4835
  fields: [
4530
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4531
- { key: "slug", label: "Slug", type: "text", required: true },
4532
- { key: "date", label: "Tarih", type: "date" },
4533
- { key: "body", label: "\u0130\xE7erik", type: "richtext" }
4836
+ {
4837
+ key: "title",
4838
+ label: "Title",
4839
+ type: "text",
4840
+ required: true
4841
+ },
4842
+ {
4843
+ key: "slug",
4844
+ label: "Slug",
4845
+ type: "text",
4846
+ required: true
4847
+ },
4848
+ {
4849
+ key: "date",
4850
+ label: "Date",
4851
+ type: "date"
4852
+ },
4853
+ {
4854
+ key: "body",
4855
+ label: "Body",
4856
+ type: "richtext"
4857
+ }
4534
4858
  ]
4535
4859
  }
4536
4860
  ],
4537
4861
  singletons: [
4538
4862
  {
4539
4863
  name: "settings",
4540
- label: "Ayarlar",
4864
+ label: "Settings",
4541
4865
  path: "settings.json",
4542
- fields: [{ key: "title", label: "Site ad\u0131", type: "text" }]
4866
+ fields: [
4867
+ {
4868
+ key: "title",
4869
+ label: "Site name",
4870
+ type: "text"
4871
+ }
4872
+ ]
4543
4873
  }
4544
4874
  ]
4545
4875
  },
4546
4876
  samples: {
4547
- posts: [{ title: "\u0130lk yaz\u0131", slug: "ilk-yazi", body: "Merhaba." }]
4877
+ posts: [
4878
+ {
4879
+ title: "First post",
4880
+ slug: "first-post",
4881
+ body: "Hello."
4882
+ }
4883
+ ],
4884
+ settings: {
4885
+ title: "My blog"
4886
+ }
4548
4887
  }
4549
4888
  };
4550
4889
 
4551
4890
  // src/templates/catalog.json
4552
4891
  var catalog_default = {
4553
- title: "\xDCr\xFCn Katalo\u011Fu",
4554
- description: "Fiyat, kategori ve g\xF6rselli \xFCr\xFCnler; bir de Ma\u011Faza ayarlar\u0131.",
4892
+ title: "Product catalog",
4893
+ description: "Products with price, category and an image, plus Store settings.",
4555
4894
  schema: {
4556
4895
  version: 1,
4557
4896
  collections: [
4558
4897
  {
4559
- name: "urunler",
4560
- label: "\xDCr\xFCnler",
4561
- path: "urunler",
4898
+ name: "products",
4899
+ label: "Products",
4900
+ path: "products",
4562
4901
  fields: [
4563
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4564
- { key: "slug", label: "Slug", type: "text", required: true },
4565
- { key: "fiyat", label: "Fiyat", type: "number" },
4566
4902
  {
4567
- key: "kategori",
4568
- label: "Kategori",
4903
+ key: "title",
4904
+ label: "Title",
4905
+ type: "text",
4906
+ required: true
4907
+ },
4908
+ {
4909
+ key: "slug",
4910
+ label: "Slug",
4911
+ type: "text",
4912
+ required: true
4913
+ },
4914
+ {
4915
+ key: "price",
4916
+ label: "Price",
4917
+ type: "number"
4918
+ },
4919
+ {
4920
+ key: "category",
4921
+ label: "Category",
4569
4922
  type: "select",
4570
- options: ["Ev", "Giyim", "Aksesuar"]
4923
+ options: ["Home", "Clothing", "Accessories"]
4924
+ },
4925
+ {
4926
+ key: "cover",
4927
+ label: "Image",
4928
+ type: "image"
4571
4929
  },
4572
- { key: "kapak", label: "G\xF6rsel", type: "image" },
4573
- { key: "aciklama", label: "A\xE7\u0131klama", type: "richtext" }
4930
+ {
4931
+ key: "description",
4932
+ label: "Description",
4933
+ type: "richtext"
4934
+ }
4574
4935
  ]
4575
4936
  }
4576
4937
  ],
4577
4938
  singletons: [
4578
4939
  {
4579
- name: "magaza",
4580
- label: "Ma\u011Faza",
4581
- path: "magaza.json",
4940
+ name: "store",
4941
+ label: "Store",
4942
+ path: "store.json",
4582
4943
  fields: [
4583
- { key: "title", label: "Ma\u011Faza ad\u0131", type: "text" },
4584
- { key: "slogan", label: "Slogan", type: "text" }
4944
+ {
4945
+ key: "title",
4946
+ label: "Store name",
4947
+ type: "text"
4948
+ },
4949
+ {
4950
+ key: "tagline",
4951
+ label: "Tagline",
4952
+ type: "text"
4953
+ }
4585
4954
  ]
4586
4955
  }
4587
4956
  ]
4588
4957
  },
4589
4958
  samples: {
4590
- urunler: [
4959
+ products: [
4591
4960
  {
4592
- title: "Seramik Kupa",
4593
- slug: "seramik-kupa",
4594
- fiyat: 180,
4595
- kategori: "Ev",
4596
- aciklama: "Elde yap\u0131m seramik kupa."
4961
+ title: "Ceramic mug",
4962
+ slug: "ceramic-mug",
4963
+ price: 18,
4964
+ category: "Home",
4965
+ description: "Hand-made ceramic mug."
4597
4966
  }
4598
- ]
4967
+ ],
4968
+ store: {
4969
+ title: "My store",
4970
+ tagline: "Small batch, made well"
4971
+ }
4599
4972
  }
4600
4973
  };
4601
4974
 
4602
4975
  // src/templates/changelog.json
4603
4976
  var changelog_default = {
4604
4977
  title: "Changelog",
4605
- description: "S\xFCr\xFCm notlar\u0131: versiyon, tarih, t\xFCr ve rich-text a\xE7\u0131klama.",
4978
+ description: "Release notes: version, date, type and a rich-text body.",
4606
4979
  schema: {
4607
4980
  version: 1,
4608
4981
  collections: [
4609
4982
  {
4610
4983
  name: "releases",
4611
- label: "S\xFCr\xFCmler",
4984
+ label: "Releases",
4612
4985
  path: "releases",
4613
4986
  fields: [
4614
- { key: "version", label: "Versiyon", type: "text", required: true },
4615
- { key: "slug", label: "Slug", type: "text", required: true },
4616
- { key: "date", label: "Tarih", type: "date" },
4987
+ {
4988
+ key: "version",
4989
+ label: "Version",
4990
+ type: "text",
4991
+ required: true
4992
+ },
4993
+ {
4994
+ key: "slug",
4995
+ label: "Slug",
4996
+ type: "text",
4997
+ required: true
4998
+ },
4999
+ {
5000
+ key: "date",
5001
+ label: "Date",
5002
+ type: "date"
5003
+ },
4617
5004
  {
4618
5005
  key: "type",
4619
- label: "T\xFCr",
5006
+ label: "Type",
4620
5007
  type: "select",
4621
- options: ["Eklendi", "De\u011Fi\u015Fti", "D\xFCzeltildi"]
5008
+ options: ["Added", "Changed", "Fixed"]
4622
5009
  },
4623
- { key: "body", label: "Notlar", type: "richtext" }
5010
+ {
5011
+ key: "body",
5012
+ label: "Notes",
5013
+ type: "richtext"
5014
+ }
4624
5015
  ]
4625
5016
  }
4626
5017
  ],
@@ -4631,8 +5022,8 @@ var changelog_default = {
4631
5022
  {
4632
5023
  version: "1.0.0",
4633
5024
  slug: "1-0-0",
4634
- type: "Eklendi",
4635
- body: "\u0130lk s\xFCr\xFCm."
5025
+ type: "Added",
5026
+ body: "First release."
4636
5027
  }
4637
5028
  ]
4638
5029
  }
@@ -4641,161 +5032,314 @@ var changelog_default = {
4641
5032
  // src/templates/cv.json
4642
5033
  var cv_default = {
4643
5034
  title: "CV",
4644
- description: "Deneyim koleksiyonu ve ki\u015Fisel bilgiler i\xE7in bir Profil.",
5035
+ description: "An Experience collection and a Profile singleton for your personal details.",
4645
5036
  schema: {
4646
5037
  version: 1,
4647
5038
  collections: [
4648
5039
  {
4649
5040
  name: "experience",
4650
- label: "Deneyim",
5041
+ label: "Experience",
4651
5042
  path: "experience",
4652
5043
  fields: [
4653
- { key: "role", label: "Pozisyon", type: "text", required: true },
4654
- { key: "company", label: "\u015Eirket", type: "text", required: true },
4655
- { key: "summary", label: "\xD6zet", type: "richtext" }
5044
+ {
5045
+ key: "role",
5046
+ label: "Role",
5047
+ type: "text",
5048
+ required: true
5049
+ },
5050
+ {
5051
+ key: "company",
5052
+ label: "Company",
5053
+ type: "text",
5054
+ required: true
5055
+ },
5056
+ {
5057
+ key: "summary",
5058
+ label: "Summary",
5059
+ type: "richtext"
5060
+ }
4656
5061
  ]
4657
5062
  }
4658
5063
  ],
4659
5064
  singletons: [
4660
5065
  {
4661
5066
  name: "profile",
4662
- label: "Profil",
5067
+ label: "Profile",
4663
5068
  path: "profile.json",
4664
5069
  fields: [
4665
- { key: "name", label: "Ad", type: "text", required: true },
4666
- { key: "headline", label: "Ba\u015Fl\u0131k", type: "text" }
5070
+ {
5071
+ key: "name",
5072
+ label: "Name",
5073
+ type: "text",
5074
+ required: true
5075
+ },
5076
+ {
5077
+ key: "headline",
5078
+ label: "Headline",
5079
+ type: "text"
5080
+ }
4667
5081
  ]
4668
5082
  }
4669
5083
  ]
4670
5084
  },
4671
5085
  samples: {
4672
- experience: [{ role: "Yaz\u0131l\u0131mc\u0131", company: "\xD6rnek A.\u015E.", summary: "..." }]
5086
+ experience: [
5087
+ {
5088
+ role: "Developer",
5089
+ company: "Acme Inc.",
5090
+ summary: "..."
5091
+ }
5092
+ ],
5093
+ profile: {
5094
+ name: "Ada Lovelace",
5095
+ headline: "Software engineer"
5096
+ }
4673
5097
  }
4674
5098
  };
4675
5099
 
4676
5100
  // src/templates/docs.json
4677
5101
  var docs_default = {
4678
- title: "Dok\xFCmantasyon",
4679
- description: "S\u0131ralanabilir sayfalar ve rich-text i\xE7erik; site ba\u015Fl\u0131\u011F\u0131 i\xE7in bir Ayarlar.",
5102
+ title: "Documentation",
5103
+ description: "Orderable pages with rich-text content, plus a Settings singleton for the site title.",
4680
5104
  schema: {
4681
5105
  version: 1,
4682
5106
  collections: [
4683
5107
  {
4684
5108
  name: "pages",
4685
- label: "Sayfalar",
5109
+ label: "Pages",
4686
5110
  path: "pages",
4687
5111
  fields: [
4688
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4689
- { key: "slug", label: "Slug", type: "text", required: true },
4690
- { key: "order", label: "S\u0131ra", type: "number" },
4691
- { key: "body", label: "\u0130\xE7erik", type: "richtext" }
5112
+ {
5113
+ key: "title",
5114
+ label: "Title",
5115
+ type: "text",
5116
+ required: true
5117
+ },
5118
+ {
5119
+ key: "slug",
5120
+ label: "Slug",
5121
+ type: "text",
5122
+ required: true
5123
+ },
5124
+ {
5125
+ key: "order",
5126
+ label: "Order",
5127
+ type: "number"
5128
+ },
5129
+ {
5130
+ key: "body",
5131
+ label: "Body",
5132
+ type: "richtext"
5133
+ }
4692
5134
  ]
4693
5135
  }
4694
5136
  ],
4695
5137
  singletons: [
4696
5138
  {
4697
5139
  name: "settings",
4698
- label: "Ayarlar",
5140
+ label: "Settings",
4699
5141
  path: "settings.json",
4700
5142
  fields: [
4701
- { key: "title", label: "Site ad\u0131", type: "text" },
4702
- { key: "tagline", label: "Slogan", type: "text" }
5143
+ {
5144
+ key: "title",
5145
+ label: "Site name",
5146
+ type: "text"
5147
+ },
5148
+ {
5149
+ key: "tagline",
5150
+ label: "Tagline",
5151
+ type: "text"
5152
+ }
4703
5153
  ]
4704
5154
  }
4705
5155
  ]
4706
5156
  },
4707
5157
  samples: {
4708
5158
  pages: [
4709
- { title: "Ba\u015Flarken", slug: "baslarken", order: 1, body: "Kuruluma ho\u015F geldin." }
4710
- ]
5159
+ {
5160
+ title: "Getting started",
5161
+ slug: "getting-started",
5162
+ order: 1,
5163
+ body: "Welcome aboard."
5164
+ }
5165
+ ],
5166
+ settings: {
5167
+ title: "Docs",
5168
+ tagline: "Everything you need to know"
5169
+ }
4711
5170
  }
4712
5171
  };
4713
5172
 
4714
5173
  // src/templates/event.json
4715
5174
  var event_default = {
4716
- title: "Etkinlik Program\u0131",
4717
- description: "Tarih, saat ve konu\u015Fmac\u0131l\u0131 oturumlardan olu\u015Fan bir etkinlik ajandas\u0131.",
5175
+ title: "Event schedule",
5176
+ description: "An event agenda of sessions with date, time and speaker.",
4718
5177
  schema: {
4719
5178
  version: 1,
4720
5179
  collections: [
4721
5180
  {
4722
- name: "oturumlar",
4723
- label: "Oturumlar",
4724
- path: "oturumlar",
5181
+ name: "sessions",
5182
+ label: "Sessions",
5183
+ path: "sessions",
4725
5184
  fields: [
4726
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4727
- { key: "slug", label: "Slug", type: "text", required: true },
4728
- { key: "tarih", label: "Tarih", type: "date" },
4729
- { key: "saat", label: "Saat", type: "text" },
4730
- { key: "konusmaci", label: "Konu\u015Fmac\u0131", type: "text" },
4731
- { key: "yer", label: "Salon", type: "text" },
4732
- { key: "aciklama", label: "A\xE7\u0131klama", type: "richtext" }
5185
+ {
5186
+ key: "title",
5187
+ label: "Title",
5188
+ type: "text",
5189
+ required: true
5190
+ },
5191
+ {
5192
+ key: "slug",
5193
+ label: "Slug",
5194
+ type: "text",
5195
+ required: true
5196
+ },
5197
+ {
5198
+ key: "date",
5199
+ label: "Date",
5200
+ type: "date"
5201
+ },
5202
+ {
5203
+ key: "time",
5204
+ label: "Time",
5205
+ type: "text"
5206
+ },
5207
+ {
5208
+ key: "speaker",
5209
+ label: "Speaker",
5210
+ type: "text"
5211
+ },
5212
+ {
5213
+ key: "room",
5214
+ label: "Room",
5215
+ type: "text"
5216
+ },
5217
+ {
5218
+ key: "description",
5219
+ label: "Description",
5220
+ type: "richtext"
5221
+ }
4733
5222
  ]
4734
5223
  }
4735
5224
  ],
4736
5225
  singletons: [
4737
5226
  {
4738
- name: "etkinlik",
4739
- label: "Etkinlik",
4740
- path: "etkinlik.json",
5227
+ name: "event",
5228
+ label: "Event",
5229
+ path: "event.json",
4741
5230
  fields: [
4742
- { key: "title", label: "Etkinlik ad\u0131", type: "text" },
4743
- { key: "slogan", label: "Slogan", type: "text" }
5231
+ {
5232
+ key: "title",
5233
+ label: "Event name",
5234
+ type: "text"
5235
+ },
5236
+ {
5237
+ key: "tagline",
5238
+ label: "Tagline",
5239
+ type: "text"
5240
+ }
4744
5241
  ]
4745
5242
  }
4746
5243
  ]
4747
5244
  },
4748
5245
  samples: {
4749
- oturumlar: [
5246
+ sessions: [
4750
5247
  {
4751
- title: "A\xE7\u0131l\u0131\u015F Konu\u015Fmas\u0131",
4752
- slug: "acilis-konusmasi",
4753
- saat: "09:00",
4754
- konusmaci: "Ay\u015Fe Y\u0131lmaz",
4755
- yer: "Ana Salon",
4756
- aciklama: "Etkinli\u011Fe ho\u015F geldiniz."
5248
+ title: "Opening keynote",
5249
+ slug: "opening-keynote",
5250
+ time: "09:00",
5251
+ speaker: "Ada Lovelace",
5252
+ room: "Main hall",
5253
+ description: "Welcome to the event."
4757
5254
  }
4758
- ]
5255
+ ],
5256
+ event: {
5257
+ title: "My event",
5258
+ tagline: "One day, one track"
5259
+ }
4759
5260
  }
4760
5261
  };
4761
5262
 
4762
5263
  // src/templates/portfolio.json
4763
5264
  var portfolio_default = {
4764
- title: "Portfolyo",
4765
- description: "Kapak g\xF6rseli, etiket ve rich-text i\xE7erikli projeler; bir de Hakk\u0131nda sayfas\u0131.",
5265
+ title: "Portfolio",
5266
+ description: "Projects with a cover image, tag and rich-text body, plus an About page.",
4766
5267
  schema: {
4767
5268
  version: 1,
4768
5269
  collections: [
4769
5270
  {
4770
5271
  name: "projects",
4771
- label: "Projeler",
5272
+ label: "Projects",
4772
5273
  path: "projects",
4773
5274
  fields: [
4774
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4775
- { key: "slug", label: "Slug", type: "text", required: true },
4776
- { key: "summary", label: "\xD6zet", type: "text" },
4777
- { key: "cover", label: "Kapak", type: "image" },
4778
- { key: "url", label: "Ba\u011Flant\u0131", type: "text" },
5275
+ {
5276
+ key: "title",
5277
+ label: "Title",
5278
+ type: "text",
5279
+ required: true
5280
+ },
5281
+ {
5282
+ key: "slug",
5283
+ label: "Slug",
5284
+ type: "text",
5285
+ required: true
5286
+ },
5287
+ {
5288
+ key: "summary",
5289
+ label: "Summary",
5290
+ type: "text"
5291
+ },
5292
+ {
5293
+ key: "cover",
5294
+ label: "Cover",
5295
+ type: "image"
5296
+ },
5297
+ {
5298
+ key: "url",
5299
+ label: "Link",
5300
+ type: "url"
5301
+ },
4779
5302
  {
4780
5303
  key: "kind",
4781
- label: "T\xFCr",
5304
+ label: "Kind",
4782
5305
  type: "select",
4783
- options: ["Web", "Mobil", "Tasar\u0131m"]
5306
+ options: ["Web", "Mobile", "Design"]
4784
5307
  },
4785
- { key: "body", label: "\u0130\xE7erik", type: "richtext" },
4786
- { key: "date", label: "Tarih", type: "date" }
5308
+ {
5309
+ key: "body",
5310
+ label: "Body",
5311
+ type: "richtext"
5312
+ },
5313
+ {
5314
+ key: "date",
5315
+ label: "Date",
5316
+ type: "date"
5317
+ }
4787
5318
  ]
4788
5319
  }
4789
5320
  ],
4790
5321
  singletons: [
4791
5322
  {
4792
5323
  name: "about",
4793
- label: "Hakk\u0131nda",
5324
+ label: "About",
4794
5325
  path: "about.json",
4795
5326
  fields: [
4796
- { key: "name", label: "Ad", type: "text", required: true },
4797
- { key: "avatar", label: "Avatar", type: "image" },
4798
- { key: "bio", label: "Biyografi", type: "richtext" }
5327
+ {
5328
+ key: "name",
5329
+ label: "Name",
5330
+ type: "text",
5331
+ required: true
5332
+ },
5333
+ {
5334
+ key: "avatar",
5335
+ label: "Avatar",
5336
+ type: "image"
5337
+ },
5338
+ {
5339
+ key: "bio",
5340
+ label: "Bio",
5341
+ type: "richtext"
5342
+ }
4799
5343
  ]
4800
5344
  }
4801
5345
  ]
@@ -4803,58 +5347,101 @@ var portfolio_default = {
4803
5347
  samples: {
4804
5348
  projects: [
4805
5349
  {
4806
- title: "\xD6rnek Proje",
4807
- slug: "ornek-proje",
4808
- summary: "K\u0131sa bir a\xE7\u0131klama.",
5350
+ title: "Sample project",
5351
+ slug: "sample-project",
5352
+ summary: "A short description.",
4809
5353
  kind: "Web",
4810
- body: "Proje detaylar\u0131 burada."
5354
+ body: "Project details go here."
4811
5355
  }
4812
- ]
5356
+ ],
5357
+ about: {
5358
+ name: "Ada Lovelace",
5359
+ bio: "I build things for the web."
5360
+ }
4813
5361
  }
4814
5362
  };
4815
5363
 
4816
5364
  // src/templates/recipe.json
4817
5365
  var recipe_default = {
4818
- title: "Tarif Defteri",
4819
- description: "Malzeme, yap\u0131l\u0131\u015F ve kapak g\xF6rselli tarifler; bir de Mutfak ayarlar\u0131.",
5366
+ title: "Recipe box",
5367
+ description: "Recipes with ingredients, steps and a cover image, plus Kitchen settings.",
4820
5368
  schema: {
4821
5369
  version: 1,
4822
5370
  collections: [
4823
5371
  {
4824
- name: "tarifler",
4825
- label: "Tarifler",
4826
- path: "tarifler",
5372
+ name: "recipes",
5373
+ label: "Recipes",
5374
+ path: "recipes",
4827
5375
  fields: [
4828
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4829
- { key: "slug", label: "Slug", type: "text", required: true },
4830
- { key: "sure", label: "S\xFCre", type: "text" },
4831
- { key: "porsiyon", label: "Porsiyon", type: "number" },
4832
- { key: "kapak", label: "Kapak", type: "image" },
4833
- { key: "malzemeler", label: "Malzemeler", type: "richtext" },
4834
- { key: "yapilis", label: "Yap\u0131l\u0131\u015F", type: "richtext" }
5376
+ {
5377
+ key: "title",
5378
+ label: "Title",
5379
+ type: "text",
5380
+ required: true
5381
+ },
5382
+ {
5383
+ key: "slug",
5384
+ label: "Slug",
5385
+ type: "text",
5386
+ required: true
5387
+ },
5388
+ {
5389
+ key: "time",
5390
+ label: "Time",
5391
+ type: "text"
5392
+ },
5393
+ {
5394
+ key: "servings",
5395
+ label: "Servings",
5396
+ type: "number"
5397
+ },
5398
+ {
5399
+ key: "cover",
5400
+ label: "Cover",
5401
+ type: "image"
5402
+ },
5403
+ {
5404
+ key: "ingredients",
5405
+ label: "Ingredients",
5406
+ type: "richtext"
5407
+ },
5408
+ {
5409
+ key: "steps",
5410
+ label: "Steps",
5411
+ type: "richtext"
5412
+ }
4835
5413
  ]
4836
5414
  }
4837
5415
  ],
4838
5416
  singletons: [
4839
5417
  {
4840
- name: "mutfak",
4841
- label: "Mutfak",
4842
- path: "mutfak.json",
4843
- fields: [{ key: "title", label: "Site ad\u0131", type: "text" }]
5418
+ name: "kitchen",
5419
+ label: "Kitchen",
5420
+ path: "kitchen.json",
5421
+ fields: [
5422
+ {
5423
+ key: "title",
5424
+ label: "Site name",
5425
+ type: "text"
5426
+ }
5427
+ ]
4844
5428
  }
4845
5429
  ]
4846
5430
  },
4847
5431
  samples: {
4848
- tarifler: [
5432
+ recipes: [
4849
5433
  {
4850
- title: "Ak\u015Fam Makarnas\u0131",
4851
- slug: "aksam-makarnasi",
4852
- sure: "20 dk",
4853
- porsiyon: 2,
4854
- malzemeler: "- 200g makarna\n- 2 di\u015F sar\u0131msak\n- Zeytinya\u011F\u0131",
4855
- yapilis: "Makarnay\u0131 ha\u015Fla. Sar\u0131msa\u011F\u0131 zeytinya\u011F\u0131nda kavur. Kar\u0131\u015Ft\u0131r."
5434
+ title: "Weeknight pasta",
5435
+ slug: "weeknight-pasta",
5436
+ time: "20 min",
5437
+ servings: 2,
5438
+ ingredients: "- 200g pasta\n- 2 cloves garlic\n- Olive oil",
5439
+ steps: "Boil the pasta. Fry the garlic in olive oil. Toss together."
4856
5440
  }
4857
- ]
5441
+ ],
5442
+ kitchen: {
5443
+ title: "My recipe box"
5444
+ }
4858
5445
  }
4859
5446
  };
4860
5447
 
@@ -4894,10 +5481,15 @@ async function applyTemplate(adapter, contentDir, template) {
4894
5481
  const schema = parseSchema(template.schema);
4895
5482
  await saveSchema(adapter, schema, contentDir);
4896
5483
  const store = new ContentStore(adapter, schema, contentDir);
4897
- for (const [collection, rows] of Object.entries(template.samples)) {
4898
- for (const row of rows) {
4899
- const slug = slugify(typeof row.slug === "string" ? row.slug : String(row.title ?? "icerik"));
4900
- await store.writeEntry(collection, slug, row);
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) {
5491
+ const slug = slugify(typeof row.slug === "string" ? row.slug : String(row.title ?? "content"));
5492
+ await store.writeEntry(name, slug, row);
4901
5493
  }
4902
5494
  }
4903
5495
  return schema;
@@ -4908,29 +5500,402 @@ async function initProject(root2, templateName) {
4908
5500
  const adapter = new FsAdapter(root2);
4909
5501
  const contentDir = await resolveContentDir(root2);
4910
5502
  if (await loadSchema(adapter, contentDir)) {
4911
- throw new Error("Bu klas\xF6rde zaten bir \u015Fema var (content/_schema.json).");
5503
+ throw new Error("This folder already has a schema (content/_schema.json).");
4912
5504
  }
4913
5505
  await applyTemplate(adapter, contentDir, template);
4914
5506
  }
4915
5507
 
4916
5508
  // src/commands/types.ts
4917
5509
  import { join as join2 } from "path";
4918
- async function generateTypesFile(root2, outPath = "types.ts") {
5510
+ async function generateTypesFile(root2, outPath = "types.ts", loaderPath = "content.ts") {
4919
5511
  const adapter = new FsAdapter(root2);
4920
5512
  const contentDir = await resolveContentDir(root2);
4921
5513
  const schema = await loadSchema(adapter, contentDir);
4922
- if (!schema) throw new Error("\u015Eema bulunamad\u0131. \xD6nce `justjson init` \xE7al\u0131\u015Ft\u0131r\u0131n.");
5514
+ if (!schema) throw new Error("No schema found. Run `justjson init` first.");
4923
5515
  await adapter.write(outPath, generateTypes(schema));
5516
+ await adapter.write(loaderPath, generateLoader(schema, contentDir));
4924
5517
  return join2(root2, outPath);
4925
5518
  }
4926
5519
 
5520
+ // src/commands/validate.ts
5521
+ import { readdir as readdir2 } from "fs/promises";
5522
+ import { join as join3 } from "path";
5523
+ async function listDir(path) {
5524
+ try {
5525
+ const entries = await readdir2(path, { withFileTypes: true });
5526
+ return entries.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
5527
+ } catch {
5528
+ return [];
5529
+ }
5530
+ }
5531
+ async function loadProjectContent(root2) {
5532
+ const adapter = new FsAdapter(root2);
5533
+ const contentDir = await resolveContentDir(root2);
5534
+ const schema = await loadSchema(adapter, contentDir);
5535
+ if (!schema) return null;
5536
+ const store = new ContentStore(adapter, schema, contentDir);
5537
+ const collections = {};
5538
+ for (const col of schema.collections) {
5539
+ const slugs = await store.listEntries(col.name);
5540
+ const entries = [];
5541
+ for (const slug of slugs) {
5542
+ entries.push({ slug, data: await store.readEntry(col.name, slug) ?? {} });
5543
+ }
5544
+ collections[col.name] = entries;
5545
+ }
5546
+ const known = new Set(schema.collections.map((c) => c.name));
5547
+ for (const { name, isDir } of await listDir(join3(root2, contentDir))) {
5548
+ if (!isDir || name === "media" || known.has(name) || collections[name]) continue;
5549
+ const files = await listDir(join3(root2, contentDir, name));
5550
+ collections[name] = files.filter((f) => !f.isDir && f.name.endsWith(".json")).map((f) => ({ slug: f.name.slice(0, -".json".length), data: {} }));
5551
+ }
5552
+ const singletons = {};
5553
+ for (const s of schema.singletons) {
5554
+ singletons[s.name] = await store.readSingleton(s.name);
5555
+ }
5556
+ const media = (await listDir(join3(root2, contentDir, "media"))).filter((f) => !f.isDir).map((f) => `${contentDir}/media/${f.name}`);
5557
+ return { schema, content: { collections, singletons, media } };
5558
+ }
5559
+ async function validateProjectAt(root2) {
5560
+ const loaded = await loadProjectContent(root2);
5561
+ if (!loaded) return null;
5562
+ return validateProject(loaded.schema, loaded.content);
5563
+ }
5564
+ function summarize(issues) {
5565
+ let errors = 0;
5566
+ let warnings = 0;
5567
+ for (const i of issues) {
5568
+ if (i.level === "error") errors++;
5569
+ else warnings++;
5570
+ }
5571
+ return { errors, warnings };
5572
+ }
5573
+ function shouldFail(issues, strict) {
5574
+ const { errors, warnings } = summarize(issues);
5575
+ return errors > 0 || strict && warnings > 0;
5576
+ }
5577
+ function location(issue) {
5578
+ const parts = [];
5579
+ if (issue.collection) parts.push(issue.collection);
5580
+ if (issue.singleton) parts.push(issue.singleton);
5581
+ if (issue.slug) parts.push(issue.slug);
5582
+ if (issue.field) parts.push(issue.field);
5583
+ return parts.join(":") || "(project)";
5584
+ }
5585
+ function formatText(issues) {
5586
+ if (issues.length === 0) return "\u2713 No issues \u2014 content matches the schema.";
5587
+ const lines = issues.map((i) => {
5588
+ const tag = i.level === "error" ? "ERROR" : "WARN ";
5589
+ return ` ${tag} ${location(i)} ${i.message}`;
5590
+ });
5591
+ const { errors, warnings } = summarize(issues);
5592
+ lines.push("");
5593
+ lines.push(`${errors} error(s), ${warnings} warning(s)`);
5594
+ return lines.join("\n");
5595
+ }
5596
+ function formatJson(issues) {
5597
+ const { errors, warnings } = summarize(issues);
5598
+ return JSON.stringify({ ok: errors === 0, errors, warnings, issues }, null, 2);
5599
+ }
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.6.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
+
4927
5776
  // src/server.ts
4928
5777
  import { exec } from "child_process";
4929
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
4930
- import { basename, dirname as dirname2, extname, join as join3, 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";
4931
5780
  import { fileURLToPath } from "url";
4932
5781
  import { serve } from "@hono/node-server";
4933
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
+ async function git(root2, args) {
5813
+ const { stdout } = await run("git", args, { cwd: root2 });
5814
+ return stdout.trim();
5815
+ }
5816
+ async function tryGit(root2, args) {
5817
+ try {
5818
+ return await git(root2, args);
5819
+ } catch {
5820
+ return null;
5821
+ }
5822
+ }
5823
+ async function hasGh() {
5824
+ try {
5825
+ await run("gh", ["--version"]);
5826
+ return true;
5827
+ } catch {
5828
+ return false;
5829
+ }
5830
+ }
5831
+ async function gitStatus(root2, contentDir) {
5832
+ const inside = await tryGit(root2, ["rev-parse", "--is-inside-work-tree"]);
5833
+ if (inside !== "true") {
5834
+ return {
5835
+ isRepo: false,
5836
+ branch: null,
5837
+ hasRemote: false,
5838
+ remoteUrl: null,
5839
+ pendingFiles: 0,
5840
+ hasGh: await hasGh()
5841
+ };
5842
+ }
5843
+ const branch = await tryGit(root2, ["branch", "--show-current"]);
5844
+ const remoteUrl = await tryGit(root2, ["remote", "get-url", "origin"]);
5845
+ const porcelain = await tryGit(root2, ["status", "--porcelain", "-uall", "--", contentDir]) ?? "";
5846
+ return {
5847
+ isRepo: true,
5848
+ branch: branch || null,
5849
+ hasRemote: remoteUrl !== null,
5850
+ remoteUrl,
5851
+ pendingFiles: porcelain ? porcelain.split("\n").filter(Boolean).length : 0,
5852
+ hasGh: await hasGh()
5853
+ };
5854
+ }
5855
+ async function commitContent(root2, contentDir, message) {
5856
+ const inside = await tryGit(root2, ["rev-parse", "--is-inside-work-tree"]);
5857
+ if (inside !== "true") throw new Error("Not a git repository \u2014 run `git init` first.");
5858
+ await git(root2, ["add", "--", contentDir]);
5859
+ const staged = await tryGit(root2, ["diff", "--cached", "--name-only", "--", contentDir]);
5860
+ if (!staged) return { committed: false, count: 0 };
5861
+ await git(root2, ["commit", "-m", message, "--", contentDir]);
5862
+ return { committed: true, count: staged.split("\n").filter(Boolean).length };
5863
+ }
5864
+ async function pushContent(root2) {
5865
+ const branch = await tryGit(root2, ["branch", "--show-current"]);
5866
+ if (!branch || branch === "HEAD") throw new Error("No branch to push \u2014 commit something first.");
5867
+ const hasUpstream = await tryGit(root2, ["rev-parse", "--abbrev-ref", `${branch}@{upstream}`]);
5868
+ const args = hasUpstream ? ["push"] : ["push", "-u", "origin", branch];
5869
+ await git(root2, args);
5870
+ return { branch };
5871
+ }
5872
+ async function createGitHubRepo(root2, options) {
5873
+ if (!await hasGh()) {
5874
+ throw new Error("GitHub CLI (gh) not found \u2014 install it, or add a remote yourself.");
5875
+ }
5876
+ const inside = await tryGit(root2, ["rev-parse", "--is-inside-work-tree"]);
5877
+ if (inside !== "true") {
5878
+ await git(root2, ["init", "-b", "main"]);
5879
+ }
5880
+ await run(
5881
+ "gh",
5882
+ [
5883
+ "repo",
5884
+ "create",
5885
+ options.name,
5886
+ options.private ? "--private" : "--public",
5887
+ "--source",
5888
+ ".",
5889
+ "--push"
5890
+ ],
5891
+ {
5892
+ cwd: root2
5893
+ }
5894
+ );
5895
+ return { name: options.name };
5896
+ }
5897
+
5898
+ // src/server.ts
4934
5899
  var editorDir = fileURLToPath(new URL("./editor", import.meta.url));
4935
5900
  var PROVIDER_BASE_URLS = {
4936
5901
  groq: "https://api.groq.com/openai/v1",
@@ -4948,16 +5913,17 @@ async function callGemini(apiKey, model, system, prompt) {
4948
5913
  })
4949
5914
  });
4950
5915
  const data = await res.json();
4951
- if (!res.ok) throw new Error(data.error?.message ?? `Gemini iste\u011Fi ba\u015Far\u0131s\u0131z (${res.status})`);
5916
+ if (!res.ok) throw new Error(data.error?.message ?? `Gemini request failed (${res.status})`);
4952
5917
  const text = data.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
4953
- if (!text) throw new Error("Gemini bo\u015F yan\u0131t d\xF6nd\xFC");
5918
+ if (!text) throw new Error("Gemini returned an empty response");
4954
5919
  return text;
4955
5920
  }
4956
5921
  async function listGeminiModels(apiKey) {
4957
5922
  const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}&pageSize=1000`;
4958
5923
  const res = await fetch(url);
4959
5924
  const data = await res.json();
4960
- if (!res.ok) throw new Error(data.error?.message ?? `Model listesi al\u0131namad\u0131 (${res.status})`);
5925
+ if (!res.ok)
5926
+ throw new Error(data.error?.message ?? `Could not fetch the model list (${res.status})`);
4961
5927
  return (data.models ?? []).filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((m) => ({
4962
5928
  id: (m.name ?? "").replace(/^models\//, ""),
4963
5929
  label: m.displayName ?? (m.name ?? "").replace(/^models\//, "")
@@ -4966,16 +5932,17 @@ async function listGeminiModels(apiKey) {
4966
5932
  );
4967
5933
  }
4968
5934
  async function listOpenAiCompatibleModels(baseUrl, apiKey) {
4969
- if (!baseUrl) throw new Error("Bu sa\u011Flay\u0131c\u0131 i\xE7in taban URL gerekli");
5935
+ if (!baseUrl) throw new Error("This provider needs a base URL");
4970
5936
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
4971
5937
  headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {}
4972
5938
  });
4973
5939
  const data = await res.json();
4974
- if (!res.ok) throw new Error(data.error?.message ?? `Model listesi al\u0131namad\u0131 (${res.status})`);
5940
+ if (!res.ok)
5941
+ throw new Error(data.error?.message ?? `Could not fetch the model list (${res.status})`);
4975
5942
  return (data.data ?? []).map((m) => ({ id: m.id ?? "", label: m.id ?? "" })).filter((m) => m.id);
4976
5943
  }
4977
5944
  async function callOpenAiCompatible(baseUrl, apiKey, model, system, prompt) {
4978
- if (!baseUrl) throw new Error("Bu sa\u011Flay\u0131c\u0131 i\xE7in taban URL gerekli");
5945
+ if (!baseUrl) throw new Error("This provider needs a base URL");
4979
5946
  const messages = [
4980
5947
  ...system ? [{ role: "system", content: system }] : [],
4981
5948
  { role: "user", content: prompt }
@@ -4986,9 +5953,9 @@ async function callOpenAiCompatible(baseUrl, apiKey, model, system, prompt) {
4986
5953
  body: JSON.stringify({ model, messages })
4987
5954
  });
4988
5955
  const data = await res.json();
4989
- if (!res.ok) throw new Error(data.error?.message ?? `\u0130stek ba\u015Far\u0131s\u0131z (${res.status})`);
5956
+ if (!res.ok) throw new Error(data.error?.message ?? `Request failed (${res.status})`);
4990
5957
  const text = data.choices?.[0]?.message?.content ?? "";
4991
- if (!text) throw new Error("Sa\u011Flay\u0131c\u0131 bo\u015F yan\u0131t d\xF6nd\xFC");
5958
+ if (!text) throw new Error("The provider returned an empty response");
4992
5959
  return text;
4993
5960
  }
4994
5961
  var MIME = {
@@ -5009,28 +5976,68 @@ async function createServer(root2) {
5009
5976
  let store = new ContentStore(adapter, schema, contentDir);
5010
5977
  const app = new Hono();
5011
5978
  app.onError((err, c) => {
5012
- const msg = err.message;
5013
- if (msg.startsWith("Bilinmeyen")) return c.json({ error: msg }, 404);
5014
- if (msg.startsWith("G\xFCvensiz") || msg.startsWith("Yol")) return c.json({ error: msg }, 400);
5015
- return c.json({ error: "sunucu hatas\u0131" }, 500);
5979
+ if (err instanceof NotFoundError) return c.json({ error: err.message }, 404);
5980
+ if (err instanceof UnsafeSlugError || err instanceof PathEscapeError) {
5981
+ return c.json({ error: err.message }, 400);
5982
+ }
5983
+ return c.json({ error: "server error" }, 500);
5016
5984
  });
5017
5985
  app.get(
5018
5986
  "/api/_project",
5019
5987
  (c) => c.json({
5020
- name: basename(root2) || "proje",
5988
+ name: basename2(root2) || "project",
5021
5989
  path: root2,
5022
5990
  contentDir,
5023
5991
  collections: schema.collections.length,
5024
5992
  singletons: schema.singletons.length
5025
5993
  })
5026
5994
  );
5995
+ app.post("/api/_ship/scaffold", async (c) => {
5996
+ if (schema.collections.length === 0 && schema.singletons.length === 0) {
5997
+ return c.json({ error: "Pick a template or build a schema first." }, 400);
5998
+ }
5999
+ return c.json(await writeAstroSite(root2, schema, basename2(root2) || "my-site"));
6000
+ });
6001
+ app.get(
6002
+ "/api/_ship",
6003
+ async (c) => c.json({
6004
+ framework: await detectFramework(root2),
6005
+ git: await gitStatus(root2, contentDir)
6006
+ })
6007
+ );
6008
+ app.post("/api/_ship/commit", async (c) => {
6009
+ const { message } = await c.req.json();
6010
+ try {
6011
+ return c.json(await commitContent(root2, contentDir, message?.trim() || "content: update"));
6012
+ } catch (e) {
6013
+ return c.json({ error: e.message }, 400);
6014
+ }
6015
+ });
6016
+ app.post("/api/_ship/push", async (c) => {
6017
+ try {
6018
+ return c.json(await pushContent(root2));
6019
+ } catch (e) {
6020
+ return c.json({ error: e.message }, 400);
6021
+ }
6022
+ });
6023
+ app.post("/api/_ship/repo", async (c) => {
6024
+ const { name, private: isPrivate } = await c.req.json();
6025
+ if (!name?.trim()) return c.json({ error: "A repository name is required." }, 400);
6026
+ try {
6027
+ return c.json(
6028
+ await createGitHubRepo(root2, { name: name.trim(), private: isPrivate !== false })
6029
+ );
6030
+ } catch (e) {
6031
+ return c.json({ error: e.message }, 400);
6032
+ }
6033
+ });
5027
6034
  app.get("/api/_templates", (c) => c.json({ items: templateList() }));
5028
6035
  app.post("/api/_init", async (c) => {
5029
6036
  const { template: id } = await c.req.json();
5030
6037
  const t = id ? getTemplate(id) : void 0;
5031
- if (!t) return c.json({ error: "Bilinmeyen template" }, 404);
6038
+ if (!t) return c.json({ error: "Unknown template" }, 404);
5032
6039
  if (schema.collections.length > 0 || schema.singletons.length > 0) {
5033
- return c.json({ error: "Bu klas\xF6rde zaten bir \u015Fema var." }, 400);
6040
+ return c.json({ error: "This folder already has a schema." }, 400);
5034
6041
  }
5035
6042
  schema = await applyTemplate(adapter, contentDir, t);
5036
6043
  store = new ContentStore(adapter, schema, contentDir);
@@ -5038,7 +6045,7 @@ async function createServer(root2) {
5038
6045
  });
5039
6046
  app.post("/api/_import", async (c) => {
5040
6047
  if (schema.collections.length > 0 || schema.singletons.length > 0) {
5041
- return c.json({ error: "Bu klas\xF6rde zaten bir \u015Fema var." }, 400);
6048
+ return c.json({ error: "This folder already has a schema." }, 400);
5042
6049
  }
5043
6050
  const { raw } = await c.req.json();
5044
6051
  let next;
@@ -5053,7 +6060,7 @@ async function createServer(root2) {
5053
6060
  entries = inferred.entries;
5054
6061
  singletonData = inferred.singletons;
5055
6062
  } catch (e) {
5056
- return c.json({ error: `\u0130\xE7e aktar\u0131lamad\u0131: ${e.message}` }, 400);
6063
+ return c.json({ error: `Import failed: ${e.message}` }, 400);
5057
6064
  }
5058
6065
  }
5059
6066
  await saveSchema(adapter, next, contentDir);
@@ -5061,7 +6068,7 @@ async function createServer(root2) {
5061
6068
  store = new ContentStore(adapter, schema, contentDir);
5062
6069
  for (const [collection, rows] of Object.entries(entries)) {
5063
6070
  for (const row of rows) {
5064
- const slug = slugify(String(row.slug ?? row.title ?? "icerik")) || "icerik";
6071
+ const slug = slugify(String(row.slug ?? row.title ?? "content")) || "content";
5065
6072
  await store.writeEntry(collection, slug, row);
5066
6073
  }
5067
6074
  }
@@ -5103,19 +6110,19 @@ async function createServer(root2) {
5103
6110
  });
5104
6111
  app.post("/api/_media", async (c) => {
5105
6112
  const body = await c.req.json();
5106
- if (!body.dataBase64) return c.json({ error: "veri yok" }, 400);
6113
+ if (!body.dataBase64) return c.json({ error: "no data" }, 400);
5107
6114
  const base = slugify((body.filename ?? "gorsel").replace(/\.[^.]+$/, "")) || "gorsel";
5108
6115
  const name = `${base}-${Date.now().toString(36)}.webp`;
5109
6116
  const rel = `${contentDir}/media/${name}`;
5110
- const abs = join3(root2, rel);
5111
- await mkdir2(dirname2(abs), { recursive: true });
5112
- await writeFile3(abs, Buffer.from(body.dataBase64, "base64"));
6117
+ const abs = join6(root2, rel);
6118
+ await mkdir3(dirname3(abs), { recursive: true });
6119
+ await writeFile4(abs, Buffer.from(body.dataBase64, "base64"));
5113
6120
  return c.json({ path: rel });
5114
6121
  });
5115
6122
  app.post("/api/_ai/models", async (c) => {
5116
6123
  const body = await c.req.json();
5117
6124
  const { provider, apiKey } = body;
5118
- if (!provider) return c.json({ error: "provider zorunlu" }, 400);
6125
+ if (!provider) return c.json({ error: "provider is required" }, 400);
5119
6126
  try {
5120
6127
  const models = provider === "gemini" ? await listGeminiModels(apiKey ?? "") : await listOpenAiCompatibleModels(
5121
6128
  body.baseUrl || PROVIDER_BASE_URLS[provider] || "",
@@ -5130,7 +6137,7 @@ async function createServer(root2) {
5130
6137
  const body = await c.req.json();
5131
6138
  const { provider, apiKey, model, system, prompt } = body;
5132
6139
  if (!provider || !apiKey || !model || !prompt) {
5133
- return c.json({ error: "provider, model, apiKey ve prompt zorunlu" }, 400);
6140
+ return c.json({ error: "provider, model, apiKey and prompt are required" }, 400);
5134
6141
  }
5135
6142
  try {
5136
6143
  const text = provider === "gemini" ? await callGemini(apiKey, model, system, prompt) : await callOpenAiCompatible(
@@ -5151,10 +6158,10 @@ async function createServer(root2) {
5151
6158
  return c.text("forbidden", 400);
5152
6159
  }
5153
6160
  try {
5154
- const bytes = await readFile2(join3(root2, contentDir, "media", file));
6161
+ const bytes = await readFile3(join6(root2, contentDir, "media", file));
5155
6162
  return new Response(bytes, { headers: { "content-type": "image/webp" } });
5156
6163
  } catch {
5157
- return c.text("yok", 404);
6164
+ return c.text("not found", 404);
5158
6165
  }
5159
6166
  });
5160
6167
  app.get("/api/:collection", async (c) => {
@@ -5163,7 +6170,7 @@ async function createServer(root2) {
5163
6170
  });
5164
6171
  app.get("/api/:collection/:slug", async (c) => {
5165
6172
  const data = await store.readEntry(c.req.param("collection"), c.req.param("slug"));
5166
- return data ? c.json(data) : c.json({ error: "bulunamad\u0131" }, 404);
6173
+ return data ? c.json(data) : c.json({ error: "not found" }, 404);
5167
6174
  });
5168
6175
  app.put("/api/:collection/:slug", async (c) => {
5169
6176
  const slug = slugify(c.req.param("slug"));
@@ -5178,17 +6185,17 @@ async function createServer(root2) {
5178
6185
  app.get("/*", async (c) => {
5179
6186
  const urlPath = c.req.path === "/" ? "/index.html" : c.req.path;
5180
6187
  const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
5181
- const file = join3(editorDir, rel);
6188
+ const file = join6(editorDir, rel);
5182
6189
  if (!file.startsWith(editorDir)) return c.text("forbidden", 403);
5183
6190
  const type = MIME[extname(file)] ?? "application/octet-stream";
5184
6191
  try {
5185
- return new Response(await readFile2(file), { headers: { "content-type": type } });
6192
+ return new Response(await readFile3(file), { headers: { "content-type": type } });
5186
6193
  } catch {
5187
6194
  try {
5188
- const html = await readFile2(join3(editorDir, "index.html"));
6195
+ const html = await readFile3(join6(editorDir, "index.html"));
5189
6196
  return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
5190
6197
  } catch {
5191
- return c.text("Edit\xF6r aray\xFCz\xFC bulunamad\u0131 (justjson build gerekli).", 404);
6198
+ return c.text("Editor UI not found (run the justjson build first).", 404);
5192
6199
  }
5193
6200
  }
5194
6201
  });
@@ -5203,27 +6210,51 @@ async function startServer(root2, port) {
5203
6210
  const app = await createServer(root2);
5204
6211
  serve({ fetch: app.fetch, port });
5205
6212
  const url = `http://localhost:${port}`;
5206
- console.log(`JustJSON \xE7al\u0131\u015F\u0131yor: ${url}`);
6213
+ console.log(`JustJSON is running at ${url}`);
5207
6214
  openBrowser(url);
5208
6215
  }
5209
6216
 
6217
+ // src/version.ts
6218
+ import { readFileSync } from "fs";
6219
+ function readVersion() {
6220
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
6221
+ return JSON.parse(raw).version;
6222
+ }
6223
+
5210
6224
  // src/cli.ts
5211
6225
  var program = new Command();
5212
6226
  var root = process.cwd();
5213
- program.name("justjson").description("Lokalde \xE7al\u0131\u015Fan, JSON \xFCreten mini CMS").version("0.0.0");
5214
- program.command("init").description("Bir template ile projeyi ba\u015Flat\u0131r").argument("[template]", `template ad\u0131 (${listTemplates().join(", ")})`, "blog").action(async (template) => {
6227
+ program.name("justjson").description("A tiny local-first CMS that keeps your content as plain JSON").version(readVersion());
6228
+ 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) => {
5215
6229
  await initProject(root, template);
5216
- console.log(`'${template}' template'i ile ba\u015Flat\u0131ld\u0131.`);
6230
+ console.log(`Scaffolded from the '${template}' template.`);
6231
+ if (!opts.astro) return;
6232
+ const schema = await loadSchema(new FsAdapter(root), await resolveContentDir(root));
6233
+ if (!schema) return;
6234
+ const { written, skipped } = await writeAstroSite(root, schema, basename3(root) || "my-site");
6235
+ console.log(`Wrote ${written.length} site file(s).`);
6236
+ if (skipped.length > 0) console.log(`Kept your existing: ${skipped.join(", ")}`);
6237
+ console.log("\nNext:\n npm install\n npm run dev");
5217
6238
  });
5218
- program.command("types").description("\u015Eemadan types.ts \xFCretir").action(async () => {
6239
+ program.command("types").description("Generate types.ts and a typed content.ts loader from your schema").action(async () => {
5219
6240
  const out = await generateTypesFile(root);
5220
- console.log(`Yaz\u0131ld\u0131: ${out}`);
6241
+ console.log(`Wrote: ${out}`);
5221
6242
  });
5222
- program.command("export").description("\u0130\xE7eri\u011Fi ZIP olarak d\u0131\u015Fa aktar\u0131r").action(async () => {
6243
+ program.command("export").description("Export schema, content and types as a ZIP").action(async () => {
5223
6244
  const out = await exportZip(root);
5224
- console.log(`Yaz\u0131ld\u0131: ${out}`);
6245
+ console.log(`Wrote: ${out}`);
6246
+ });
6247
+ program.command("validate").description("Check your content against the schema (great for CI)").option("--json", "machine-readable JSON output").option("--strict", "treat warnings as errors too").action(async (opts) => {
6248
+ const issues = await validateProjectAt(root);
6249
+ if (issues === null) {
6250
+ console.error("No schema found. Run `justjson init` first.");
6251
+ process.exitCode = 1;
6252
+ return;
6253
+ }
6254
+ console.log(opts.json ? formatJson(issues) : formatText(issues));
6255
+ if (shouldFail(issues, Boolean(opts.strict))) process.exitCode = 1;
5225
6256
  });
5226
- program.command("serve", { isDefault: true }).description("Lokal edit\xF6r sunucusunu ba\u015Flat\u0131r").option("-p, --port <port>", "port", "5180").action(async (opts) => {
6257
+ program.command("serve", { isDefault: true }).description("Start the local editor server").option("-p, --port <port>", "port", "5180").action(async (opts) => {
5227
6258
  await startServer(root, Number(opts.port));
5228
6259
  });
5229
6260
  program.parseAsync();