eventmodeler 0.6.9 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +229 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1815,8 +1815,8 @@ var require_semver2 = __commonJS((exports, module) => {
1815
1815
  });
1816
1816
 
1817
1817
  // src/index.ts
1818
- import * as fs3 from "node:fs";
1819
- import * as path3 from "node:path";
1818
+ import * as fs5 from "node:fs";
1819
+ import * as path5 from "node:path";
1820
1820
  import { fileURLToPath } from "node:url";
1821
1821
  import { Command } from "commander";
1822
1822
 
@@ -2871,6 +2871,9 @@ var SCOPE_SCHEMAS = {
2871
2871
  notes: NoteSchema,
2872
2872
  flows: FlowSchema
2873
2873
  };
2874
+ function isValidatedScope(scope) {
2875
+ return scope in SCOPE_SCHEMAS;
2876
+ }
2874
2877
  function validateEntry(scope, data) {
2875
2878
  const schema = pickSchema(scope, data);
2876
2879
  const result = schema.safeParse(data);
@@ -3727,6 +3730,9 @@ function resolveType(alias) {
3727
3730
  return;
3728
3731
  return TYPES.find((t) => t.type === canonical);
3729
3732
  }
3733
+ function resolveScope(scope2) {
3734
+ return TYPES.find((t) => t.scope === scope2);
3735
+ }
3730
3736
  function knownTypeAliases() {
3731
3737
  return TYPES.map((t) => t.type);
3732
3738
  }
@@ -3824,6 +3830,10 @@ function registerCreateCommands(program) {
3824
3830
  });
3825
3831
  }
3826
3832
 
3833
+ // src/commands/update.ts
3834
+ import * as fs3 from "node:fs";
3835
+ import * as path3 from "node:path";
3836
+
3827
3837
  // src/lib/resolve-entity.ts
3828
3838
  function resolveEntity(doc, scope2, idKey, nameKey, name, opts = {}) {
3829
3839
  const entries = getEntries(doc, scope2);
@@ -3864,6 +3874,78 @@ function resolveEntity(doc, scope2, idKey, nameKey, name, opts = {}) {
3864
3874
  return { id: String(hit[idKey]), entry: hit };
3865
3875
  }
3866
3876
 
3877
+ // src/lib/model-snapshot.ts
3878
+ var SNAPSHOT_FILENAME = "model.eventmodel";
3879
+ var SNAPSHOT_VERSION = 1;
3880
+ var SNAPSHOT_SCOPES = [...Object.keys(SCOPE_SCHEMAS), "scenarios"];
3881
+ function serializeModel(doc, modelId) {
3882
+ const scopes = {};
3883
+ for (const scope2 of SNAPSHOT_SCOPES) {
3884
+ const map = getScopeMap(doc, scope2);
3885
+ if (map.size === 0)
3886
+ continue;
3887
+ const entries = {};
3888
+ for (const [id, entry] of map.entries()) {
3889
+ entries[id] = entry.toJSON();
3890
+ }
3891
+ scopes[scope2] = entries;
3892
+ }
3893
+ return { version: SNAPSHOT_VERSION, modelId, scopes };
3894
+ }
3895
+ function applyModelSnapshot(doc, modelId, snapshot) {
3896
+ const issues = [];
3897
+ const unknownScopes = [];
3898
+ const writes = [];
3899
+ for (const [scope2, entries] of Object.entries(snapshot.scopes ?? {})) {
3900
+ if (!SNAPSHOT_SCOPES.includes(scope2)) {
3901
+ unknownScopes.push(scope2);
3902
+ continue;
3903
+ }
3904
+ const map = getScopeMap(doc, scope2);
3905
+ const validated = isValidatedScope(scope2);
3906
+ const meta = resolveScope(scope2);
3907
+ for (const [id, rawEntry] of Object.entries(entries)) {
3908
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
3909
+ issues.push(`${scope2}/${id}: entry must be a JSON object`);
3910
+ continue;
3911
+ }
3912
+ let finalEntry;
3913
+ if (validated) {
3914
+ const res = validateEntry(scope2, rawEntry);
3915
+ if (!res.ok) {
3916
+ for (const issue of res.issues)
3917
+ issues.push(`${scope2}/${id}: ${issue}`);
3918
+ continue;
3919
+ }
3920
+ finalEntry = { ...res.data, modelId };
3921
+ if (meta)
3922
+ finalEntry[meta.idKey] = id;
3923
+ } else {
3924
+ finalEntry = { ...rawEntry };
3925
+ }
3926
+ writes.push({ scope: scope2, id, entry: finalEntry, isNew: !map.has(id) });
3927
+ }
3928
+ }
3929
+ if (issues.length)
3930
+ return { ok: false, issues };
3931
+ const perScope = {};
3932
+ doc.transact(() => {
3933
+ for (const w of writes) {
3934
+ const map = getScopeMap(doc, w.scope);
3935
+ map.delete(w.id);
3936
+ map.set(w.id, deepToY(w.entry));
3937
+ const counts = perScope[w.scope] ??= { created: 0, updated: 0 };
3938
+ if (w.isNew)
3939
+ counts.created++;
3940
+ else
3941
+ counts.updated++;
3942
+ }
3943
+ });
3944
+ const created = Object.values(perScope).reduce((a, s) => a + s.created, 0);
3945
+ const updated = Object.values(perScope).reduce((a, s) => a + s.updated, 0);
3946
+ return { ok: true, result: { created, updated, perScope, unknownScopes } };
3947
+ }
3948
+
3867
3949
  // src/commands/update.ts
