@bobbykim/manguito-cms-cli 0.6.1 → 0.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.
@@ -49,7 +49,7 @@ var init_esm_shims = __esm({
49
49
  }
50
50
  });
51
51
 
52
- // ../api/dist/chunk-JVP33FI2.mjs
52
+ // ../api/dist/chunk-FXPKGPYF.mjs
53
53
  init_esm_shims();
54
54
  import crypto from "crypto";
55
55
 
@@ -1212,7 +1212,7 @@ Subquery.prototype.getSQL = function() {
1212
1212
  return new SQL([this]);
1213
1213
  };
1214
1214
 
1215
- // ../api/dist/chunk-JVP33FI2.mjs
1215
+ // ../api/dist/chunk-FXPKGPYF.mjs
1216
1216
  function isColumnBacked(field) {
1217
1217
  const col = field.db_column;
1218
1218
  if (col === null) return false;
@@ -1220,7 +1220,7 @@ function isColumnBacked(field) {
1220
1220
  if (col.junction) return false;
1221
1221
  return true;
1222
1222
  }
1223
- function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
1223
+ function buildFieldKeyMap(pairs, allLabels, droppedColumns, droppedLabels) {
1224
1224
  const labelToColumn = /* @__PURE__ */ new Map();
1225
1225
  const columnToLabel = /* @__PURE__ */ new Map();
1226
1226
  for (const { label, column } of pairs) {
@@ -1228,7 +1228,7 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
1228
1228
  columnToLabel.set(column, label);
1229
1229
  }
1230
1230
  for (const { label, ownColumn } of allLabels) {
1231
- if (ownColumn === label) continue;
1231
+ if (ownColumn !== void 0) continue;
1232
1232
  const columnOwner = columnToLabel.get(label);
1233
1233
  if (columnOwner !== void 0 && columnOwner !== label) {
1234
1234
  throw new Error(
@@ -1237,23 +1237,33 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
1237
1237
  }
1238
1238
  }
1239
1239
  for (const label of labelToColumn.keys()) {
1240
- if (droppedKeys.has(label)) labelToColumn.delete(label);
1240
+ if (droppedLabels.has(label)) labelToColumn.delete(label);
1241
1241
  }
1242
1242
  for (const column of columnToLabel.keys()) {
1243
- if (droppedKeys.has(column)) columnToLabel.delete(column);
1243
+ if (droppedColumns.has(column)) columnToLabel.delete(column);
1244
1244
  }
1245
1245
  const diverges = [...labelToColumn].some(([label, column]) => label !== column);
1246
- function remap(input, lookup) {
1246
+ function remap(input, lookup, dropped, droppedIfUnmapped) {
1247
1247
  const out = {};
1248
1248
  for (const key of Object.keys(input)) {
1249
- if (droppedKeys.has(key)) continue;
1250
- out[lookup.get(key) ?? key] = input[key];
1249
+ const mapped = lookup.get(key);
1250
+ if (mapped !== void 0) {
1251
+ out[mapped] = input[key];
1252
+ continue;
1253
+ }
1254
+ if (dropped.has(key) || droppedIfUnmapped?.has(key)) continue;
1255
+ out[key] = input[key];
1251
1256
  }
1252
1257
  return out;
1253
1258
  }
1254
1259
  return {
1255
- toStorage: (input) => remap(input, labelToColumn),
1256
- toLabels: (row) => remap(row, columnToLabel),
1260
+ // toStorage reads LABEL-keyed input: a live label maps and writes
1261
+ // regardless of either drop set; an unmapped key is refused if it is
1262
+ // EITHER the tombstone's own name (droppedLabels) or a raw, unexposed
1263
+ // column (droppedColumns) — see remap's comment.
1264
+ // toLabels reads COLUMN-keyed input, so it consults only droppedColumns.
1265
+ toStorage: (input) => remap(input, labelToColumn, droppedLabels, droppedColumns),
1266
+ toLabels: (row) => remap(row, columnToLabel, droppedColumns),
1257
1267
  columnFor: (label) => labelToColumn.get(label),
1258
1268
  labelFor: (column) => columnToLabel.get(column),
1259
1269
  labels: [...labelToColumn.keys()],
@@ -1261,14 +1271,15 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
1261
1271
  };
1262
1272
  }
1263
1273
  function createFieldKeyMap(fields) {
1264
- const droppedKeys = /* @__PURE__ */ new Set();
1274
+ const droppedColumns = /* @__PURE__ */ new Set();
1275
+ const droppedLabels = /* @__PURE__ */ new Set();
1265
1276
  const pairs = [];
1266
1277
  for (const f of fields) {
1267
1278
  if (!isColumnBacked(f)) continue;
1268
1279
  pairs.push({ label: f.name, column: f.db_column.column_name });
1269
1280
  if (f.removed === true) {
1270
- droppedKeys.add(f.name);
1271
- droppedKeys.add(f.db_column.column_name);
1281
+ droppedLabels.add(f.name);
1282
+ droppedColumns.add(f.db_column.column_name);
1272
1283
  }
1273
1284
  }
1274
1285
  return buildFieldKeyMap(
@@ -1279,30 +1290,33 @@ function createFieldKeyMap(fields) {
1279
1290
  // forbids present-and-undefined.
1280
1291
  ...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
1281
1292
  })),
1282
- droppedKeys
1293
+ droppedColumns,
1294
+ droppedLabels
1283
1295
  );
1284
1296
  }
1285
1297
  function createFieldKeyMapFromProjection(projectionType, allFields) {
1286
1298
  const pairs = projectionType.fields.map((f) => ({ label: f.exposed_as, column: f.column_name }));
1287
- const projected = new Set(pairs.map((p) => p.label));
1288
- const dropped = /* @__PURE__ */ new Set();
1299
+ const projectedColumns = new Set(pairs.map((p) => p.column));
1300
+ const projectedLabels = new Set(pairs.map((p) => p.label));
1301
+ const droppedColumns = /* @__PURE__ */ new Set();
1302
+ const droppedLabels = /* @__PURE__ */ new Set();
1289
1303
  for (const f of allFields) {
1290
1304
  if (!isColumnBacked(f)) continue;
1291
- dropped.add(f.name);
1292
- dropped.add(f.db_column.column_name);
1305
+ if (!projectedColumns.has(f.db_column.column_name)) droppedColumns.add(f.db_column.column_name);
1306
+ if (!projectedLabels.has(f.name)) droppedLabels.add(f.name);
1293
1307
  }
1294
- for (const p of pairs) dropped.delete(p.column);
1295
- for (const label of projected) dropped.delete(label);
1296
1308
  return buildFieldKeyMap(
1297
1309
  pairs,
1298
1310
  allFields.map((f) => ({
1299
1311
  label: f.name,
1300
- // See buildFieldKeyMap's collision check: this is what lets it tell a
1301
- // version exposing a field's own column under a new label (fine) from a
1302
- // label that reuses another field's column (not fine).
1312
+ // See buildFieldKeyMap's collision check: `ownColumn` marks every
1313
+ // column-backed field so the check skips it entirely — only a
1314
+ // paragraph/junction/programmatic label (no column of its own) can
1315
+ // still collide.
1303
1316
  ...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
1304
1317
  })),
1305
- dropped
1318
+ droppedColumns,
1319
+ droppedLabels
1306
1320
  );
1307
1321
  }
1308
1322
  function quoteIdent(name) {
@@ -1474,9 +1488,8 @@ function groupBy(items, key) {
1474
1488
  }
1475
1489
  return result;
1476
1490
  }
1477
- function needsFkResolution(row, fkColumn, fieldName) {
1478
- if (fkColumn !== fieldName && !(fkColumn in row)) return false;
1479
- const current = row[fieldName];
1491
+ function needsFkResolution(row, fkColumn) {
1492
+ const current = row[fkColumn];
1480
1493
  return !(typeof current === "object" && current !== null);
1481
1494
  }
1482
1495
  async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
@@ -1493,15 +1506,11 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
1493
1506
  row[fieldName] = byParent[row["id"]] ?? [];
1494
1507
  }
1495
1508
  } else if (rel.type === "reference") {
1496
- const dropFk = rel.fk_column !== fieldName;
1497
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
1509
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
1498
1510
  if (pending.length === 0) return;
1499
1511
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
1500
1512
  if (fkValues.length === 0) {
1501
- for (const row of pending) {
1502
- row[fieldName] = null;
1503
- if (dropFk) delete row[rel.fk_column];
1504
- }
1513
+ for (const row of pending) row[rel.fk_column] = null;
1505
1514
  return;
1506
1515
  }
1507
1516
  const unique = [...new Set(fkValues)];
@@ -1518,8 +1527,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
1518
1527
  }
