eventmodeler 0.2.1 → 0.2.3

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 (34) hide show
  1. package/README.md +175 -0
  2. package/dist/index.js +51 -19
  3. package/dist/lib/config.d.ts +2 -0
  4. package/dist/lib/config.js +26 -0
  5. package/dist/lib/format.d.ts +3 -0
  6. package/dist/lib/format.js +11 -0
  7. package/dist/projection.js +29 -35
  8. package/dist/slices/list-chapters/index.d.ts +2 -1
  9. package/dist/slices/list-chapters/index.js +10 -11
  10. package/dist/slices/list-commands/index.d.ts +2 -1
  11. package/dist/slices/list-commands/index.js +9 -10
  12. package/dist/slices/list-events/index.d.ts +2 -1
  13. package/dist/slices/list-events/index.js +33 -13
  14. package/dist/slices/list-slices/index.d.ts +2 -1
  15. package/dist/slices/list-slices/index.js +9 -10
  16. package/dist/slices/search/index.d.ts +2 -1
  17. package/dist/slices/search/index.js +148 -21
  18. package/dist/slices/show-actor/index.d.ts +3 -2
  19. package/dist/slices/show-actor/index.js +44 -9
  20. package/dist/slices/show-aggregate-completeness/index.d.ts +3 -2
  21. package/dist/slices/show-aggregate-completeness/index.js +60 -9
  22. package/dist/slices/show-chapter/index.d.ts +2 -1
  23. package/dist/slices/show-chapter/index.js +9 -9
  24. package/dist/slices/show-command/index.d.ts +2 -1
  25. package/dist/slices/show-command/index.js +52 -9
  26. package/dist/slices/show-completeness/index.d.ts +3 -1
  27. package/dist/slices/show-completeness/index.js +225 -32
  28. package/dist/slices/show-event/index.d.ts +2 -1
  29. package/dist/slices/show-event/index.js +41 -9
  30. package/dist/slices/show-model-summary/index.d.ts +2 -1
  31. package/dist/slices/show-model-summary/index.js +18 -9
  32. package/dist/slices/show-slice/index.d.ts +2 -1
  33. package/dist/slices/show-slice/index.js +162 -9
  34. package/package.json +5 -3
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # eventmodeler
2
+
3
+ CLI tool for interacting with [Event Model](https://eventmodeling.org) files. Query, update, and export event models from the terminal.
4
+
5
+ Works with `.eventmodel` files created by the [Event Modeling App](https://www.eventmodeling.app).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g eventmodeler
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Open Event Modeling app in browser
17
+ eventmodeler
18
+
19
+ # List all slices
20
+ eventmodeler list slices
21
+
22
+ # Show details of a slice
23
+ eventmodeler show slice "Place Order"
24
+
25
+ # Search for elements
26
+ eventmodeler search "order"
27
+
28
+ # Get JSON output instead of XML
29
+ eventmodeler list slices --format json
30
+ ```
31
+
32
+ ## Commands
33
+
34
+ ### List Commands
35
+
36
+ ```bash
37
+ eventmodeler list slices # List all slices with status
38
+ eventmodeler list events # List all events
39
+ eventmodeler list commands # List all commands
40
+ eventmodeler list chapters # List all chapters
41
+ eventmodeler list aggregates # List all aggregates
42
+ eventmodeler list actors # List all actors
43
+ ```
44
+
45
+ ### Show Commands
46
+
47
+ ```bash
48
+ eventmodeler show slice <name> # Show detailed view of a slice
49
+ eventmodeler show event <name> # Show detailed view of an event
50
+ eventmodeler show command <name> # Show detailed view of a command
51
+ eventmodeler show chapter <name> # Show chapter with its slices
52
+ eventmodeler show actor <name> # Show actor with its screens
53
+ ```
54
+
55
+ ### Completeness Commands
56
+
57
+ ```bash
58
+ eventmodeler show completeness <name> # Show field mapping completeness for any element
59
+ eventmodeler show model-completeness # Show completeness of all flows in the model
60
+ eventmodeler show aggregate-completeness <name> # Check if events have the aggregate ID field
61
+ ```
62
+
63
+ ### Search
64
+
65
+ ```bash
66
+ eventmodeler search <term> # Search for entities by name
67
+ ```
68
+
69
+ ### Modify Commands
70
+
71
+ ```bash
72
+ # Mark slice status
73
+ eventmodeler mark "Place Order" done
74
+ eventmodeler mark "Cancel Order" in-progress
75
+
76
+ # Add scenario to a slice
77
+ eventmodeler add scenario --slice "Place Order" --json '{"name": "Happy path", ...}'
78
+
79
+ # Add field to an entity
80
+ eventmodeler add field --event "OrderPlaced" --json '{"name": "orderId", "type": "UUID"}'
81
+
82
+ # Remove field
83
+ eventmodeler remove field --event "OrderPlaced" --field "legacyId"
84
+
85
+ # Update field properties
86
+ eventmodeler update field --read-model "OrderSummary" --field "notes" --optional true
87
+
88
+ # Map fields between elements
89
+ eventmodeler map fields --flow "OrderPlaced→OrderSummary" --json '[{"from": "total", "to": "totalAmount"}]'
90
+ ```
91
+
92
+ ### Export
93
+
94
+ ```bash
95
+ eventmodeler summary # Show model summary statistics
96
+ eventmodeler export json # Export entire model as JSON
97
+ ```
98
+
99
+ ## Output Format
100
+
101
+ By default, output is in XML format (optimized for AI agents). Use `--format json` for JSON output:
102
+
103
+ ```bash
104
+ eventmodeler list slices --format json
105
+ eventmodeler show slice "Place Order" --format json
106
+ ```
107
+
108
+ ### Setting Default Format
109
+
110
+ Set your preferred default format via:
111
+
112
+ **Environment variable:**
113
+ ```bash
114
+ export EVENTMODELER_FORMAT=json
115
+ ```
116
+
117
+ **Config file** (`~/.eventmodeler/config.json`):
118
+ ```json
119
+ {
120
+ "format": "json"
121
+ }
122
+ ```
123
+
124
+ Priority: CLI flag > environment variable > config file > default (xml)
125
+
126
+ ## Options
127
+
128
+ ```
129
+ -f, --file <path> Path to .eventmodel file (default: auto-detect)
130
+ --format <xml|json> Output format (default: xml)
131
+ -h, --help Show help message
132
+ -v, --version Show version number
133
+ ```
134
+
135
+ ## Use Cases
136
+
137
+ ### For AI Agents
138
+
139
+ The CLI is designed to work well with AI coding assistants. The XML output format provides structured, readable data that AI agents can easily parse and understand:
140
+
141
+ ```bash
142
+ eventmodeler show slice "Place Order"
143
+ ```
144
+
145
+ ### For Code Generators
146
+
147
+ Use JSON output to build code generators that read your event model:
148
+
149
+ ```bash
150
+ eventmodeler list events --format json | your-codegen-tool
151
+ eventmodeler show slice "Place Order" --format json
152
+ ```
153
+
154
+ ### For CI/CD
155
+
156
+ Check model completeness in your pipeline:
157
+
158
+ ```bash
159
+ eventmodeler show model-completeness --format json
160
+ ```
161
+
162
+ ## Requirements
163
+
164
+ - Node.js >= 18.0.0
165
+ - A `.eventmodel` file in the current directory (or specify with `-f`)
166
+
167
+ ## License
168
+
169
+ MIT
170
+
171
+ ## Links
172
+
173
+ - [Event Modeling App](https://www.eventmodeling.app)
174
+ - [Event Modeling](https://eventmodeling.org)
175
+ - [GitHub Repository](https://github.com/theoema/event-modeler)
package/dist/index.js CHANGED
@@ -1,6 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
3
5
  import { findEventModelFile, loadModel } from './lib/file-loader.js';
6
+ import { getDefaultFormat } from './lib/config.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
10
+ const VERSION = packageJson.version;
4
11
  // Import slices
5
12
  import { listSlices } from './slices/list-slices/index.js';
6
13
  import { listEvents } from './slices/list-events/index.js';
@@ -19,7 +26,7 @@ import { addScenario } from './slices/add-scenario/index.js';
19
26
  import { addField } from './slices/add-field/index.js';
20
27
  import { removeScenario } from './slices/remove-scenario/index.js';
21
28
  import { removeField } from './slices/remove-field/index.js';
22
- import { showCompleteness } from './slices/show-completeness/index.js';
29
+ import { showCompleteness, showModelCompleteness } from './slices/show-completeness/index.js';
23
30
  import { mapFields } from './slices/map-fields/index.js';
24
31
  import { updateField } from './slices/update-field/index.js';
25
32
  import { showAggregateCompleteness, listAggregates } from './slices/show-aggregate-completeness/index.js';
@@ -53,7 +60,8 @@ COMMANDS:
53
60
  show event <name> Show detailed XML view of an event
54
61
  show command <name> Show detailed XML view of a command
55
62
  show chapter <name> Show chapter with its slices
56
- show completeness <read-model> Show field mapping completeness status
63
+ show completeness <name> Show field mapping completeness for any element
64
+ show model-completeness Show completeness of all flows in the model
57
65
  show aggregate-completeness <name>
58
66
  Show if events in aggregate have the ID field
59
67
  show actor <name> Show actor with its screens
@@ -85,7 +93,14 @@ COMMANDS:
85
93
 
86
94
  OPTIONS:
87
95
  -f, --file <path> Path to .eventmodel file (default: auto-detect)
96
+ --format <xml|json> Output format (default: xml, or from config)
88
97
  -h, --help Show this help message
98
+ -v, --version Show version number
99
+
100
+ CONFIGURATION:
101
+ Set default output format via:
102
+ - Environment variable: EVENTMODELER_FORMAT=json
103
+ - Config file: ~/.eventmodeler/config.json with {"format": "json"}
89
104
 
90
105
  EXAMPLES:
91
106
  eventmodeler list slices
@@ -95,25 +110,38 @@ EXAMPLES:
95
110
  eventmodeler add field --event "OrderPlaced" --json '{"name": "orderId", "type": "UUID"}'
96
111
  eventmodeler remove field --event "OrderPlaced" --field "orderId"
97
112
  eventmodeler show completeness "OrderSummary"
113
+ eventmodeler show model-completeness
98
114
  eventmodeler map fields --flow "OrderPlaced→OrderSummary" --json '[{"from": "total", "to": "totalAmount"}]'
99
115
  eventmodeler update field --read-model "OrderSummary" --field "notes" --optional true
100
116
  `);
101
117
  }