3868
3950
  var UPDATE_HELP = `
3869
3951
  Replace an existing element's entry with a new JSON shape. The new JSON is
@@ -3876,10 +3958,13 @@ Pattern (read-modify-write):
3876
3958
  | jq '.fields += [{"name":"customerId","fieldType":"UUID","isList":false,"isGenerated":false}]')
3877
3959
  eventmodeler update event "OrderPlaced" "$NEW"
3878
3960
 
3879
- Linked-copy entries cannot be updated. They share the original's name and
3880
- fields by design edit the original and all copies pick up the change. The
3881
- CLI refuses both name-based updates (ambiguous with the original) and
3882
- --id <copy-uuid> updates (would diverge from the original).
3961
+ Linked-copy entries can be repositioned/resized but never re-identified. A
3962
+ copy shares its original's name and fields by design, so an update to a copy
3963
+ writes geometry (x, y, width, height) only any name/field keys in the
3964
+ payload are ignored. To change a copy's name or fields, edit the original and
3965
+ all copies pick up the change. Address a copy by uuid:
3966
+
3967
+ eventmodeler --id <copy-uuid> update event '{"x":120,"y":340}'
3883
3968
 
3884
3969
  Flows have no addressable name and must be updated via uuid:
3885
3970
 
@@ -3893,9 +3978,82 @@ semantics); to preserve it, round-trip via \`show screen ... | jq\`.
3893
3978
 
3894
3979
  This is a *replace*, not a patch: keys present in the previous entry but
3895
3980
  absent in the new JSON are removed.
3981
+
3982
+ Whole-model upsert:
3983
+ eventmodeler update model [file]
3984
+
3985
+ Reads a snapshot file (default ${SNAPSHOT_FILENAME}; see \`eventmodeler
3986
+ snapshot\`) and upserts every entry by id — existing ids are replaced, new
3987
+ ids created, and entries absent from the file are left untouched (upsert, not
3988
+ sync; nothing is deleted). Validation is all-or-nothing: if any entry fails,
3989
+ nothing is written.
3896
3990
  `;
