eventmodeler 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.ts +282 -0
- package/dist/api/index.js +320 -0
- package/dist/eventmodeler.js +5645 -0
- package/dist/index.js +256 -809
- package/dist/lib/config.d.ts +2 -1
- package/dist/lib/project-config.d.ts +0 -3
- package/dist/lib/project-config.js +1 -8
- package/dist/lib/slice-utils.d.ts +0 -79
- package/dist/lib/slice-utils.js +87 -237
- package/dist/projection.js +0 -30
- package/dist/slices/add-scenario/index.d.ts +0 -2
- package/dist/slices/add-scenario/index.js +120 -318
- package/dist/slices/init/index.d.ts +1 -0
- package/dist/slices/init/index.js +40 -93
- package/dist/slices/whoami/index.d.ts +1 -1
- package/dist/slices/whoami/index.js +19 -10
- package/package.json +5 -2
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
2
|
+
const xmlParser = new XMLParser({
|
|
3
|
+
ignoreAttributes: false,
|
|
4
|
+
attributeNamePrefix: '@_',
|
|
5
|
+
allowBooleanAttributes: false,
|
|
6
|
+
parseAttributeValue: false,
|
|
7
|
+
isArray: (name) => name === 'event' || name === 'field',
|
|
8
|
+
trimValues: true,
|
|
9
|
+
});
|
|
10
|
+
function asArray(value) {
|
|
11
|
+
if (value === undefined || value === null)
|
|
12
|
+
return [];
|
|
13
|
+
return Array.isArray(value) ? value : [value];
|
|
14
|
+
}
|
|
4
15
|
function asRecord(value, context) {
|
|
5
16
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
6
17
|
throw new Error(`${context} must be an object`);
|
|
@@ -28,91 +39,62 @@ function parseFieldValue(value) {
|
|
|
28
39
|
return trimmed;
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const selfClosing = /\/>$/.test(token.trim());
|
|
51
|
-
if (depth === 0) {
|
|
52
|
-
startIndex = match.index;
|
|
53
|
-
startTagEnd = match.index + token.length;
|
|
54
|
-
startAttrs = token.slice(tagName.length + 1, token.length - (selfClosing ? 2 : 1)).trim();
|
|
55
|
-
if (selfClosing) {
|
|
56
|
-
blocks.push({ attrs: startAttrs, inner: '', selfClosing: true });
|
|
57
|
-
startIndex = -1;
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
depth = 1;
|
|
61
|
-
}
|
|
42
|
+
function parseFieldValuesFromNodes(nodes) {
|
|
43
|
+
if (nodes.length === 0)
|
|
44
|
+
return undefined;
|
|
45
|
+
const result = {};
|
|
46
|
+
for (const node of nodes) {
|
|
47
|
+
const n = node;
|
|
48
|
+
const name = n['@_name'];
|
|
49
|
+
if (!name)
|
|
50
|
+
continue;
|
|
51
|
+
const childFields = asArray(n['field']);
|
|
52
|
+
let value;
|
|
53
|
+
if (childFields.length > 0) {
|
|
54
|
+
value = parseFieldValuesFromNodes(childFields);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Get text content (fast-xml-parser stores it as #text)
|
|
58
|
+
const text = n['#text'];
|
|
59
|
+
if (text === undefined || text === '') {
|
|
60
|
+
value = null;
|
|
62
61
|
}
|
|
63
|
-
else
|
|
64
|
-
|
|
62
|
+
else {
|
|
63
|
+
value = parseFieldValue(String(text));
|
|
65
64
|
}
|
|
66
|
-
continue;
|
|
67
65
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
66
|
+
// Handle duplicate keys (multiple values → array)
|
|
67
|
+
if (name in result) {
|
|
68
|
+
const current = result[name];
|
|
69
|
+
if (Array.isArray(current)) {
|
|
70
|
+
current.push(value);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
result[name] = [current, value];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
result[name] = value;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
function firstTagBlock(xml, tagName) {
|
|
83
|
-
return extractTopLevelTagBlocks(xml, tagName)[0];
|
|
84
|
-
}
|
|
85
|
-
function assignFieldValue(target, key, value) {
|
|
86
|
-
if (!(key in target)) {
|
|
87
|
-
target[key] = value;
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const current = target[key];
|
|
91
|
-
if (Array.isArray(current)) {
|
|
92
|
-
current.push(value);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
target[key] = [current, value];
|
|
96
|
-
}
|
|
80
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
97
81
|
}
|
|
98
|
-
function
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
if (!
|
|
82
|
+
function parseEventInputsFromNodes(nodes) {
|
|
83
|
+
const result = [];
|
|
84
|
+
for (const node of nodes) {
|
|
85
|
+
const n = node;
|
|
86
|
+
const nameFromAttr = n['@_name'];
|
|
87
|
+
const textContent = n['#text'];
|
|
88
|
+
const eventName = nameFromAttr ?? textContent;
|
|
89
|
+
if (!eventName)
|
|
106
90
|
continue;
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
:
|
|
111
|
-
|
|
112
|
-
: parseFieldValue(stripTags(field.inner));
|
|
113
|
-
assignFieldValue(result, name, value);
|
|
91
|
+
const childFields = asArray(n['field']);
|
|
92
|
+
result.push({
|
|
93
|
+
event: eventName,
|
|
94
|
+
fieldValues: parseFieldValuesFromNodes(childFields),
|
|
95
|
+
});
|
|
114
96
|
}
|
|
115
|
-
return
|
|
97
|
+
return result;
|
|
116
98
|
}
|
|
117
99
|
function parseEventInput(value, context) {
|
|
118
100
|
if (typeof value === 'string') {
|
|
@@ -214,55 +196,49 @@ function normalizeScenarioInput(rawValue) {
|
|
|
214
196
|
function parseJsonInput(input) {
|
|
215
197
|
return normalizeScenarioInput(JSON.parse(input));
|
|
216
198
|
}
|
|
217
|
-
function parseEventInputsFromXml(xml) {
|
|
218
|
-
const result = [];
|
|
219
|
-
const eventBlocks = extractTopLevelTagBlocks(xml, 'event');
|
|
220
|
-
for (const eventBlock of eventBlocks) {
|
|
221
|
-
const nameFromAttr = getAttr(eventBlock.attrs, 'name');
|
|
222
|
-
const nameFromContent = eventBlock.inner.split('<', 1)[0].trim();
|
|
223
|
-
const eventName = nameFromAttr ?? nameFromContent;
|
|
224
|
-
if (!eventName)
|
|
225
|
-
continue;
|
|
226
|
-
result.push({
|
|
227
|
-
event: eventName,
|
|
228
|
-
fieldValues: parseFieldValuesFromXml(eventBlock.inner),
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
return result;
|
|
232
|
-
}
|
|
233
199
|
function parseXmlInput(input) {
|
|
234
|
-
const
|
|
235
|
-
|
|
200
|
+
const parsed = xmlParser.parse(input);
|
|
201
|
+
const scenario = parsed['scenario'];
|
|
202
|
+
if (!scenario) {
|
|
236
203
|
throw new Error('Invalid XML: missing <scenario> tag');
|
|
237
204
|
}
|
|
238
|
-
const name =
|
|
205
|
+
const name = scenario['@_name'];
|
|
239
206
|
if (!name) {
|
|
240
207
|
throw new Error('Invalid XML: scenario must have a name attribute');
|
|
241
208
|
}
|
|
242
|
-
const description =
|
|
243
|
-
|
|
244
|
-
const
|
|
209
|
+
const description = scenario['@_description'];
|
|
210
|
+
// Parse given events
|
|
211
|
+
const givenNode = scenario['given'];
|
|
212
|
+
const given = givenNode ? parseEventInputsFromNodes(asArray(givenNode['event'])) : [];
|
|
213
|
+
// Parse when
|
|
245
214
|
let when;
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
215
|
+
const whenNode = scenario['when'];
|
|
216
|
+
if (whenNode) {
|
|
217
|
+
const commandNodes = asArray(whenNode['command']);
|
|
218
|
+
const commandNode = commandNodes[0];
|
|
219
|
+
const whenEvents = parseEventInputsFromNodes(asArray(whenNode['event']));
|
|
220
|
+
if (commandNode || whenEvents.length > 0) {
|
|
221
|
+
let commandName;
|
|
222
|
+
let commandFieldValues;
|
|
223
|
+
if (commandNode) {
|
|
224
|
+
const nameFromAttr = commandNode['@_name'];
|
|
225
|
+
const textContent = commandNode['#text'];
|
|
226
|
+
commandName = nameFromAttr ?? textContent ?? undefined;
|
|
227
|
+
commandFieldValues = parseFieldValuesFromNodes(asArray(commandNode['field']));
|
|
228
|
+
}
|
|
254
229
|
when = {
|
|
255
230
|
command: commandName || undefined,
|
|
256
|
-
commandFieldValues
|
|
231
|
+
commandFieldValues,
|
|
257
232
|
events: whenEvents.length > 0 ? whenEvents : undefined,
|
|
258
233
|
};
|
|
259
234
|
}
|
|
260
235
|
}
|
|
261
|
-
|
|
262
|
-
|
|
236
|
+
// Parse then
|
|
237
|
+
const thenNode = scenario['then'];
|
|
238
|
+
if (!thenNode) {
|
|
263
239
|
throw new Error('Invalid XML: missing <then> tag');
|
|
264
240
|
}
|
|
265
|
-
const thenType =
|
|
241
|
+
const thenType = thenNode['@_type'];
|
|
266
242
|
if (thenType !== 'error' &&
|
|
267
243
|
thenType !== 'events' &&
|
|
268
244
|
thenType !== 'readModelAssertion' &&
|
|
@@ -270,37 +246,47 @@ function parseXmlInput(input) {
|
|
|
270
246
|
thenType !== 'noCommand') {
|
|
271
247
|
throw new Error('Invalid XML: <then> must have a type attribute');
|
|
272
248
|
}
|
|
273
|
-
const thenBody = thenMatch[2] ?? '';
|
|
274
249
|
const then = { type: thenType };
|
|
275
250
|
if (thenType === 'error') {
|
|
276
|
-
then.errorType =
|
|
277
|
-
|
|
251
|
+
then.errorType = thenNode['@_errorType'];
|
|
252
|
+
const errorTypeNode = thenNode['errorType'];
|
|
253
|
+
if (!then.errorType && errorTypeNode) {
|
|
254
|
+
then.errorType = typeof errorTypeNode === 'string' ? errorTypeNode : errorTypeNode['#text'];
|
|
255
|
+
}
|
|
256
|
+
const messageNode = thenNode['message'];
|
|
257
|
+
if (messageNode) {
|
|
258
|
+
then.errorMessage = typeof messageNode === 'string' ? messageNode : messageNode['#text'];
|
|
259
|
+
}
|
|
278
260
|
}
|
|
279
261
|
else if (thenType === 'events') {
|
|
280
|
-
then.events =
|
|
262
|
+
then.events = parseEventInputsFromNodes(asArray(thenNode['event']));
|
|
281
263
|
}
|
|
282
264
|
else if (thenType === 'command') {
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
265
|
+
const commandNodes = asArray(thenNode['command']);
|
|
266
|
+
const commandNode = commandNodes[0];
|
|
267
|
+
if (commandNode) {
|
|
268
|
+
const nameFromAttr = commandNode['@_name'];
|
|
269
|
+
const textContent = commandNode['#text'];
|
|
270
|
+
then.command = nameFromAttr ?? textContent ?? undefined;
|
|
271
|
+
then.commandFieldValues = parseFieldValuesFromNodes(asArray(commandNode['field']));
|
|
287
272
|
}
|
|
288
273
|
}
|
|
289
274
|
else if (thenType === 'readModelAssertion') {
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
const
|
|
275
|
+
const readModelNode = thenNode['read-model'];
|
|
276
|
+
if (readModelNode) {
|
|
277
|
+
const nameFromAttr = readModelNode['@_name'];
|
|
278
|
+
const textContent = readModelNode['#text'];
|
|
279
|
+
then.readModel = nameFromAttr ?? textContent ?? undefined;
|
|
280
|
+
const expectedNode = readModelNode['expected'];
|
|
281
|
+
const directFields = parseFieldValuesFromNodes(asArray(readModelNode['field']));
|
|
282
|
+
const expectedFields = expectedNode ? parseFieldValuesFromNodes(asArray(expectedNode['field'])) : undefined;
|
|
296
283
|
then.expected = {
|
|
297
|
-
...(
|
|
298
|
-
...(
|
|
284
|
+
...(directFields ?? {}),
|
|
285
|
+
...(expectedFields ?? {}),
|
|
299
286
|
};
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
then.givenEvents = parseEventInputsFromXml(givenBlockInAssertion.inner);
|
|
287
|
+
const givenEventsNode = (readModelNode['given'] ?? readModelNode['given-events']);
|
|
288
|
+
if (givenEventsNode) {
|
|
289
|
+
then.givenEvents = parseEventInputsFromNodes(asArray(givenEventsNode['event']));
|
|
304
290
|
}
|
|
305
291
|
}
|
|
306
292
|
}
|
|
@@ -319,187 +305,3 @@ export function parseScenarioInput(input) {
|
|
|
319
305
|
}
|
|
320
306
|
return parseJsonInput(trimmed);
|
|
321
307
|
}
|
|
322
|
-
export function addScenario(model, filePath, sliceName, input) {
|
|
323
|
-
// Parse input
|
|
324
|
-
let scenarioInput;
|
|
325
|
-
try {
|
|
326
|
-
scenarioInput = parseScenarioInput(input);
|
|
327
|
-
}
|
|
328
|
-
catch (err) {
|
|
329
|
-
console.error(`Error: Invalid input format: ${err.message}`);
|
|
330
|
-
process.exit(1);
|
|
331
|
-
}
|
|
332
|
-
// Find slice by name
|
|
333
|
-
const slice = findElementOrExit(model.slices, sliceName, 'slice');
|
|
334
|
-
// Resolve event references in given (exclude linked copies)
|
|
335
|
-
const givenEvents = [];
|
|
336
|
-
if (scenarioInput.given) {
|
|
337
|
-
for (const g of scenarioInput.given) {
|
|
338
|
-
const event = findElementOrExit(excludeLinkedCopies(model.events), g.event, 'event');
|
|
339
|
-
givenEvents.push({
|
|
340
|
-
eventStickyId: event.id,
|
|
341
|
-
fieldValues: g.fieldValues,
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
// Resolve command reference in when
|
|
346
|
-
let whenCommand = null;
|
|
347
|
-
if (scenarioInput.when?.command) {
|
|
348
|
-
const command = findElementOrExit(model.commands, scenarioInput.when.command, 'command');
|
|
349
|
-
whenCommand = {
|
|
350
|
-
commandStickyId: command.id,
|
|
351
|
-
fieldValues: scenarioInput.when.commandFieldValues,
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
// Resolve event references in when
|
|
355
|
-
const whenEvents = [];
|
|
356
|
-
if (scenarioInput.when?.events) {
|
|
357
|
-
for (const e of scenarioInput.when.events) {
|
|
358
|
-
const event = findElementOrExit(excludeLinkedCopies(model.events), e.event, 'event');
|
|
359
|
-
whenEvents.push({
|
|
360
|
-
eventStickyId: event.id,
|
|
361
|
-
fieldValues: e.fieldValues,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
// Resolve then clause
|
|
366
|
-
const then = { type: scenarioInput.then.type };
|
|
367
|
-
if (scenarioInput.then.type === 'error') {
|
|
368
|
-
then.errorMessage = scenarioInput.then.errorMessage;
|
|
369
|
-
then.errorType = scenarioInput.then.errorType;
|
|
370
|
-
}
|
|
371
|
-
else if (scenarioInput.then.type === 'events') {
|
|
372
|
-
then.expectedEvents = [];
|
|
373
|
-
if (scenarioInput.then.events) {
|
|
374
|
-
for (const e of scenarioInput.then.events) {
|
|
375
|
-
const event = findElementOrExit(excludeLinkedCopies(model.events), e.event, 'event');
|
|
376
|
-
then.expectedEvents.push({
|
|
377
|
-
eventStickyId: event.id,
|
|
378
|
-
fieldValues: e.fieldValues,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
else if (scenarioInput.then.type === 'command') {
|
|
384
|
-
if (!scenarioInput.then.command) {
|
|
385
|
-
console.error('Error: command then type requires a command name');
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
388
|
-
const command = findElementOrExit(model.commands, scenarioInput.then.command, 'command');
|
|
389
|
-
then.expectedCommand = {
|
|
390
|
-
commandStickyId: command.id,
|
|
391
|
-
fieldValues: scenarioInput.then.commandFieldValues,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
else if (scenarioInput.then.type === 'readModelAssertion') {
|
|
395
|
-
if (!scenarioInput.then.readModel) {
|
|
396
|
-
console.error('Error: readModelAssertion requires a readModel name');
|
|
397
|
-
process.exit(1);
|
|
398
|
-
}
|
|
399
|
-
const readModel = findElementOrExit(excludeLinkedCopies(model.readModels), scenarioInput.then.readModel, 'read model');
|
|
400
|
-
const assertionGivenEvents = [];
|
|
401
|
-
if (scenarioInput.then.givenEvents) {
|
|
402
|
-
for (const g of scenarioInput.then.givenEvents) {
|
|
403
|
-
const event = findElementOrExit(excludeLinkedCopies(model.events), g.event, 'event');
|
|
404
|
-
assertionGivenEvents.push({
|
|
405
|
-
eventStickyId: event.id,
|
|
406
|
-
fieldValues: g.fieldValues,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
then.readModelAssertion = {
|
|
411
|
-
readModelStickyId: readModel.id,
|
|
412
|
-
givenEvents: assertionGivenEvents,
|
|
413
|
-
expectedFieldValues: scenarioInput.then.expected ?? {},
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
// Calculate position below slice
|
|
417
|
-
// Scenarios are tall (GWT rows with 100px stickies + labels + padding = ~400px+)
|
|
418
|
-
// so we need adequate spacing between them
|
|
419
|
-
const SCENARIO_GAP = 20; // Match slice gap
|
|
420
|
-
const SCENARIO_ESTIMATED_HEIGHT = 450; // Approximate rendered height for positioning
|
|
421
|
-
const existingScenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
|
|
422
|
-
const sliceBottom = slice.position.y + slice.size.height;
|
|
423
|
-
let positionY;
|
|
424
|
-
if (existingScenarios.length === 0) {
|
|
425
|
-
positionY = sliceBottom + SCENARIO_GAP;
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
// Use estimated height since stored height (80px) doesn't reflect actual rendered size
|
|
429
|
-
const lowestY = Math.max(...existingScenarios.map(s => s.position.y + SCENARIO_ESTIMATED_HEIGHT));
|
|
430
|
-
positionY = lowestY + SCENARIO_GAP;
|
|
431
|
-
}
|
|
432
|
-
const position = {
|
|
433
|
-
x: slice.position.x + 10,
|
|
434
|
-
y: positionY,
|
|
435
|
-
};
|
|
436
|
-
// Generate scenario ID
|
|
437
|
-
const scenarioId = crypto.randomUUID();
|
|
438
|
-
// Append ScenarioCreated event
|
|
439
|
-
appendEvent(filePath, {
|
|
440
|
-
type: 'ScenarioCreated',
|
|
441
|
-
data: {
|
|
442
|
-
scenarioId,
|
|
443
|
-
sliceId: slice.id,
|
|
444
|
-
name: scenarioInput.name,
|
|
445
|
-
position,
|
|
446
|
-
width: 200,
|
|
447
|
-
height: 80,
|
|
448
|
-
timestamp: Date.now(),
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
// Append description update if provided
|
|
452
|
-
if (scenarioInput.description) {
|
|
453
|
-
appendEvent(filePath, {
|
|
454
|
-
type: 'ScenarioDescriptionUpdated',
|
|
455
|
-
data: {
|
|
456
|
-
scenarioId,
|
|
457
|
-
description: scenarioInput.description,
|
|
458
|
-
timestamp: Date.now(),
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
// Append given events update if provided
|
|
463
|
-
if (givenEvents.length > 0) {
|
|
464
|
-
appendEvent(filePath, {
|
|
465
|
-
type: 'ScenarioGivenEventsUpdated',
|
|
466
|
-
data: {
|
|
467
|
-
scenarioId,
|
|
468
|
-
givenEvents,
|
|
469
|
-
timestamp: Date.now(),
|
|
470
|
-
},
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
// Append when command update if provided
|
|
474
|
-
if (whenCommand) {
|
|
475
|
-
appendEvent(filePath, {
|
|
476
|
-
type: 'ScenarioWhenCommandUpdated',
|
|
477
|
-
data: {
|
|
478
|
-
scenarioId,
|
|
479
|
-
whenCommand,
|
|
480
|
-
timestamp: Date.now(),
|
|
481
|
-
},
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
// Append when events update if provided
|
|
485
|
-
if (whenEvents.length > 0) {
|
|
486
|
-
appendEvent(filePath, {
|
|
487
|
-
type: 'ScenarioWhenEventsUpdated',
|
|
488
|
-
data: {
|
|
489
|
-
scenarioId,
|
|
490
|
-
whenEvents,
|
|
491
|
-
timestamp: Date.now(),
|
|
492
|
-
},
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
// Append then update
|
|
496
|
-
appendEvent(filePath, {
|
|
497
|
-
type: 'ScenarioThenUpdated',
|
|
498
|
-
data: {
|
|
499
|
-
scenarioId,
|
|
500
|
-
then,
|
|
501
|
-
timestamp: Date.now(),
|
|
502
|
-
},
|
|
503
|
-
});
|
|
504
|
-
console.log(`Added scenario "${scenarioInput.name}" to slice "${slice.name}"`);
|
|
505
|
-
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import * as readline from 'node:readline';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
2
|
import { isAuthenticated } from '../../lib/config.js';
|
|
5
|
-
import {
|
|
3
|
+
import { listModels } from '../../api/index.js';
|
|
6
4
|
import { saveProjectConfig, getProjectRoot, isInProject } from '../../lib/project-config.js';
|
|
7
5
|
/**
|
|
8
6
|
* Interactive init command to set up a project.
|
|
7
|
+
* Connects the current directory to an event model.
|
|
9
8
|
*/
|
|
10
9
|
export async function init() {
|
|
11
|
-
// Check if already initialized
|
|
12
10
|
if (isInProject()) {
|
|
13
11
|
console.log('This project is already initialized.');
|
|
14
12
|
console.log('To reconfigure, delete .eventmodeler.json and run init again.');
|
|
@@ -16,6 +14,11 @@ export async function init() {
|
|
|
16
14
|
}
|
|
17
15
|
const projectRoot = getProjectRoot();
|
|
18
16
|
console.log(`Initializing event model project in: ${projectRoot}\n`);
|
|
17
|
+
if (!isAuthenticated()) {
|
|
18
|
+
console.log('You need to be logged in first.');
|
|
19
|
+
console.log('Run "eventmodeler login" first, then try init again.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
19
22
|
const rl = readline.createInterface({
|
|
20
23
|
input: process.stdin,
|
|
21
24
|
output: process.stdout,
|
|
@@ -28,22 +31,46 @@ export async function init() {
|
|
|
28
31
|
});
|
|
29
32
|
};
|
|
30
33
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
console.log('Fetching your event models...\n');
|
|
35
|
+
const models = await listModels();
|
|
36
|
+
if (models.length === 0) {
|
|
37
|
+
console.log('You don\'t have any event models yet.');
|
|
38
|
+
console.log('Create one at https://eventmodeler.app first, then run init again.');
|
|
39
|
+
rl.close();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log('Your event models:\n');
|
|
43
|
+
models.forEach((model, index) => {
|
|
44
|
+
console.log(` ${index + 1}. ${model.name} (${model.modelId})`);
|
|
45
|
+
});
|
|
46
|
+
console.log(` ${models.length + 1}. Create a new model\n`);
|
|
47
|
+
const modelChoice = await question(`Select a model (1-${models.length + 1}): `);
|
|
48
|
+
const modelIndex = parseInt(modelChoice, 10) - 1;
|
|
49
|
+
let selectedModel;
|
|
50
|
+
if (modelIndex === models.length) {
|
|
51
|
+
console.log('\nTo create a new model, visit https://eventmodeler.app');
|
|
52
|
+
console.log('After creating it, run init again to link it to this project.');
|
|
53
|
+
rl.close();
|
|
54
|
+
return;
|
|
38
55
|
}
|
|
39
|
-
else if (
|
|
40
|
-
|
|
56
|
+
else if (modelIndex >= 0 && modelIndex < models.length) {
|
|
57
|
+
selectedModel = models[modelIndex];
|
|
41
58
|
}
|
|
42
59
|
else {
|
|
43
60
|
console.log('\nInvalid choice. Please run init again.');
|
|
44
61
|
rl.close();
|
|
45
62
|
return;
|
|
46
63
|
}
|
|
64
|
+
const config = {
|
|
65
|
+
type: 'cloud',
|
|
66
|
+
modelId: selectedModel.modelId,
|
|
67
|
+
modelName: selectedModel.name,
|
|
68
|
+
};
|
|
69
|
+
const configPath = saveProjectConfig(config, projectRoot);
|
|
70
|
+
console.log(`\nProject initialized successfully!`);
|
|
71
|
+
console.log(`Config saved to: ${configPath}`);
|
|
72
|
+
console.log(`\nLinked to model: ${selectedModel.name}`);
|
|
73
|
+
console.log('\nYou can now use eventmodeler commands in this directory.');
|
|
47
74
|
rl.close();
|
|
48
75
|
}
|
|
49
76
|
catch (err) {
|
|
@@ -51,83 +78,3 @@ export async function init() {
|
|
|
51
78
|
throw err;
|
|
52
79
|
}
|
|
53
80
|
}
|
|
54
|
-
async function initCloud(question, projectRoot) {
|
|
55
|
-
// Check authentication
|
|
56
|
-
if (!isAuthenticated()) {
|
|
57
|
-
console.log('\nYou need to be logged in to use cloud storage.');
|
|
58
|
-
console.log('Run "eventmodeler login" first, then try init again.');
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
console.log('\nFetching your event models...\n');
|
|
62
|
-
const client = await createCloudClient();
|
|
63
|
-
const models = await client.listModels();
|
|
64
|
-
if (models.length === 0) {
|
|
65
|
-
console.log('You don\'t have any event models yet.');
|
|
66
|
-
console.log('Create one at https://eventmodeler.app first, then run init again.');
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
console.log('Your event models:\n');
|
|
70
|
-
models.forEach((model, index) => {
|
|
71
|
-
console.log(` ${index + 1}. ${model.name} (${model.modelId})`);
|
|
72
|
-
});
|
|
73
|
-
console.log(` ${models.length + 1}. Create a new model\n`);
|
|
74
|
-
const modelChoice = await question(`Select a model (1-${models.length + 1}): `);
|
|
75
|
-
const modelIndex = parseInt(modelChoice, 10) - 1;
|
|
76
|
-
let selectedModel;
|
|
77
|
-
if (modelIndex === models.length) {
|
|
78
|
-
// Create new model - for now, redirect to web
|
|
79
|
-
console.log('\nTo create a new model, visit https://eventmodeler.app');
|
|
80
|
-
console.log('After creating it, run init again to link it to this project.');
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
else if (modelIndex >= 0 && modelIndex < models.length) {
|
|
84
|
-
selectedModel = models[modelIndex];
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
console.log('\nInvalid choice. Please run init again.');
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const config = {
|
|
91
|
-
type: 'cloud',
|
|
92
|
-
modelId: selectedModel.modelId,
|
|
93
|
-
modelName: selectedModel.name,
|
|
94
|
-
};
|
|
95
|
-
const configPath = saveProjectConfig(config, projectRoot);
|
|
96
|
-
console.log(`\nProject initialized successfully!`);
|
|
97
|
-
console.log(`Config saved to: ${configPath}`);
|
|
98
|
-
console.log(`\nLinked to cloud model: ${selectedModel.name}`);
|
|
99
|
-
console.log('\nYou can now use eventmodeler commands in this directory.');
|
|
100
|
-
}
|
|
101
|
-
async function initLocal(question, projectRoot) {
|
|
102
|
-
const defaultFile = './model.eventmodel';
|
|
103
|
-
const filePath = await question(`Event model file path [${defaultFile}]: `);
|
|
104
|
-
const finalPath = filePath || defaultFile;
|
|
105
|
-
// Resolve relative to project root
|
|
106
|
-
const absolutePath = path.isAbsolute(finalPath)
|
|
107
|
-
? finalPath
|
|
108
|
-
: path.resolve(projectRoot, finalPath);
|
|
109
|
-
// Check if file exists, create if not
|
|
110
|
-
if (!fs.existsSync(absolutePath)) {
|
|
111
|
-
const create = await question(`File doesn't exist. Create it? (y/n): `);
|
|
112
|
-
if (create.toLowerCase() === 'y') {
|
|
113
|
-
// Create empty event model file
|
|
114
|
-
fs.writeFileSync(absolutePath, '');
|
|
115
|
-
console.log(`\nCreated: ${absolutePath}`);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
console.log('\nAborted. Please create the file and run init again.');
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
// Store relative path in config
|
|
123
|
-
const relativePath = path.relative(projectRoot, absolutePath);
|
|
124
|
-
const config = {
|
|
125
|
-
type: 'local',
|
|
126
|
-
file: relativePath.startsWith('.') ? relativePath : `./${relativePath}`,
|
|
127
|
-
};
|
|
128
|
-
const configPath = saveProjectConfig(config, projectRoot);
|
|
129
|
-
console.log(`\nProject initialized successfully!`);
|
|
130
|
-
console.log(`Config saved to: ${configPath}`);
|
|
131
|
-
console.log(`\nLinked to local file: ${config.file}`);
|
|
132
|
-
console.log('\nYou can now use eventmodeler commands in this directory.');
|
|
133
|
-
}
|