1519
1528
  for (const row of pending) {
1520
1529
  const fkVal = row[rel.fk_column];
1521
- row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
1522
- if (dropFk) delete row[rel.fk_column];
1530
+ row[rel.fk_column] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
1523
1531
  }
1524
1532
  } else if (rel.type === "junction") {
1525
1533
  const parentIds = batch.map((r) => r["id"]);
@@ -1547,15 +1555,11 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
1547
1555
  row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
1548
1556
  }
1549
1557
  } else if (rel.type === "media") {
1550
- const dropFk = rel.fk_column !== fieldName;
1551
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
1558
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
1552
1559
  if (pending.length === 0) return;
1553
1560
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
1554
1561
  if (fkValues.length === 0) {
1555
- for (const row of pending) {
1556
- row[fieldName] = null;
1557
- if (dropFk) delete row[rel.fk_column];
1558
- }
1562
+ for (const row of pending) row[rel.fk_column] = null;
1559
1563
  return;
1560
1564
  }
1561
1565
  const unique = [...new Set(fkValues)];
@@ -1569,8 +1573,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
1569
1573
  }
1570
1574
  for (const row of pending) {
1571
1575
  const fkVal = row[rel.fk_column];
1572
- row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
1573
- if (dropFk) delete row[rel.fk_column];
1576
+ row[rel.fk_column] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
1574
1577
  }
1575
1578
  }
1576
1579
  }
@@ -11,7 +11,7 @@ import {
11
11
  init_esm_shims,
12
12
  isColumnBacked,
13
13
  resolveRelationField
14
- } from "./chunk-SW3T2M64.js";
14
+ } from "./chunk-52I54C3X.js";
15
15
 
16
16
  // ../../node_modules/.pnpm/@whatwg-node+fetch@0.10.13/node_modules/@whatwg-node/fetch/dist/shouldSkipPonyfill.js
17
17
  var require_shouldSkipPonyfill = __commonJS({
@@ -10786,10 +10786,10 @@ var require_parser = __commonJS({
10786
10786
  }
10787
10787
  });
10788
10788
 
10789
- // ../api/dist/handler-XZVDLKZ2.mjs
10789
+ // ../api/dist/handler-GED3I7Z2.mjs
10790
10790
  init_esm_shims();
10791
10791
 
10792
- // ../api/dist/chunk-XNXM4W4K.mjs
10792
+ // ../api/dist/chunk-4LLELFAZ.mjs
10793
10793
  init_esm_shims();
10794
10794
 
10795
10795
  // ../../node_modules/.pnpm/graphql-yoga@5.21.2_graphql@16.14.2/node_modules/graphql-yoga/esm/index.js
@@ -30122,7 +30122,7 @@ function createYoga(options) {
30122
30122
  });
30123
30123
  }
30124
30124
 
30125
- // ../api/dist/chunk-XNXM4W4K.mjs
30125
+ // ../api/dist/chunk-4LLELFAZ.mjs
30126
30126
  var import_dataloader = __toESM(require_dataloader(), 1);
30127
30127
 
30128
30128
  // ../../node_modules/.pnpm/@escape.tech+graphql-armor@3.2.0_@envelop+core@5.5.1_@escape.tech+graphql-armor-types@0.7.0/node_modules/@escape.tech/graphql-armor/dist/graphql-armor.esm.js
@@ -30880,7 +30880,7 @@ var EnvelopArmor = class {
30880
30880
  }
30881
30881
  };
30882
30882
 