3991
+ var GEOMETRY_KEYS = ["x", "y", "width", "height"];
3992
+ function extractGeometry(parsed) {
3993
+ if (!parsed || typeof parsed !== "object") {
3994
+ return { ok: false, values: {}, issues: ["payload must be a JSON object"] };
3995
+ }
3996
+ const obj = parsed;
3997
+ const values = {};
3998
+ const issues = [];
3999
+ for (const k of GEOMETRY_KEYS) {
4000
+ const v = obj[k];
4001
+ if (v === undefined)
4002
+ continue;
4003
+ if (typeof v !== "number" || !Number.isFinite(v)) {
4004
+ issues.push(`${k} must be a finite number`);
4005
+ continue;
4006
+ }
4007
+ if ((k === "width" || k === "height") && v <= 0) {
4008
+ issues.push(`${k} must be positive`);
4009
+ continue;
4010
+ }
4011
+ values[k] = v;
4012
+ }
4013
+ if (issues.length === 0 && Object.keys(values).length === 0) {
4014
+ issues.push("no geometry fields (x, y, width, height) present to update");
4015
+ }
4016
+ return { ok: issues.length === 0, values, issues };
4017
+ }
4018
+ async function runModelUpdate(args, opts) {
4019
+ const file = args[0] ?? SNAPSHOT_FILENAME;
4020
+ const filePath = path3.resolve(process.cwd(), file);
4021
+ if (!fs3.existsSync(filePath)) {
4022
+ console.error(`Snapshot file not found: ${file}. Run \`eventmodeler snapshot\` first.`);
4023
+ process.exit(2);
4024
+ }
4025
+ let snapshot;
4026
+ try {
4027
+ snapshot = JSON.parse(fs3.readFileSync(filePath, "utf8"));
4028
+ } catch (err) {
4029
+ console.error(`Invalid JSON in ${file}: ${err.message}`);
4030
+ process.exit(2);
4031
+ }
4032
+ if (!snapshot || typeof snapshot !== "object" || typeof snapshot.scopes !== "object") {
4033
+ console.error(`${file} is not a model snapshot (missing "scopes"). Run \`eventmodeler snapshot\` to generate one.`);
4034
+ process.exit(2);
4035
+ }
4036
+ const modelId = resolveModelId(opts.model);
4037
+ const outcome = await withDoc(modelId, (doc) => applyModelSnapshot(doc, modelId, snapshot));
4038
+ if (!outcome.ok) {
4039
+ console.error(`Model update aborted — ${outcome.issues.length} validation error(s), nothing written:`);
4040
+ for (const issue of outcome.issues)
4041
+ console.error(` - ${issue}`);
4042
+ process.exit(2);
4043
+ }
4044
+ const { created, updated, perScope, unknownScopes } = outcome.result;
4045
+ const breakdown = Object.entries(perScope).map(([scope2, c]) => `${scope2} +${c.created}/~${c.updated}`).join(", ");
4046
+ console.log(`Model updated — ${created} created, ${updated} updated${breakdown ? ` (${breakdown})` : ""}.`);
4047
+ if (unknownScopes.length) {
4048
+ console.error(`Ignored unknown scope(s): ${unknownScopes.join(", ")}`);
4049
+ }
4050
+ }
3897
4051
  function registerUpdateCommands(program) {
3898
- program.command("update <type> <args...>").description(`Replace an element's shape with JSON. Types: ${knownTypeAliases().join(", ")}`).option("--model <id>", "Model id (defaults to .eventmodeler.json in cwd)").addHelpText("after", UPDATE_HELP).action(async (type, args, opts) => {
4052
+ program.command("update <type> [args...]").description(`Replace an element's shape with JSON (or 'model' for whole-model upsert). Types: ${knownTypeAliases().join(", ")}`).option("--model <id>", "Model id (defaults to .eventmodeler.json in cwd)").addHelpText("after", UPDATE_HELP).action(async (type, args, opts) => {
4053
+ if (type === "model") {
4054
+ await runModelUpdate(args, opts);
4055
+ return;
4056
+ }
3899
4057
  const meta = resolveType(type);
3900
4058
  if (!meta) {
3901
4059
  console.error(`Unknown type "${type}". Known: ${knownTypeAliases().join(", ")}`);
@@ -3933,32 +4091,45 @@ function registerUpdateCommands(program) {
3933
4091
  if (note)
3934
4092
  console.error(note);
3935
4093
  }
3936
- const validation = validateEntry(meta.scope, parsed);
3937
- if (!validation.ok) {
3938
- console.error(`Validation failed for ${meta.type}:`);
3939
- for (const issue of validation.issues)
3940
- console.error(` - ${issue}`);
3941
- process.exit(2);
3942
- }
3943
4094
  const modelId = resolveModelId(opts.model);
3944
4095
  await withDoc(modelId, (doc) => {
3945
4096
  const { id, entry } = resolveEntity(doc, meta.scope, meta.idKey, meta.nameKey, name, {
3946
4097
  idOverride,
3947
4098
  includeLinkedCopies: Boolean(idOverride)
3948
4099
  });
4100
+ const map = getScopeMap(doc, meta.scope);
3949
4101
  if (entry.isLinkedCopy) {
3950
- const originalId = meta.originalIdKey ? entry[meta.originalIdKey] : undefined;
3951
- console.error(`Cannot update ${meta.type} ${id} — it is a linked copy. ` + `Linked copies share their original's name and fields; edit the original instead` + (originalId ? ` (--id ${originalId}).` : "."));
4102
+ const geom = extractGeometry(parsed);
4103
+ if (!geom.ok) {
4104
+ console.error(`Cannot update ${meta.type} ${id} (linked copy):`);
4105
+ for (const issue of geom.issues)
4106
+ console.error(` - ${issue}`);
4107
+ process.exit(2);
4108
+ }
4109
+ doc.transact(() => {
4110
+ const e = map.get(id);
4111
+ if (!e)
4112
+ return;
4113
+ for (const [k, v] of Object.entries(geom.values))
4114
+ e.set(k, v);
4115
+ });
4116
+ console.log(`Updated ${meta.type} geometry (linked copy — name and fields stay tied to the original).`);
4117
+ return;
4118
+ }
4119
+ const validation = validateEntry(meta.scope, parsed);
4120
+ if (!validation.ok) {
4121
+ console.error(`Validation failed for ${meta.type}:`);
4122
+ for (const issue of validation.issues)
4123
+ console.error(` - ${issue}`);
3952
4124
  process.exit(2);
3953
4125
  }
3954
- const map = getScopeMap(doc, meta.scope);
3955
4126
  const finalEntry = { ...validation.data, [meta.idKey]: id, modelId };
3956
4127
  doc.transact(() => {
3957
4128
  map.delete(id);
3958
4129
  map.set(id, deepToY(finalEntry));
3959
4130
  });
4131
+ console.log(`Updated ${meta.type}.`);
3960
4132
  });
3961
- console.log(`Updated ${meta.type}.`);
3962
4133
  });
3963
4134
  }
3964
4135
 
@@ -4037,6 +4208,37 @@ function removeGroup(doc, meta, originalId) {
4037
4208
  return { copies, flows };
4038
4209
  }
4039
4210
 
4211
+ // src/commands/snapshot.ts
4212
+ import * as fs4 from "node:fs";
4213
+ import * as path4 from "node:path";
4214
+ var SNAPSHOT_HELP = `
4215
+ Write the entire model to a single JSON file (default ${SNAPSHOT_FILENAME})
4216
+ in the current directory. The file captures every scope — stickies,
4217
+ containers, flows and scenarios — keyed by id, so it is a faithful, editable
4218
+ copy of the whole model.
4219
+
4220
+ Workflow (snapshot → edit → upsert):
4221
+ eventmodeler snapshot # writes ${SNAPSHOT_FILENAME}
4222
+ $EDITOR ${SNAPSHOT_FILENAME} # diff/edit the whole model
4223
+ eventmodeler update model # upserts your edits back
4224
+
4225
+ The file is plain JSON. Each scope is an object of { "<id>": { ...entry } };
4226
+ the id is the map key, so renaming/adding entries means editing those keys.
4227
+ `;
4228
+ function registerSnapshotCommands(program) {
4229
+ program.command("snapshot").description(`Save the entire model as JSON to ${SNAPSHOT_FILENAME} for offline editing`).option("--model <id>", "Model id (defaults to .eventmodeler.json in cwd)").option("--out <file>", `Output file (default ${SNAPSHOT_FILENAME})`).addHelpText("after", SNAPSHOT_HELP).action(async (opts) => {
4230
+ const modelId = resolveModelId(opts.model);
4231
+ const snapshot = await withDoc(modelId, (doc) => serializeModel(doc, modelId));
4232
+ const outPath = path4.resolve(process.cwd(), opts.out ?? SNAPSHOT_FILENAME);
4233
+ fs4.writeFileSync(outPath, `${JSON.stringify(snapshot, null, 2)}
4234
+ `, "utf8");
4235
+ const scopeCount = Object.keys(snapshot.scopes).length;
4236
+ const total = Object.values(snapshot.scopes).reduce((a, s) => a + Object.keys(s).length, 0);
4237
+ const rel = path4.relative(process.cwd(), outPath) || outPath;
4238
+ console.log(`Wrote ${total} element(s) across ${scopeCount} scope(s) to ${rel}.`);
4239
+ });
4240
+ }
4241
+
4040
4242
  // src/lib/update-check.ts
4041
4243
  var import_semver = __toESM(require_semver2(), 1);
4042
4244
  import { spawn } from "node:child_process";
@@ -4097,28 +4299,28 @@ async function fetchLatestVersion() {
4097
4299
  }
4098
4300
  }
4099
4301
  function promptYesNo(question) {
4100
- return new Promise((resolve2) => {
4302
+ return new Promise((resolve4) => {
4101
4303
  const rl = readline2.createInterface({
4102
4304
  input: process.stdin,
4103
4305
  output: process.stdout
4104
4306
  });
4105
4307
  const timeout = setTimeout(() => {
4106
4308
  rl.close();
4107
- resolve2(false);
4309
+ resolve4(false);
4108
4310
  }, PROMPT_TIMEOUT_MS);
4109
4311
  rl.question(question, (answer) => {
4110
4312
  clearTimeout(timeout);
4111
4313
  rl.close();
4112
- resolve2(["y", "yes"].includes(answer.trim().toLowerCase()));
4314
+ resolve4(["y", "yes"].includes(answer.trim().toLowerCase()));
4113
4315
  });
4114
4316
  });
4115
4317
  }
4116
4318
  function runUpdate() {
4117
4319
  const [command, args] = updateCommand(detectPackageManager());
4118
- return new Promise((resolve2) => {
4320
+ return new Promise((resolve4) => {
4119
4321
  const child = spawn(command, args, { stdio: "inherit" });
4120
- child.on("close", (code) => resolve2(code ?? 1));
4121
- child.on("error", () => resolve2(1));
4322
+ child.on("close", (code) => resolve4(code ?? 1));
4323
+ child.on("error", () => resolve4(1));
4122
4324
  });
4123
4325
  }
4124
4326
  async function maybeUpdateCli(packageName, currentVersion, argv) {
@@ -4159,8 +4361,8 @@ async function maybeUpdateCli(packageName, currentVersion, argv) {
4159
4361
 
4160
4362
  // src/index.ts
4161
4363
  var __filename2 = fileURLToPath(import.meta.url);
4162
- var __dirname2 = path3.dirname(__filename2);
4163
- var packageJson = JSON.parse(fs3.readFileSync(path3.join(__dirname2, "..", "package.json"), "utf-8"));
4364
+ var __dirname2 = path5.dirname(__filename2);
4365
+ var packageJson = JSON.parse(fs5.readFileSync(path5.join(__dirname2, "..", "package.json"), "utf-8"));
4164
4366
  var program = new Command().name("eventmodeler").version(packageJson.version).description("CLI tool for interacting with Event Model files").option("--id <uuid>", "Skip name resolution, use UUID directly").option("--no-update-check", "Skip checking npm for a newer CLI version");
4165
4367
  setProgram(program);
4166
4368
  registerAuthCommands(program);
@@ -4171,6 +4373,7 @@ registerSummaryCommands(program);
4171
4373
  registerCreateCommands(program);
4172
4374
  registerUpdateCommands(program);
4173
4375
  registerRemoveCommands(program);
4376
+ registerSnapshotCommands(program);
4174
4377
  await maybeUpdateCli(packageJson.name, packageJson.version, process.argv);
4175
4378
  program.parseAsync(process.argv).catch((err) => {
4176
4379
  console.error("Error:", err.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eventmodeler",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "CLI tool for event modeling - explore, design, and generate code from your event models",
5
5
  "type": "module",
6
6
  "repository": {