102
118
  async function main() {
103
119
  let fileArg = null;
120
+ let formatArg = null;
104
121
  const filteredArgs = [];
105
122
  for (let i = 0; i < args.length; i++) {
106
123
  if (args[i] === '-f' || args[i] === '--file') {
107
124
  fileArg = args[++i];
108
125
  }
126
+ else if (args[i] === '--format') {
127
+ formatArg = args[++i];
128
+ }
109
129
  else if (args[i] === '-h' || args[i] === '--help') {
110
130
  printHelp();
111
131
  process.exit(0);
112
132
  }
133
+ else if (args[i] === '-v' || args[i] === '--version') {
134
+ console.log(`eventmodeler ${VERSION}`);
135
+ process.exit(0);
136
+ }
113
137
  else {
114
138
  filteredArgs.push(args[i]);
115
139
  }
116
140
  }
141
+ // Determine output format
142
+ const format = formatArg === 'json' ? 'json'
143
+ : formatArg === 'xml' ? 'xml'
144
+ : getDefaultFormat();
117
145
  const command = filteredArgs[0];
118
146
  const subcommand = filteredArgs[1];
119
147
  const target = filteredArgs[2];
@@ -136,22 +164,22 @@ async function main() {
136
164
  case 'list':
137
165
  switch (subcommand) {
138
166
  case 'slices':
139
- listSlices(model);
167
+ listSlices(model, format);
140
168
  break;
141
169
  case 'events':
142
- listEvents(model);
170
+ listEvents(model, format);
143
171
  break;
144
172
  case 'commands':
145
- listCommands(model);
173
+ listCommands(model, format);
146
174
  break;
147
175
  case 'chapters':
148
- listChapters(model);
176
+ listChapters(model, format);
149
177
  break;
150
178
  case 'aggregates':
151
- listAggregates(model);
179
+ listAggregates(model, format);
152
180
  break;
153
181
  case 'actors':
154
- listActors(model);
182
+ listActors(model, format);
155
183
  break;
156
184
  default:
157
185
  console.error(`Unknown list target: ${subcommand}`);
@@ -166,53 +194,57 @@ async function main() {
166
194
  console.error('Usage: eventmodeler show slice <name>');
167
195
  process.exit(1);
168
196
  }
169
- showSlice(model, target);
197
+ showSlice(model, target, format);
170
198
  break;
171
199
  case 'event':
172
200
  if (!target) {
173
201
  console.error('Usage: eventmodeler show event <name>');
174
202
  process.exit(1);
175
203
  }
176
- showEvent(model, target);
204
+ showEvent(model, target, format);
177
205
  break;
178
206
  case 'command':
179
207
  if (!target) {
180
208
  console.error('Usage: eventmodeler show command <name>');
181
209
  process.exit(1);
182
210
  }
183
- showCommand(model, target);
211
+ showCommand(model, target, format);
184
212
  break;
185
213
  case 'chapter':
186
214
  if (!target) {
187
215
  console.error('Usage: eventmodeler show chapter <name>');
188
216
  process.exit(1);
189
217
  }
190
- showChapter(model, target);
218
+ showChapter(model, target, format);
191
219
  break;
192
220
  case 'completeness':
193
221
  if (!target) {
194
- console.error('Usage: eventmodeler show completeness <read-model-name>');
222
+ console.error('Usage: eventmodeler show completeness <element-name>');
223
+ console.error('Searches: commands, events, read models, screens, processors');
195
224
  process.exit(1);
196
225
  }
197
- showCompleteness(model, target);
226
+ showCompleteness(model, target, format);
227
+ break;
228
+ case 'model-completeness':
229
+ showModelCompleteness(model, format);
198
230
  break;
199
231
  case 'aggregate-completeness':
200
232
  if (!target) {
201
233
  console.error('Usage: eventmodeler show aggregate-completeness <aggregate-name>');
202
234
  process.exit(1);
203
235
  }
204
- showAggregateCompleteness(model, target);
236
+ showAggregateCompleteness(model, target, format);
205
237
  break;
206
238
  case 'actor':
207
239
  if (!target) {
208
240
  console.error('Usage: eventmodeler show actor <actor-name>');
209
241
  process.exit(1);
210
242
  }
211
- showActor(model, target);
243
+ showActor(model, target, format);
212
244
  break;
213
245
  default:
214
246
  console.error(`Unknown show target: ${subcommand}`);
215
- console.error('Valid targets: slice, event, command, chapter, completeness, aggregate-completeness, actor');
247
+ console.error('Valid targets: slice, event, command, chapter, completeness, model-completeness, aggregate-completeness, actor');
216
248
  process.exit(1);
217
249
  }
218
250
  break;
@@ -221,7 +253,7 @@ async function main() {
221
253
  console.error('Usage: eventmodeler search <term>');
222
254
  process.exit(1);
223
255
  }
224
- search(model, subcommand);
256
+ search(model, subcommand, format);
225
257
  break;
226
258
  case 'mark':
227
259
  if (!subcommand || !target) {
@@ -232,7 +264,7 @@ async function main() {
232
264
  markSliceStatus(model, filePath, subcommand, target);
233
265
  break;
234
266
  case 'summary':
235
- showModelSummary(model);
267
+ showModelSummary(model, format);
236
268
  break;
237
269
  case 'export':
238
270
  switch (subcommand) {
@@ -0,0 +1,2 @@
1
+ import type { OutputFormat } from './format.js';
2
+ export declare function getDefaultFormat(): OutputFormat;
@@ -0,0 +1,26 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ export function getDefaultFormat() {
5
+ // 1. Check environment variable
6
+ const envFormat = process.env.EVENTMODELER_FORMAT;
7
+ if (envFormat === 'json')
8
+ return 'json';
9
+ if (envFormat === 'xml')
10
+ return 'xml';
11
+ // 2. Check config file
12
+ try {
13
+ const configPath = path.join(os.homedir(), '.eventmodeler', 'config.json');
14
+ if (fs.existsSync(configPath)) {
15
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
16
+ if (config.format === 'json' || config.format === 'xml') {
17
+ return config.format;
18
+ }
19
+ }
20
+ }
21
+ catch {
22
+ // Ignore config file errors
23
+ }
24
+ // 3. Default to XML (better for AI agents)
25
+ return 'xml';
26
+ }
@@ -0,0 +1,3 @@
1
+ export type OutputFormat = 'xml' | 'json';
2
+ export declare function escapeXml(str: string): string;
3
+ export declare function outputJson(data: unknown): void;
@@ -0,0 +1,11 @@
1
+ export function escapeXml(str) {
2
+ return str
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/'/g, '&apos;');
8
+ }
9
+ export function outputJson(data) {
10
+ console.log(JSON.stringify(data, null, 2));
11
+ }
@@ -826,57 +826,51 @@ function applyEvent(model, event) {
826
826
  break;
827
827
  }
828
828
  }
829
+ // Merge propagated fields with existing fields:
830
+ // - If a field with the same name exists, update its subfields
831
+ // - If it doesn't exist, add as new field
832
+ function mergeFieldsInto(existingFields, propagatedFields) {
833
+ for (const propagatedField of propagatedFields) {
834
+ const existingIdx = existingFields.findIndex(f => f.name === propagatedField.name);
835
+ if (existingIdx !== -1) {
836
+ // Field exists - update its subfields
837
+ existingFields[existingIdx] = {
838
+ ...existingFields[existingIdx],
839
+ subfields: propagatedField.subfields,
840
+ };
841
+ }
842
+ else {
843
+ // New field - add it
844
+ existingFields.push(propagatedField);
845
+ }
846
+ }
847
+ }
829
848
  function handleFieldPropagation(model, event) {
830
849
  const fields = event.fields;
831
850
  // Determine target based on event type
832
851
  if ('eventStickyId' in event) {
833
852
  const evt = model.events.get(event.eventStickyId);
834
- if (evt) {
835
- for (const field of fields) {
836
- if (!evt.fields.some(f => f.name === field.name)) {
837
- evt.fields.push(field);
838
- }
839
- }
840
- }
853
+ if (evt)
854
+ mergeFieldsInto(evt.fields, fields);
841
855
  }
842
856
  else if ('commandStickyId' in event) {
843
857
  const cmd = model.commands.get(event.commandStickyId);
844
- if (cmd) {
845
- for (const field of fields) {
846
- if (!cmd.fields.some(f => f.name === field.name)) {
847
- cmd.fields.push(field);
848
- }
849
- }
850
- }
858
+ if (cmd)
859
+ mergeFieldsInto(cmd.fields, fields);
851
860
  }
852
861
  else if ('readModelStickyId' in event) {
853
862
  const rm = model.readModels.get(event.readModelStickyId);
854
- if (rm) {
855
- for (const field of fields) {
856
- if (!rm.fields.some(f => f.name === field.name)) {
857
- rm.fields.push(field);
858
- }
859
- }
860
- }
863
+ if (rm)
864
+ mergeFieldsInto(rm.fields, fields);
861
865
  }
862
866
  else if ('screenId' in event) {
863
867
  const scr = model.screens.get(event.screenId);
864
- if (scr) {
865
- for (const field of fields) {
866
- if (!scr.fields.some(f => f.name === field.name)) {
867
- scr.fields.push(field);
868
- }
869
- }
870
- }
868
+ if (scr)
869
+ mergeFieldsInto(scr.fields, fields);
871
870
  }
872
871
  else if ('processorId' in event) {
873
872
  const proc = model.processors.get(event.processorId);
874
- if (proc) {
875
- for (const field of fields) {
876
- if (!proc.fields.some(f => f.name === field.name)) {
877
- proc.fields.push(field);
878
- }
879
- }
880
- }
873
+ if (proc)
874
+ mergeFieldsInto(proc.fields, fields);
881
875
  }
882
876
  }
@@ -1,2 +1,3 @@
1
1
  import type { EventModel } from '../../types.js';
2
- export declare function listChapters(model: EventModel): void;
2
+ import { type OutputFormat } from '../../lib/format.js';
3
+ export declare function listChapters(model: EventModel, format: OutputFormat): void;
@@ -1,19 +1,18 @@
1
- function escapeXml(str) {
2
- return str
3
- .replace(/&/g, '&amp;')
4
- .replace(/</g, '&lt;')
5
- .replace(/>/g, '&gt;')
6
- .replace(/"/g, '&quot;')
7
- .replace(/'/g, '&apos;');
8
- }
9
- export function listChapters(model) {
1
+ import { escapeXml, outputJson } from '../../lib/format.js';
2
+ export function listChapters(model, format) {
10
3
  const chapters = [...model.chapters.values()];
4
+ // Sort by x position (left to right on timeline)
5
+ const sorted = [...chapters].sort((a, b) => a.position.x - b.position.x);
6
+ if (format === 'json') {
7
+ outputJson({
8
+ chapters: sorted.map(ch => ({ name: ch.name }))
9
+ });
10
+ return;
11
+ }
11
12
  if (chapters.length === 0) {
12
13
  console.log('<chapters/>');
13
14
  return;
14
15
  }
15
- // Sort by x position (left to right on timeline)
16
- const sorted = [...chapters].sort((a, b) => a.position.x - b.position.x);
17
16
  console.log('<chapters>');
18
17
  for (const chapter of sorted) {
19
18
  console.log(` <chapter name="${escapeXml(chapter.name)}"/>`);
@@ -1,2 +1,3 @@
1
1
  import type { EventModel } from '../../types.js';
2
- export declare function listCommands(model: EventModel): void;
2
+ import { type OutputFormat } from '../../lib/format.js';
3
+ export declare function listCommands(model: EventModel, format: OutputFormat): void;
@@ -1,18 +1,17 @@
1
- function escapeXml(str) {
2
- return str
3
- .replace(/&/g, '&amp;')
4
- .replace(/</g, '&lt;')
5
- .replace(/>/g, '&gt;')
6
- .replace(/"/g, '&quot;')
7
- .replace(/'/g, '&apos;');
8
- }
9
- export function listCommands(model) {
1
+ import { escapeXml, outputJson } from '../../lib/format.js';
2
+ export function listCommands(model, format) {
10
3
  const commands = [...model.commands.values()];
4
+ const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name));
5
+ if (format === 'json') {
6
+ outputJson({
7
+ commands: sorted.map(cmd => ({ name: cmd.name, fields: cmd.fields.length }))
8
+ });
9
+ return;
10
+ }
11
11
  if (commands.length === 0) {
12
12
  console.log('<commands/>');
13
13
  return;
14
14
  }
15
- const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name));
16
15
  console.log('<commands>');
17
16
  for (const cmd of sorted) {
18
17
  console.log(` <command name="${escapeXml(cmd.name)}" fields="${cmd.fields.length}"/>`);
@@ -1,2 +1,3 @@
1
1
  import type { EventModel } from '../../types.js';
2
- export declare function listEvents(model: EventModel): void;
2
+ import { type OutputFormat } from '../../lib/format.js';
3
+ export declare function listEvents(model: EventModel, format: OutputFormat): void;
@@ -1,11 +1,4 @@
1
- function escapeXml(str) {
2
- return str
3
- .replace(/&/g, '&amp;')
4
- .replace(/</g, '&lt;')
5
- .replace(/>/g, '&gt;')
6
- .replace(/"/g, '&quot;')
7
- .replace(/'/g, '&apos;');
8
- }
1
+ import { escapeXml, outputJson } from '../../lib/format.js';
9
2
  // Find which aggregate an event belongs to (center point inside aggregate bounds)
10
3
  function findAggregateForEvent(model, event) {
11
4
  const centerX = event.position.x + event.width / 2;
@@ -37,12 +30,8 @@ function findSliceForEvent(model, event) {
37
30
  }
38
31
  return null;
39
32
  }
40
- export function listEvents(model) {
33
+ export function listEvents(model, format) {
41
34
  const events = [...model.events.values()];
42
- if (events.length === 0) {
43
- console.log('<events/>');
44
- return;
45
- }
46
35
  // Sort: originals first, then copies; alphabetically within each group
47
36
  const sorted = [...events].sort((a, b) => {
48
37
  // Originals before copies
@@ -53,6 +42,37 @@ export function listEvents(model) {
53
42
  // Alphabetically by name
54
43
  return a.name.localeCompare(b.name);
55
44
  });
45
+ if (format === 'json') {
46
+ outputJson({
47
+ events: sorted.map(evt => {
48
+ const aggregate = findAggregateForEvent(model, evt);
49
+ const result = {
50
+ name: evt.name,
51
+ fields: evt.fields.length,
52
+ };
53
+ if (aggregate)
54
+ result.aggregate = aggregate.name;
55
+ if (evt.originalNodeId) {
56
+ result.linkedCopy = true;
57
+ const original = model.events.get(evt.originalNodeId);
58
+ const originSlice = original ? findSliceForEvent(model, original) : null;
59
+ if (originSlice)
60
+ result.originSlice = originSlice;
61
+ }
62
+ else if (evt.canonicalId) {
63
+ const copyCount = [...model.events.values()].filter(e => e.canonicalId === evt.canonicalId && e.originalNodeId).length;
64
+ if (copyCount > 0)
65
+ result.copies = copyCount;
66
+ }
67
+ return result;
68
+ })
69
+ });
70
+ return;
71
+ }
72
+ if (events.length === 0) {
73
+ console.log('<events/>');
74
+ return;
75
+ }
56
76
  console.log('<events>');
57
77
  for (const evt of sorted) {
58
78
  const aggregate = findAggregateForEvent(model, evt);
@@ -1,2 +1,3 @@
1
1
  import type { EventModel } from '../../types.js';
2
- export declare function listSlices(model: EventModel): void;
2
+ import { type OutputFormat } from '../../lib/format.js';
3
+ export declare function listSlices(model: EventModel, format: OutputFormat): void;