30883
- // ../api/dist/chunk-XNXM4W4K.mjs
30883
+ // ../api/dist/chunk-4LLELFAZ.mjs
30884
30884
  var DateTimeScalar = new GraphQLScalarType({
30885
30885
  name: "DateTime",
30886
30886
  description: "ISO-8601 date-time string",
@@ -31086,29 +31086,22 @@ function scalarFieldResolver(field, fallback) {
31086
31086
  function relationFieldResolver(typeName, schemaFieldName) {
31087
31087
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
31088
31088
  }
31089
- function programmaticFieldResolver(typeName, schemaFieldName, mediaFields = [], fieldKeys) {
31089
+ function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = [], fieldKeys) {
31090
31090
  return async (parent, _args, ctx) => {
31091
31091
  let p = ctx.programmaticMemo.get(parent);
31092
31092
  if (!p) {
31093
- p = resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys);
31093
+ p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys);
31094
31094
  ctx.programmaticMemo.set(parent, p);
31095
31095
  }
31096
31096
  return (await p)[schemaFieldName];
31097
31097
  };
31098
31098
  }
31099
- async function resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys) {
31099
+ async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys) {
31100
31100
  const toLabels = (row) => fieldKeys ? fieldKeys.toLabels(row) : row;
31101
- if (mediaFields.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
31101
+ if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
31102
31102
  const enriched = { ...parent };
31103
- await Promise.all(mediaFields.map((m) => ctx.loaders.load(typeName, m.name, enriched)));
31104
- return ctx.resolver.resolveItem(typeName, relabelMedia(toLabels(enriched), enriched, mediaFields));
31105
- }
31106
- function relabelMedia(record, enriched, mediaFields) {
31107
- for (const { name, exposedAs } of mediaFields) {
31108
- if (!(name in enriched)) continue;
31109
- record[exposedAs] = enriched[name];
31110
- }
31111
- return record;
31103
+ await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
31104
+ return ctx.resolver.resolveItem(typeName, toLabels(enriched));
31112
31105
  }
31113
31106
  function collectionResolver(typeName, nameMap, fieldKeys) {
31114
31107
  return async (_root, args, ctx) => {
@@ -31180,7 +31173,7 @@ var MEDIA = new GraphQLObjectType({
31180
31173
  duration: { type: GraphQLInt, resolve: (m) => m.duration }
31181
31174
  }
31182
31175
  });
31183
- function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
31176
+ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view, programmaticKeyMaps = fieldKeyMaps) {
31184
31177
  const objectTypes = /* @__PURE__ */ new Map();
31185
31178
  const enumTypes = /* @__PURE__ */ new Map();
31186
31179
  for (const [name, enumType] of Object.entries(registry.enum_types)) {
@@ -31244,9 +31237,9 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
31244
31237
  }
31245
31238
  function buildObjectType(machineName, type) {
31246
31239
  const visible = viewFieldsFor(machineName, type);
31247
- const mediaFields = visible.filter(
31248
- (v) => v.field.field_type === "image" || v.field.field_type === "video" || v.field.field_type === "file"
31249
- ).map((v) => ({ name: v.field.name, exposedAs: v.exposedAs }));
31240
+ const mediaFieldNames = type.fields.filter(
31241
+ (f) => f.removed !== true && (f.field_type === "image" || f.field_type === "video" || f.field_type === "file")
31242
+ ).map((f) => f.name);
31250
31243
  return new GraphQLObjectType({
31251
31244
  name: graphqlTypeName(machineName),
31252
31245
  fields: () => {
@@ -31266,7 +31259,7 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
31266
31259
  const outType = outputTypeForField(field, vf.required);
31267
31260
  let resolve;
31268
31261
  if (field.field_type === "programmatic") {
31269
- resolve = programmaticFieldResolver(machineName, field.name, mediaFields, fieldKeyMaps[machineName]);
31262
+ resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames, programmaticKeyMaps[machineName]);
31270
31263
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
31271
31264
  resolve = relationFieldResolver(machineName, field.name);
31272
31265
  } else {
@@ -31311,7 +31304,10 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
31311
31304
  for (const [name, ct] of Object.entries(registry.content_types)) {
31312
31305
  const objType = objectTypes.get(name);
31313
31306
  const visible = viewFieldsFor(name, ct);
31314
- const nameMap = buildFieldNameMap(visible.map((v) => v.exposedAs));
31307
+ const nameMap = buildFieldNameMap([
31308
+ ...visible.map((v) => v.exposedAs),
31309
+ ...ct.system_fields.map((s) => s.name)
31310
+ ]);
31315
31311
  if (ct.only_one) {
31316
31312
  queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
31317
31313
  continue;
@@ -31452,7 +31448,8 @@ function createRelationLoaders(db, registry) {
31452
31448
  async (parents) => {
31453
31449
  const rows = parents;
31454
31450
  await resolveRelationField(db, rows, fieldName, rel, cache, true);
31455
- return rows.map((r) => r[fieldName]);
31451
+ const resultKey = rel.type === "reference" || rel.type === "media" ? rel.fk_column : fieldName;
31452
+ return rows.map((r) => r[resultKey]);
31456
31453
  },
31457
31454
  // Batch within a tick; do not memoize by parent identity (rows are mutated
31458
31455
  // and may recur across nesting levels).
@@ -31513,7 +31510,7 @@ function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, optio
31513
31510
  currentVersion: versioning.currentVersion
31514
31511
  });
31515
31512
  }
31516
- const schema = buildGraphQLSchema(registry, fieldKeyMaps, view);
31513
+ const schema = buildGraphQLSchema(registry, fieldKeyMaps, view, versioning.currentFieldKeyMaps ?? fieldKeyMaps);
31517
31514
  const { plugins } = buildArmorPlugin({
31518
31515
  maxDepth: options.maxDepth,
31519
31516
  maxComplexity: options.maxComplexity
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  resolveRelationField,
40
40
  sql,
41
41
  tracer
42
- } from "./chunk-SW3T2M64.js";
42
+ } from "./chunk-52I54C3X.js";
43
43
 
44
44
  // ../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
45
45
  var require_identity = __commonJS({
@@ -43958,6 +43958,16 @@ function loadEnvFile(filePath) {
43958
43958
  config2({ path: filePath });
43959
43959
  }
43960
43960
 
43961
+ // src/utils/project-version-model.ts
43962
+ init_esm_shims();
43963
+ function loadProjectVersionModel(schema, registry2) {
43964
+ const snapshots = loadVersionSnapshots(schema, registry2);
43965
+ if (!snapshots.ok) return snapshots;
43966
+ const model = computeVersionModel({ current: registry2, snapshots: snapshots.value });
43967
+ if (!model.ok) return model;
43968
+ return { ok: true, value: { snapshots: snapshots.value, model: model.value } };
43969
+ }
43970
+
43961
43971
  // src/commands/build.ts
43962
43972
  function registerBuild(program2) {
43963
43973
  program2.command("build").description("Run codegen and compile project to dist/").option("--env <path>", "path to .env file to load").action(async (options) => {
@@ -44018,6 +44028,11 @@ async function runBuild(options, deps) {
44018
44028
  parsedRoutesDef,
44019
44029
  parsedRoles
44020
44030
  );
44031
+ const versions = loadProjectVersionModel(resolveSchemaConfig(cwd, config3), registry2);
44032
+ if (!versions.ok) {
44033
+ printValidationErrors(versions.errors, "Version model errors", "manguito build");
44034
+ process.exit(1);
44035
+ }
44021
44036
  printSuccess("Config loaded");
44022
44037
  const contentCount = parsedSchemas.filter((s) => s.schema_type === "content-type").length;
44023
44038
  const paragraphCount = parsedSchemas.filter((s) => s.schema_type === "paragraph-type").length;
@@ -44033,19 +44048,8 @@ async function runBuild(options, deps) {
44033
44048
  await generateSchemaRegistry(registry2, generatedDir);
44034
44049
  await generateRoutes2(registry2, generatedDir);
44035
44050
  await generateForms(registry2, join9(generatedDir, "forms"));
44036
- const schema = resolveSchemaConfig(cwd, config3);
44037
- const snapshots = loadVersionSnapshots(schema, registry2);
44038
- if (!snapshots.ok) {
44039
- printValidationErrors(snapshots.errors, "Snapshot errors", "manguito build");
44040
- process.exit(1);
44041
- }
44042
- const versionModel = computeVersionModel({ current: registry2, snapshots: snapshots.value });
44043
- if (!versionModel.ok) {
44044
- printValidationErrors(versionModel.errors, "Version model errors", "manguito build");
44045
- process.exit(1);
44046
- }
44047
- await generateVersionModel(versionModel.value, generatedDir);
44048
- printSuccess(`Version model baked (live: ${versionModel.value.live.join(", ")})`);
44051
+ await generateVersionModel(versions.value.model, generatedDir);
44052
+ printSuccess(`Version model baked (live: ${versions.value.model.live.join(", ")})`);
44049
44053
  const programmaticDir = resolve4(cwd, config3.programmatic.dir);
44050
44054
  const resolverFiles = existsSync3(programmaticDir) ? walkFiles(programmaticDir).filter((f) => /\.(ts|mjs|js)$/.test(f)) : [];
44051
44055
  await generateProgrammaticRegistry(resolverFiles, generatedDir);
@@ -47618,14 +47622,16 @@ function isPlainRow(v2) {
47618
47622
  return typeof v2 === "object" && v2 !== null && !Array.isArray(v2) && !(v2 instanceof Date);
47619
47623
  }
47620
47624
  var MEDIA_FIELD_TYPES = /* @__PURE__ */ new Set(["image", "video", "file"]);
47621
- function nestedTargets(fields) {
47625
+ function nestedTargets(fields, map2) {
47622
47626
  const out = [];
47623
47627
  for (const f of fields) {
47624
47628
  if (MEDIA_FIELD_TYPES.has(f.field_type)) continue;
47625
47629
  if (f.field_type !== "paragraph" && f.field_type !== "reference") continue;
47626
47630
  const ref = f.ui_component.ref;
47627
47631
  if (!ref) continue;
47628
- out.push({ label: f.name, target: ref });
47632
+ const label = isColumnBacked(f) ? map2.labelFor(f.db_column.column_name) : f.name;
47633
+ if (label === void 0) continue;
47634
+ out.push({ label, target: ref });
47629
47635
  }
47630
47636
  return out;
47631
47637
  }
@@ -47643,7 +47649,7 @@ function buildProjectors(registry2, fieldKeyMaps, fallbacks) {
47643
47649
  const typeFallbacks = fallbacks?.[typeName];
47644
47650
  projectors[typeName] = {
47645
47651
  map: map2,
47646
- nested: nestedTargets(type.fields),
47652
+ nested: nestedTargets(type.fields, map2),
47647
47653
  // Spread rather than assign: with exactOptionalPropertyTypes, an optional
47648
47654
  // property may be ABSENT but not present-and-undefined.
47649
47655
  ...typeFallbacks !== void 0 && { fallbacks: typeFallbacks }
@@ -47723,7 +47729,28 @@ function parseFilters(url2, validFields, columnFor) {
47723
47729
  function isPublished(item) {
47724
47730
  return item["published"] === true;
47725
47731
  }
47726
- function registerPublicContentRoutes(app, registry2, repos, projectors, paths, listRateLimit, resolver) {
47732
+ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
47733
+ const forResolver = resolverProjectors ?? projectors;
47734
+ function pickProgrammatic(resolved, names) {
47735
+ const out = {};
47736
+ for (const name of names) if (name in resolved) out[name] = resolved[name];
47737
+ return out;
47738
+ }
47739
+ async function respondItem(row, typeName, programmatic) {
47740
+ const data = projectRow(row, typeName, projectors);
47741
+ if (!resolver?.hasSchema(typeName)) return data;
47742
+ const resolved = await resolver.resolveItem(typeName, projectRow(row, typeName, forResolver));
47743
+ return { ...data, ...pickProgrammatic(resolved, programmatic) };
47744
+ }
47745
+ async function respondList(rows, typeName, programmatic) {
47746
+ const data = rows.map((row) => projectRow(row, typeName, projectors));
47747
+ if (!resolver?.hasSchema(typeName)) return data;
47748
+ const resolved = await resolver.resolveList(
47749
+ typeName,
47750
+ rows.map((row) => projectRow(row, typeName, forResolver))
47751
+ );
47752
+ return data.map((d2, i) => ({ ...d2, ...pickProgrammatic(resolved[i], programmatic) }));
47753
+ }
47727
47754
  function registerListRoute(path7, handler) {
47728
47755
  if (listRateLimit) {
47729
47756
  app.get(path7, listRateLimit, handler);
@@ -47747,18 +47774,29 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47747
47774
  return c.json({ ok: true, data });
47748
47775
  });
47749
47776
  for (const [typeName, contentType] of Object.entries(registry2.content_types)) {
47777
+ let translateInclude2 = function(requested) {
47778
+ const include = [];
47779
+ for (const field of requested) {
47780
+ const registryName = includeNames.get(field);
47781
+ if (registryName === void 0) return { ok: false, field };
47782
+ include.push(registryName);
47783
+ }
47784
+ return { ok: true, include };
47785
+ };
47786
+ var translateInclude = translateInclude2;
47750
47787
  const basePath = contentType.default_base_path;
47751
47788
  const repo = repos[typeName];
47752
47789
  if (!repo) continue;
47753
47790
  const fieldKeys = projectors[typeName].map;
47754
- const relationFieldNames = /* @__PURE__ */ new Set();
47791
+ const programmatic = contentType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
47792
+ const includeNames = /* @__PURE__ */ new Map();
47755
47793
  for (const f of contentType.fields) {
47756
47794
  if (!RELATION_FIELD_TYPES.has(f.field_type)) continue;
47757
47795
  if (isColumnBacked(f)) {
47758
47796
  const label = fieldKeys.labelFor(f.db_column.column_name);
47759
- if (label !== void 0) relationFieldNames.add(label);
47797
+ if (label !== void 0) includeNames.set(label, f.name);
47760
47798
  } else {
47761
- relationFieldNames.add(f.name);
47799
+ includeNames.set(f.name, f.name);
47762
47800
  }
47763
47801
  }
47764
47802
  const filterableFieldNames = /* @__PURE__ */ new Set([
@@ -47777,8 +47815,11 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47777
47815
  404
47778
47816
  );
47779
47817
  }
47780
- let data = projectRow(result.data[0], typeName, projectors);
47781
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
47818
+ const data = await respondItem(
47819
+ result.data[0],
47820
+ typeName,
47821
+ programmatic
47822
+ );
47782
47823
  return c.json({ ok: true, data });
47783
47824
  });
47784
47825
  } else {
@@ -47835,21 +47876,20 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47835
47876
  400
47836
47877
  );
47837
47878
  }
47838
- const include = parseInclude(c.req.query("include"));
47839
- for (const field of include) {
47840
- if (!relationFieldNames.has(field)) {
47841
- return c.json(
47842
- {
47843
- ok: false,
47844
- error: {
47845
- code: "INVALID_INCLUDE_FIELD",
47846
- message: `'${field}' is not a valid relation field`
47847
- }
47848
- },
47849
- 400
47850
- );
47851
- }
47879
+ const translated = translateInclude2(parseInclude(c.req.query("include")));
47880
+ if (!translated.ok) {
47881
+ return c.json(
47882
+ {
47883
+ ok: false,
47884
+ error: {
47885
+ code: "INVALID_INCLUDE_FIELD",
47886
+ message: `'${translated.field}' is not a valid relation field`
47887
+ }
47888
+ },
47889
+ 400
47890
+ );
47852
47891
  }
47892
+ const include = translated.include;
47853
47893
  const sortColumn = fieldKeys.labels.includes(sortBy) ? fieldKeys.columnFor(sortBy) : sortBy;
47854
47894
  const result = await repo.findMany({
47855
47895
  published_only: true,
@@ -47860,29 +47900,25 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47860
47900
  filters: filtersResult.filters,
47861
47901
  include
47862
47902
  });
47863
- const labeled = result.data.map(
47864
- (row) => projectRow(row, typeName, projectors)
47865
- );
47866
- const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
47903
+ const data = await respondList(result.data, typeName, programmatic);
47867
47904
  return c.json({ ...result, data });
47868
47905
  });
47869
47906
  app.get(paths.item(basePath), async (c) => {
47870
47907
  const slug = c.req.param("slug");
47871
- const include = parseInclude(c.req.query("include"));
47872
- for (const field of include) {
47873
- if (!relationFieldNames.has(field)) {
47874
- return c.json(
47875
- {
47876
- ok: false,
47877
- error: {
47878
- code: "INVALID_INCLUDE_FIELD",
47879
- message: `'${field}' is not a valid relation field`
47880
- }
47881
- },
47882
- 400
47883
- );
47884
- }
47908
+ const translated = translateInclude2(parseInclude(c.req.query("include")));
47909
+ if (!translated.ok) {
47910
+ return c.json(
47911
+ {
47912
+ ok: false,
47913
+ error: {
47914
+ code: "INVALID_INCLUDE_FIELD",
47915
+ message: `'${translated.field}' is not a valid relation field`
47916
+ }
47917
+ },
47918
+ 400
47919
+ );
47885
47920
  }
47921
+ const include = translated.include;
47886
47922
  const item = await repo.findBySlug(slug, include);
47887
47923
  if (!item || !isPublished(item)) {
47888
47924
  return c.json(
@@ -47893,15 +47929,15 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47893
47929
  404
47894
47930
  );
47895
47931
  }
47896
- let data = projectRow(item, typeName, projectors);
47897
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
47932
+ const data = await respondItem(item, typeName, programmatic);
47898
47933
  return c.json({ ok: true, data });
47899
47934
  });
47900
47935
  }
47901
47936
  }
47902
- for (const [typeName] of Object.entries(registry2.taxonomy_types)) {
47937
+ for (const [typeName, taxonomyType] of Object.entries(registry2.taxonomy_types)) {
47903
47938
  const repo = repos[typeName];
47904
47939
  if (!repo) continue;
47940
+ const programmatic = taxonomyType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
47905
47941
  registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
47906
47942
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
47907
47943
  if (!pagination.ok) {
@@ -47921,10 +47957,7 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47921
47957
  page: pagination.page,
47922
47958
  per_page: pagination.per_page
47923
47959
  });
47924
- const labeled = result.data.map(
47925
- (row) => projectRow(row, typeName, projectors)
47926
- );
47927
- const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
47960
+ const data = await respondList(result.data, typeName, programmatic);
47928
47961
  return c.json({ ...result, data });
47929
47962
  });
47930
47963
  app.get(paths.taxonomyItem(typeName), async (c) => {
@@ -47939,8 +47972,7 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47939
47972
  404
47940
47973
  );
47941
47974
  }
47942
- let data = projectRow(item, typeName, projectors);
47943
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
47975
+ const data = await respondItem(item, typeName, programmatic);
47944
47976
  return c.json({ ok: true, data });
47945
47977
  });
47946
47978
  }
@@ -49702,7 +49734,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
49702
49734
  }
49703
49735
  var API_VERSION = (() => {
49704
49736
  try {
49705
- return "0.6.0";
49737
+ return "0.7.0";
49706
49738
  } catch {
49707
49739
  return "0.0.0";
49708
49740
  }
@@ -50343,7 +50375,7 @@ function createCmsApp(options) {
50343
50375
  app.use(path7, attachDeprecationHeaders);
50344
50376
  }
50345
50377
  }
50346
- registerPublicContentRoutes(app, registry2, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
50378
+ registerPublicContentRoutes(app, registry2, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver, projectors);
50347
50379
  }
50348
50380
  if (options.versions !== void 0) {
50349
50381
  app.all(`${prefix}/:version/*`, async (c, next) => {
@@ -50385,7 +50417,7 @@ function createCmsApp(options) {
50385
50417
  handler: null,
50386
50418
  error: null
50387
50419
  }));
50388
- const ready = import("./handler-XZVDLKZ2-2TIH4GG7.js").then(({ createGraphQLHandler }) => {
50420
+ const ready = import("./handler-GED3I7Z2-HZAD77OI.js").then(({ createGraphQLHandler }) => {
50389
50421
  gqlPasses.forEach((pass, i) => {
50390
50422
  const mount = mounts[i];
50391
50423
  try {
@@ -50409,7 +50441,8 @@ function createCmsApp(options) {
50409
50441
  endpoint: mount.path,
50410
50442
  currentVersion: model.current,
50411
50443
  ...projection !== void 0 && { projection },
50412
- ...currentProjection !== void 0 && { currentProjection }
50444
+ ...currentProjection !== void 0 && { currentProjection },
50445
+ currentFieldKeyMaps: fieldKeyMaps
50413
50446
  }
50414
50447
  );
50415
50448
  } catch (err) {
@@ -50712,18 +50745,12 @@ async function runDev(options, deps) {
50712
50745
  await generateRoutes2(registry2, manguitoDir);
50713
50746
  await generateForms(registry2, join13(manguitoDir, "forms"));
50714
50747
  await generateNav(registry2, manguitoDir);
50715
- const schemaConfig = resolveSchemaConfig(cwd, config3);
50716
- const snapshotsResult = loadVersionSnapshots(schemaConfig, registry2);
50717
- if (!snapshotsResult.ok) {
50718
- printValidationErrors(snapshotsResult.errors, "Version snapshot errors", "manguito version:diff");
50748
+ const versionsResult = loadProjectVersionModel(resolveSchemaConfig(cwd, config3), registry2);
50749
+ if (!versionsResult.ok) {
50750
+ printValidationErrors(versionsResult.errors, "Version model errors", "manguito validate");
50719
50751
  process.exit(1);
50720
50752
  }
50721
- const versionModelResult = computeVersionModel({ current: registry2, snapshots: snapshotsResult.value });
50722
- if (!versionModelResult.ok) {
50723
- printValidationErrors(versionModelResult.errors, "Version model errors", "manguito version:diff");
50724
- process.exit(1);
50725
- }
50726
- const versionModel = reduceVersionModel(versionModelResult.value);
50753
+ const versionModel = reduceVersionModel(versionsResult.value.model);
50727
50754
  const resolverMap = await loadProgrammaticResolvers(cwd, config3.programmatic.dir);
50728
50755
  const adapter = createCmsApp({
50729
50756
  name: config3.name,
@@ -50862,20 +50889,13 @@ async function onSchemaFileChange(args) {
50862
50889
  await generateRoutes2(registry2, manguitoDir);
50863
50890
  await generateForms(registry2, join13(manguitoDir, "forms"));
50864
50891
  await generateNav(registry2, manguitoDir);
50865
- const schemaConfig = resolveSchemaConfig(cwd, config3);
50866
- const snapshotsResult = loadVersionSnapshots(schemaConfig, registry2);
50867
- if (!snapshotsResult.ok) {
50868
- printValidationErrors(snapshotsResult.errors, "Version snapshot errors", "manguito version:diff");
50892
+ const versionsResult = loadProjectVersionModel(resolveSchemaConfig(cwd, config3), registry2);
50893
+ if (!versionsResult.ok) {
50894
+ printValidationErrors(versionsResult.errors, "Version model errors", "manguito validate");
50869
50895
  process.stderr.write("\u26A0 Changes not applied.\n");
50870
50896
  return;
50871
50897
  }
50872
- const versionModelResult = computeVersionModel({ current: registry2, snapshots: snapshotsResult.value });
50873
- if (!versionModelResult.ok) {
50874
- printValidationErrors(versionModelResult.errors, "Version model errors", "manguito version:diff");
50875
- process.stderr.write("\u26A0 Changes not applied.\n");
50876
- return;
50877
- }
50878
- const versionModel = reduceVersionModel(versionModelResult.value);
50898
+ const versionModel = reduceVersionModel(versionsResult.value.model);
50879
50899
  try {
50880
50900
  await runDevMigration(drizzleConfigPath);
50881
50901
  } catch (err) {
@@ -51333,10 +51353,16 @@ async function runValidate(options, deps) {
51333
51353
  parsedRoutesDef = routesParseResult.value;
51334
51354
  }
51335
51355
  }
51356
+ let liveVersions = null;
51336
51357
  if (allErrors.length === 0 && parsedRoles !== null && parsedRoutesDef !== null) {
51337
51358
  const registry2 = buildSchemaRegistry(parsedSchemas, parsedRoutesDef, parsedRoles);
51338
51359
  const crossRefErrors = validateCrossReferences(registry2, config3.api.media?.max_file_size);
51339
51360
  allErrors.push(...crossRefErrors);
51361
+ if (crossRefErrors.length === 0) {
51362
+ const versions = loadProjectVersionModel(resolveSchemaConfig(cwd, config3), registry2);
51363
+ if (versions.ok) liveVersions = versions.value.model.live;
51364
+ else allErrors.push(...versions.errors);
51365
+ }
51340
51366
  }
51341
51367
  if (allErrors.length > 0) {
51342
51368
  printValidationErrors(allErrors, "Validation errors", "manguito validate");
@@ -51351,6 +51377,7 @@ async function runValidate(options, deps) {
51351
51377
  );
51352
51378
  printSuccess("roles.json valid");
51353
51379
  printSuccess("routes.json valid");
51380
+ if (liveVersions !== null) printSuccess(`Versions valid (live: ${liveVersions.join(", ")})`);
51354
51381
  process.stdout.write("\nNo errors found.\n");
51355
51382
  }
51356
51383
 
@@ -51664,7 +51691,7 @@ function formatField(change) {
51664
51691
  }
51665
51692
  }
51666
51693
  function formatSchemaChange(change) {
51667
- const header = change.from === null ? `Working schema \u2014 nothing has been cut yet, so ${change.to} would be the first version` : `Working schema vs ${change.from} (highest snapshot) \u2014 would become ${change.to}`;
51694
+ const header = change.from === null ? `Working schema \u2014 nothing has been created yet, so ${change.to} would be the first version` : `Working schema vs ${change.from} (highest snapshot) \u2014 would become ${change.to}`;
51668
51695
  if (change.identical) {
51669
51696
  return `${header}
51670
51697
 
@@ -51678,6 +51705,38 @@ No column added, renamed, tombstoned or restored.`;
51678
51705
  });
51679
51706
  return [header, "", ...blocks].join("\n");
51680
51707
  }
51708
+ function driftSummary(change) {
51709
+ const counts = { renamed: 0, tombstoned: 0, added: 0, restored: 0 };
51710
+ for (const type of change.types) {
51711
+ for (const field of type.fields) counts[field.kind]++;
51712
+ }
51713
+ const parts = [];
51714
+ if (counts.renamed > 0) parts.push(`${counts.renamed} renamed`);
51715
+ if (counts.tombstoned > 0) parts.push(`${counts.tombstoned} removed`);
51716
+ if (counts.added > 0) parts.push(`${counts.added} added`);
51717
+ if (counts.restored > 0) parts.push(`${counts.restored} restored`);
51718
+ if (parts.length > 0) return `${parts.join(", ")} since`;
51719
+ if (change.identical) return "identical to current";
51720
+ const newTypes = change.types.filter((t) => t.status === "added").length;
51721
+ return `${newTypes} new type${newTypes === 1 ? "" : "s"} since`;
51722
+ }
51723
+ function formatVersionList(input2) {
51724
+ const { prefix, current, older } = input2;
51725
+ if (older.length === 0) {
51726
+ return `No versions created yet. Your working schema is ${current}, served at ${prefix}/${current} and ${prefix}.
51727
+ Run \`manguito version:create\` before making a breaking change.`;
51728
+ }
51729
+ const rows = [
51730
+ ...older.map((o) => [o.version, `${prefix}/${o.version}`, driftSummary(o.change)]),
51731
+ [current, `${prefix}/${current}`, `current \u2014 also ${prefix}`]
51732
+ ];
51733
+ const nameWidth = Math.max(...rows.map((r) => r[0].length));
51734
+ const pathWidth = Math.max(...rows.map((r) => r[1].length));
51735
+ return [
51736
+ `Live versions (${rows.length}):`,
51737
+ ...rows.map(([name, url2, note]) => ` ${name.padEnd(nameWidth)} ${url2.padEnd(pathWidth)} ${note}`)
51738
+ ].join("\n");
51739
+ }
51681
51740
 
51682
51741
  // src/commands/version-fs.ts
51683
51742
  init_esm_shims();
@@ -51735,17 +51794,19 @@ async function loadVersionContext(options, deps, command) {
51735
51794
  const config3 = await resolveConfig(deps.cwd);
51736
51795
  const schema = resolveSchemaConfig(deps.cwd, config3);
51737
51796
  const registry2 = loadWorkingRegistry(deps.cwd, config3, command);
51738
- const snapshots = loadVersionSnapshots(schema, registry2);
51739
- if (!snapshots.ok) {
51740
- printValidationErrors(snapshots.errors, "Snapshot errors", command);
51797
+ const versions = loadProjectVersionModel(schema, registry2);
51798
+ if (!versions.ok) {
51799
+ printValidationErrors(versions.errors, "Version model errors", command);
51741
51800
  process.exit(1);
51742
51801
  }
51743
- const model = computeVersionModel({ current: registry2, snapshots: snapshots.value });
51744
- if (!model.ok) {
51745
- printValidationErrors(model.errors, "Version model errors", command);
51746
- process.exit(1);
51747
- }
51748
- return { schema, registry: registry2, snapshots: snapshots.value, model: model.value };
51802
+ return {
51803
+ schema,
51804
+ registry: registry2,
51805
+ snapshots: versions.value.snapshots,
51806
+ model: versions.value.model,
51807
+ // Same default `dev` uses: the prefix the served routes actually carry.
51808
+ apiPrefix: config3.api.prefix ?? "/api"
51809
+ };
51749
51810
  }
51750
51811
  function orphanedTombstoneErrors(input2) {
51751
51812
  const remaining = input2.snapshots.filter((s) => s.version !== input2.retiring);
@@ -51753,14 +51814,22 @@ function orphanedTombstoneErrors(input2) {
51753
51814
  if (model.ok) return [];
51754
51815
  return model.errors.filter((e) => e.code === "ORPHANED_TOMBSTONE");
51755
51816
  }
51817
+ var VERSION_CUT_DEPRECATION = "`version:cut` is deprecated \u2014 use `version:create`. It will be removed in a future release.\n";
51756
51818
  function registerVersion(program2) {
51757
- program2.command("version:diff").description("Show what cutting a new version would freeze").option("--env <path>", "path to .env file to load").action(async (options) => {
51819
+ program2.command("version:diff").description("Show what creating a new version would freeze").option("--env <path>", "path to .env file to load").action(async (options) => {
51758
51820
  await runVersionDiff(options, { cwd: process.cwd() });
51759
51821
  });
51760
- program2.command("version:cut").description("Freeze the working schema as a new version").option("--env <path>", "path to .env file to load").option("--yes", "skip the confirmation prompt").action(async (options) => {
51761
- await runVersionCut(options, { cwd: process.cwd(), prompt: createPromptAdapter() });
51822
+ program2.command("version:create").description("Create a version: freeze the working schema so it keeps being served unchanged").option("--env <path>", "path to .env file to load").option("--yes", "skip the confirmation prompt").action(async (options) => {
51823
+ await runVersionCreate(options, { cwd: process.cwd(), prompt: createPromptAdapter() });
51824
+ });
51825
+ program2.command("version:list").description("List the versions being served and how far each is behind the working schema").option("--env <path>", "path to .env file to load").action(async (options) => {
51826
+ await runVersionList(options, { cwd: process.cwd() });
51827
+ });
51828
+ program2.command("version:cut", { hidden: true }).option("--env <path>", "path to .env file to load").option("--yes", "skip the confirmation prompt").action(async (options) => {
51829
+ process.stderr.write(VERSION_CUT_DEPRECATION);
51830
+ await runVersionCreate(options, { cwd: process.cwd(), prompt: createPromptAdapter() });
51762
51831
  });
51763
- program2.command("version:retire <version>").description("Stop serving a cut version and delete its snapshot").option("--env <path>", "path to .env file to load").option("--yes", "skip the confirmation prompt").action(async (version3, options) => {
51832
+ program2.command("version:retire <version>").description("Stop serving a created version and delete its snapshot").option("--env <path>", "path to .env file to load").option("--yes", "skip the confirmation prompt").action(async (version3, options) => {
51764
51833
  await runVersionRetire(version3, options, { cwd: process.cwd(), prompt: createPromptAdapter() });
51765
51834
  });
51766
51835
  }
@@ -51776,15 +51845,15 @@ async function runVersionDiff(options, deps) {
51776
51845
  if (change.identical) {
51777
51846
  process.stdout.write(
51778
51847
  `
51779
- Nothing to cut \u2014 no column was added, renamed, tombstoned or restored, so ${ctx.model.current} would expose the same contract.
51848
+ Nothing to create \u2014 no column was added, renamed, tombstoned or restored, so ${ctx.model.current} would expose the same contract.
51780
51849
  `
51781
51850
  );
51782
51851
  return;
51783
51852
  }
51784
- printSuccess(`Cutting now would create ${ctx.schema.base_path}/versions/${ctx.model.current}/`);
51853
+ printSuccess(`Creating a version now would write ${ctx.schema.base_path}/versions/${ctx.model.current}/`);
51785
51854
  }
51786
- async function runVersionCut(options, deps) {
51787
- const ctx = await loadVersionContext(options, deps, "manguito version:cut");
51855
+ async function runVersionCreate(options, deps) {
51856
+ const ctx = await loadVersionContext(options, deps, "manguito version:create");
51788
51857
  const from = highestSnapshot(ctx.snapshots);
51789
51858
  const version3 = ctx.model.current;
51790
51859
  const change = describeSchemaChange({
@@ -51793,8 +51862,8 @@ async function runVersionCut(options, deps) {
51793
51862
  });
51794
51863
  if (change.identical) {
51795
51864
  printGuidedError(
51796
- `No column was added, renamed, tombstoned or restored since ${from?.version ?? "the last cut"} \u2014 cutting ${version3} would freeze an identical contract.`,
51797
- "Changes to paragraph, programmatic, many-to-many and enum definitions are not versioned and need no new version. A live version commits you to retaining every column it exposes, so cut only when a column actually changed \u2014 or run `manguito version:retire <version>` if you meant to shrink the live set."
51865
+ `No column was added, renamed, tombstoned or restored since ${from?.version ?? "the working schema began"} \u2014 creating ${version3} would freeze an identical contract.`,
51866
+ "Changes to paragraph, programmatic, many-to-many and enum definitions are not versioned and need no new version. A live version commits you to retaining every column it exposes, so create one only when a column actually changed \u2014 or run `manguito version:retire <version>` if you meant to shrink the live set."
51798
51867
  );
51799
51868
  process.exit(1);
51800
51869
  }
@@ -51803,22 +51872,32 @@ async function runVersionCut(options, deps) {
51803
51872
  if (fs6.existsSync(target)) {
51804
51873
  printGuidedError(
51805
51874
  `${target} already exists.`,
51806
- "A snapshot directory is never overwritten. Remove or rename it, then run version:cut again."
51875
+ "A snapshot directory is never overwritten. Remove or rename it, then run version:create again."
51807
51876
  );
51808
51877
  process.exit(1);
51809
51878
  }
51810
51879
  process.stdout.write(`${formatSchemaChange(change)}
51811
51880
 
51812
51881
  `);
51813
- const live = [...ctx.model.live.filter((v2) => v2 !== version3), version3].join(" ");
51882
+ const next = `v${Number.parseInt(version3.slice(1), 10) + 1}`;
51883
+ const liveAfter = [...ctx.model.live.filter((v2) => v2 !== version3), version3, next];
51884
+ if (from === null) {
51885
+ const prefix = ctx.apiPrefix;
51886
+ process.stdout.write(
51887
+ `This creates ${version3} from your working schema. ${prefix}/${version3} keeps serving it
51888
+ unchanged; your working schema becomes ${next}, served at ${prefix}/${next} and ${prefix}.
51889
+
51890
+ `
51891
+ );
51892
+ }
51814
51893
  process.stdout.write(
51815
- `After cutting, ${live} are live. Every column those versions expose must stay in the
51816
- schema \u2014 as a live field or a tombstone \u2014 until you retire them.
51894
+ `After creating ${version3}, these versions are live: ${liveAfter.join(" ")}. Every column they
51895
+ expose must stay in the schema \u2014 as a live field or a tombstone \u2014 until you retire them.
51817
51896
 
51818
51897
  `
51819
51898
  );
51820
51899
  if (options.yes !== true) {
51821
- const ok = await deps.prompt.confirm(`Freeze the working schema as ${version3}?`);
51900
+ const ok = await deps.prompt.confirm(`Create ${version3} from the working schema?`);
51822
51901
  if (!ok) {
51823
51902
  process.stdout.write("Cancelled. Nothing was written.\n");
51824
51903
  return;
@@ -51833,8 +51912,19 @@ schema \u2014 as a live field or a tombstone \u2014 until you retire them.
51833
51912
  );
51834
51913
  process.exit(1);
51835
51914
  }
51836
- printSuccess(`Froze the working schema as ${version3} at ${target}`);
51837
- process.stdout.write(`Live: ${live}. Working schema is now v${Number.parseInt(version3.slice(1), 10) + 1}.
51915
+ printSuccess(`Created ${version3} at ${target}`);
51916
+ process.stdout.write(`Live: ${liveAfter.join(" ")}. Working schema is now ${next}.
51917
+ `);
51918
+ }
51919
+ async function runVersionList(options, deps) {
51920
+ const ctx = await loadVersionContext(options, deps, "manguito version:list");
51921
+ const current = ctx.model.current;
51922
+ const versionNumber3 = (v2) => Number.parseInt(v2.slice(1), 10);
51923
+ const older = [...ctx.snapshots].sort((a2, b2) => versionNumber3(a2.version) - versionNumber3(b2.version)).map((s) => ({
51924
+ version: s.version,
51925
+ change: describeSchemaChange({ from: s, to: { version: current, registry: ctx.registry } })
51926
+ }));
51927
+ process.stdout.write(`${formatVersionList({ prefix: ctx.apiPrefix, current, older })}
51838
51928
  `);
51839
51929
  }
51840
51930
  async function runVersionRetire(version3, options, deps) {
@@ -51846,8 +51936,8 @@ async function runVersionRetire(version3, options, deps) {
51846
51936
  if (!ctx.snapshots.some((s) => s.version === version3)) {
51847
51937
  const existing = ctx.snapshots.map((s) => s.version).join(", ");
51848
51938
  printGuidedError(
51849
- `${version3} is not a cut version.`,
51850
- existing === "" ? "No versions have been cut yet \u2014 run `manguito version:cut` first." : `Cut versions: ${existing}.`
51939
+ `${version3} is not a created version.`,
51940
+ existing === "" ? "No versions have been created yet \u2014 run `manguito version:create` first." : `Created versions: ${existing}.`
51851
51941
  );
51852
51942
  process.exit(1);
51853
51943
  }
@@ -51855,8 +51945,8 @@ async function runVersionRetire(version3, options, deps) {
51855
51945
  if (highest !== null && highest.version === version3) {
51856
51946
  const target = path6.join(ctx.schema.base_path, "versions", version3);
51857
51947
  printGuidedError(
51858
- `${version3} is the newest cut version and cannot be retired.`,
51859
- `The working schema is ${ctx.model.current} because ${version3} is the highest snapshot \u2014 retiring it would renumber the working schema back onto ${version3}, and anyone pinned to ${version3} would get a different contract. Cutting a version with a real contract change makes ${version3} retirable. If ${version3} was cut in error, delete ${target} by hand \u2014 safe only while nothing yet consumes ${version3}, since deleting the newest snapshot renumbers the working schema back onto it.`
51948
+ `${version3} is the newest created version and cannot be retired.`,
51949
+ `The working schema is ${ctx.model.current} because ${version3} is the highest snapshot \u2014 retiring it would renumber the working schema back onto ${version3}, and anyone pinned to ${version3} would get a different contract. Creating a newer version with a real contract change makes ${version3} retirable. If ${version3} was created in error, delete ${target} by hand \u2014 safe only while nothing yet consumes ${version3}, since deleting the newest snapshot renumbers the working schema back onto it.`
51860
51950
  );
51861
51951
  process.exit(1);
51862
51952
  }
@@ -51882,7 +51972,7 @@ longer exposed by any live version. You must then delete these fields:
51882
51972
  `);
51883
51973
  }
51884
51974
  process.stdout.write(
51885
- "Until you do, `manguito validate` will report ORPHANED_TOMBSTONE. Deleting them\nshrinks the union and lets the next migration DROP those columns.\n\n"
51975
+ "Until you do, `manguito validate` and `manguito build` will fail. Deleting them\nshrinks the union and lets the next migration DROP those columns.\n\n"
51886
51976
  );
51887
51977
  }
51888
51978
  if (options.yes !== true) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbykim/manguito-cms-cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "author": "Bobby Kim <bobby.sihun.kim@gmail.com>",
6
6
  "homepage": "https://github.com/bobbykim89/manguito-cms",
@@ -29,10 +29,10 @@
29
29
  "tsup": "^8.5.1",
30
30
  "typescript": "^6.0.3",
31
31
  "vite": "^8.1.3",
32
- "@bobbykim/manguito-cms-api": "0.6.0",
32
+ "@bobbykim/manguito-cms-admin": "0.4.3",
33
+ "@bobbykim/manguito-cms-api": "0.7.0",
33
34
  "@bobbykim/manguito-cms-db": "0.1.6",
34
- "@bobbykim/manguito-cms-core": "0.6.0",
35
- "@bobbykim/manguito-cms-admin": "0.4.3"
35
+ "@bobbykim/manguito-cms-core": "0.6.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.9.5",
@@ -42,7 +42,7 @@
42
42
  "scripts": {
43
43
  "build": "tsup",
44
44
  "dev": "tsup --watch",
45
- "test": "dotenv -e .env.test -- vitest run",
45
+ "test": "dotenv -e ../../.env.test -- vitest run",
46
46
  "lint": "eslint src",
47
47
  "typecheck": "tsc --noEmit"
48
48
  }