eventmodeler 0.4.0 → 0.4.1
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/cloud/slices/index.d.ts +215 -0
- package/dist/cloud/slices/index.js +247 -0
- package/dist/index.js +984 -39
- package/dist/lib/auth.d.ts +24 -0
- package/dist/lib/auth.js +332 -0
- package/dist/lib/backend.d.ts +48 -0
- package/dist/lib/backend.js +103 -0
- package/dist/lib/cloud-client.d.ts +70 -0
- package/dist/lib/cloud-client.js +528 -0
- package/dist/lib/config.d.ts +27 -0
- package/dist/lib/config.js +80 -11
- package/dist/lib/diff/three-way-merge.js +4 -4
- package/dist/lib/file-loader.js +43 -12
- package/dist/lib/project-config.d.ts +30 -0
- package/dist/lib/project-config.js +90 -0
- package/dist/lib/slice-utils.d.ts +28 -0
- package/dist/lib/slice-utils.js +80 -0
- package/dist/local/slices/index.d.ts +11 -0
- package/dist/local/slices/index.js +13 -0
- package/dist/projection.js +372 -371
- package/dist/slices/add-field/index.js +25 -15
- package/dist/slices/add-scenario/index.js +34 -22
- package/dist/slices/create-automation-slice/index.js +93 -65
- package/dist/slices/create-flow/index.js +24 -18
- package/dist/slices/create-state-change-slice/index.js +77 -53
- package/dist/slices/create-state-view-slice/index.js +25 -17
- package/dist/slices/import/index.d.ts +8 -0
- package/dist/slices/import/index.js +63 -0
- package/dist/slices/init/index.d.ts +4 -0
- package/dist/slices/init/index.js +133 -0
- package/dist/slices/list-processors/index.d.ts +3 -0
- package/dist/slices/list-processors/index.js +20 -0
- package/dist/slices/list-readmodels/index.d.ts +3 -0
- package/dist/slices/list-readmodels/index.js +21 -0
- package/dist/slices/list-scenarios/index.d.ts +3 -0
- package/dist/slices/list-scenarios/index.js +35 -0
- package/dist/slices/list-screens/index.d.ts +3 -0
- package/dist/slices/list-screens/index.js +47 -0
- package/dist/slices/login/index.d.ts +1 -0
- package/dist/slices/login/index.js +24 -0
- package/dist/slices/logout/index.d.ts +1 -0
- package/dist/slices/logout/index.js +14 -0
- package/dist/slices/map-fields/index.js +5 -3
- package/dist/slices/mark-slice-status/index.js +4 -2
- package/dist/slices/remove-field/index.js +25 -15
- package/dist/slices/remove-scenario/index.js +8 -4
- package/dist/slices/show-aggregate/index.d.ts +3 -0
- package/dist/slices/show-aggregate/index.js +108 -0
- package/dist/slices/show-processor/index.d.ts +3 -0
- package/dist/slices/show-processor/index.js +111 -0
- package/dist/slices/show-readmodel/index.d.ts +3 -0
- package/dist/slices/show-readmodel/index.js +158 -0
- package/dist/slices/show-scenario/index.d.ts +3 -0
- package/dist/slices/show-scenario/index.js +196 -0
- package/dist/slices/show-screen/index.d.ts +3 -0
- package/dist/slices/show-screen/index.js +139 -0
- package/dist/slices/update-field/index.js +30 -20
- package/dist/slices/whoami/index.d.ts +2 -0
- package/dist/slices/whoami/index.js +35 -0
- package/dist/types.d.ts +1 -2
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type AuthTokens } from './config.js';
|
|
2
|
+
interface AuthResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
tokens?: AuthTokens;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Starts the OAuth flow using Keycloak PKCE.
|
|
9
|
+
* Opens browser and waits for the callback.
|
|
10
|
+
*/
|
|
11
|
+
export declare function startAuthFlow(): Promise<AuthResult>;
|
|
12
|
+
/**
|
|
13
|
+
* Refresh the access token using the refresh token.
|
|
14
|
+
*/
|
|
15
|
+
export declare function refreshAccessToken(): Promise<AuthResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Get a valid access token, refreshing if needed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getValidAccessToken(): Promise<string | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Log out by clearing stored tokens.
|
|
22
|
+
*/
|
|
23
|
+
export declare function logout(): void;
|
|
24
|
+
export {};
|
package/dist/lib/auth.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
import { saveAuthTokens, clearAuthTokens, getAuthTokens, getKeycloakUrl } from './config.js';
|
|
5
|
+
const KEYCLOAK_CLIENT_ID = 'eventmodeler-cli';
|
|
6
|
+
const REDIRECT_URI = 'http://localhost:8787/callback';
|
|
7
|
+
/**
|
|
8
|
+
* Decode the payload of a JWT token (no signature verification - that's the server's job).
|
|
9
|
+
*/
|
|
10
|
+
function decodeJwtPayload(token) {
|
|
11
|
+
const parts = token.split('.');
|
|
12
|
+
if (parts.length !== 3)
|
|
13
|
+
throw new Error('Invalid JWT format');
|
|
14
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
|
|
15
|
+
return JSON.parse(payload);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate PKCE code verifier and challenge.
|
|
19
|
+
*/
|
|
20
|
+
function generatePKCE() {
|
|
21
|
+
// Generate a random code verifier (43-128 characters)
|
|
22
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
23
|
+
// Generate code challenge using S256 method
|
|
24
|
+
const codeChallenge = crypto
|
|
25
|
+
.createHash('sha256')
|
|
26
|
+
.update(codeVerifier)
|
|
27
|
+
.digest('base64url');
|
|
28
|
+
return { codeVerifier, codeChallenge };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Starts the OAuth flow using Keycloak PKCE.
|
|
32
|
+
* Opens browser and waits for the callback.
|
|
33
|
+
*/
|
|
34
|
+
export async function startAuthFlow() {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const keycloakUrl = getKeycloakUrl();
|
|
37
|
+
// Generate PKCE values
|
|
38
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
39
|
+
// Generate state for CSRF protection
|
|
40
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
41
|
+
// Create local server to receive callback
|
|
42
|
+
const server = http.createServer(async (req, res) => {
|
|
43
|
+
const url = new URL(req.url ?? '/', `http://localhost:8787`);
|
|
44
|
+
if (url.pathname === '/callback') {
|
|
45
|
+
const code = url.searchParams.get('code');
|
|
46
|
+
const returnedState = url.searchParams.get('state');
|
|
47
|
+
const error = url.searchParams.get('error');
|
|
48
|
+
if (error) {
|
|
49
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
50
|
+
res.end(renderHtml('Authentication Failed', `Error: ${error}`, false));
|
|
51
|
+
server.close();
|
|
52
|
+
resolve({ success: false, error });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (returnedState !== state) {
|
|
56
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
57
|
+
res.end(renderHtml('Authentication Failed', 'Invalid state parameter. Please try again.', false));
|
|
58
|
+
server.close();
|
|
59
|
+
resolve({ success: false, error: 'Invalid state parameter' });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!code) {
|
|
63
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
64
|
+
res.end(renderHtml('Authentication Failed', 'No authorization code received.', false));
|
|
65
|
+
server.close();
|
|
66
|
+
resolve({ success: false, error: 'No authorization code' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
// Exchange the code for tokens via Keycloak token endpoint (form-urlencoded)
|
|
71
|
+
const tokenUrl = `${keycloakUrl}/protocol/openid-connect/token`;
|
|
72
|
+
const body = new URLSearchParams({
|
|
73
|
+
client_id: KEYCLOAK_CLIENT_ID,
|
|
74
|
+
code,
|
|
75
|
+
code_verifier: codeVerifier,
|
|
76
|
+
grant_type: 'authorization_code',
|
|
77
|
+
redirect_uri: REDIRECT_URI,
|
|
78
|
+
});
|
|
79
|
+
const tokenResponse = await fetch(tokenUrl, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
83
|
+
},
|
|
84
|
+
body: body.toString(),
|
|
85
|
+
});
|
|
86
|
+
if (!tokenResponse.ok) {
|
|
87
|
+
const errorData = await tokenResponse.json().catch(() => ({}));
|
|
88
|
+
throw new Error(errorData.error_description ?? errorData.error ?? `Token exchange failed: ${tokenResponse.status}`);
|
|
89
|
+
}
|
|
90
|
+
const tokenData = await tokenResponse.json();
|
|
91
|
+
// Extract user info from JWT claims
|
|
92
|
+
const claims = decodeJwtPayload(tokenData.access_token);
|
|
93
|
+
const expiresAt = Date.now() + (tokenData.expires_in ?? 3600) * 1000;
|
|
94
|
+
const tokens = {
|
|
95
|
+
accessToken: tokenData.access_token,
|
|
96
|
+
refreshToken: tokenData.refresh_token ?? '',
|
|
97
|
+
expiresAt,
|
|
98
|
+
userId: String(claims.sub ?? ''),
|
|
99
|
+
email: String(claims.email ?? ''),
|
|
100
|
+
};
|
|
101
|
+
saveAuthTokens(tokens);
|
|
102
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
103
|
+
res.end(renderHtml('Authentication Successful!', formatSuccessMessage(tokens.email), true));
|
|
104
|
+
server.close();
|
|
105
|
+
resolve({ success: true, tokens });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
109
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
110
|
+
res.end(renderHtml('Authentication Failed', errorMessage, false));
|
|
111
|
+
server.close();
|
|
112
|
+
resolve({ success: false, error: errorMessage });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
res.writeHead(404);
|
|
117
|
+
res.end('Not found');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
server.listen(8787, () => {
|
|
121
|
+
// Build Keycloak authorization URL with PKCE
|
|
122
|
+
const params = new URLSearchParams({
|
|
123
|
+
client_id: KEYCLOAK_CLIENT_ID,
|
|
124
|
+
redirect_uri: REDIRECT_URI,
|
|
125
|
+
response_type: 'code',
|
|
126
|
+
state,
|
|
127
|
+
code_challenge: codeChallenge,
|
|
128
|
+
code_challenge_method: 'S256',
|
|
129
|
+
scope: 'openid email profile',
|
|
130
|
+
});
|
|
131
|
+
const authorizationUrl = `${keycloakUrl}/protocol/openid-connect/auth?${params}`;
|
|
132
|
+
console.log('\nOpening browser for authentication...');
|
|
133
|
+
console.log(`\nIf the browser doesn't open, visit:\n${authorizationUrl}\n`);
|
|
134
|
+
// Open the browser
|
|
135
|
+
openBrowser(authorizationUrl);
|
|
136
|
+
});
|
|
137
|
+
// Timeout after 5 minutes
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
server.close();
|
|
140
|
+
resolve({ success: false, error: 'Authentication timed out' });
|
|
141
|
+
}, 5 * 60 * 1000);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Refresh the access token using the refresh token.
|
|
146
|
+
*/
|
|
147
|
+
export async function refreshAccessToken() {
|
|
148
|
+
const tokens = getAuthTokens();
|
|
149
|
+
if (!tokens?.refreshToken) {
|
|
150
|
+
return { success: false, error: 'No refresh token available' };
|
|
151
|
+
}
|
|
152
|
+
const keycloakUrl = getKeycloakUrl();
|
|
153
|
+
const tokenUrl = `${keycloakUrl}/protocol/openid-connect/token`;
|
|
154
|
+
try {
|
|
155
|
+
const body = new URLSearchParams({
|
|
156
|
+
client_id: KEYCLOAK_CLIENT_ID,
|
|
157
|
+
refresh_token: tokens.refreshToken,
|
|
158
|
+
grant_type: 'refresh_token',
|
|
159
|
+
});
|
|
160
|
+
const response = await fetch(tokenUrl, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
164
|
+
},
|
|
165
|
+
body: body.toString(),
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
const errorData = await response.json().catch(() => ({}));
|
|
169
|
+
throw new Error(errorData.error_description ?? errorData.error ?? `Token refresh failed: ${response.status}`);
|
|
170
|
+
}
|
|
171
|
+
const tokenData = await response.json();
|
|
172
|
+
// Extract user info from refreshed JWT
|
|
173
|
+
const claims = decodeJwtPayload(tokenData.access_token);
|
|
174
|
+
const newTokens = {
|
|
175
|
+
accessToken: tokenData.access_token,
|
|
176
|
+
refreshToken: tokenData.refresh_token ?? tokens.refreshToken,
|
|
177
|
+
expiresAt: Date.now() + (tokenData.expires_in ?? 3600) * 1000,
|
|
178
|
+
userId: String(claims.sub ?? tokens.userId),
|
|
179
|
+
email: String(claims.email ?? tokens.email),
|
|
180
|
+
};
|
|
181
|
+
saveAuthTokens(newTokens);
|
|
182
|
+
return { success: true, tokens: newTokens };
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
186
|
+
return { success: false, error: errorMessage };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get a valid access token, refreshing if needed.
|
|
191
|
+
*/
|
|
192
|
+
export async function getValidAccessToken() {
|
|
193
|
+
const tokens = getAuthTokens();
|
|
194
|
+
if (!tokens)
|
|
195
|
+
return null;
|
|
196
|
+
// If token is expired or will expire in 5 minutes, refresh it
|
|
197
|
+
if (tokens.expiresAt < Date.now() + 5 * 60 * 1000) {
|
|
198
|
+
const result = await refreshAccessToken();
|
|
199
|
+
if (!result.success || !result.tokens)
|
|
200
|
+
return null;
|
|
201
|
+
return result.tokens.accessToken;
|
|
202
|
+
}
|
|
203
|
+
return tokens.accessToken;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Log out by clearing stored tokens.
|
|
207
|
+
*/
|
|
208
|
+
export function logout() {
|
|
209
|
+
clearAuthTokens();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Open a URL in the default browser.
|
|
213
|
+
*/
|
|
214
|
+
function openBrowser(url) {
|
|
215
|
+
const platform = process.platform;
|
|
216
|
+
let command;
|
|
217
|
+
if (platform === 'darwin') {
|
|
218
|
+
command = `open "${url}"`;
|
|
219
|
+
}
|
|
220
|
+
else if (platform === 'win32') {
|
|
221
|
+
command = `start "" "${url}"`;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
command = `xdg-open "${url}"`;
|
|
225
|
+
}
|
|
226
|
+
exec(command, (err) => {
|
|
227
|
+
if (err) {
|
|
228
|
+
console.error('Failed to open browser:', err.message);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Render a styled HTML page for the callback matching Event Modeler's design.
|
|
234
|
+
*/
|
|
235
|
+
function renderHtml(title, message, success) {
|
|
236
|
+
const iconColor = success ? '#16a34a' : '#dc2626';
|
|
237
|
+
const icon = success
|
|
238
|
+
? `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>`
|
|
239
|
+
: `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`;
|
|
240
|
+
return `<!DOCTYPE html>
|
|
241
|
+
<html lang="en">
|
|
242
|
+
<head>
|
|
243
|
+
<meta charset="UTF-8">
|
|
244
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
245
|
+
<title>${title} - Event Modeler</title>
|
|
246
|
+
<style>
|
|
247
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
248
|
+
body {
|
|
249
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
250
|
+
display: flex;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
justify-content: center;
|
|
253
|
+
align-items: center;
|
|
254
|
+
min-height: 100vh;
|
|
255
|
+
background: #fafaf9;
|
|
256
|
+
color: #57534e;
|
|
257
|
+
padding: 1rem;
|
|
258
|
+
}
|
|
259
|
+
.card {
|
|
260
|
+
background: white;
|
|
261
|
+
border: 1px solid #e7e5e4;
|
|
262
|
+
border-radius: 12px;
|
|
263
|
+
padding: 2.5rem 3rem;
|
|
264
|
+
text-align: center;
|
|
265
|
+
max-width: 420px;
|
|
266
|
+
width: 100%;
|
|
267
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
268
|
+
}
|
|
269
|
+
.icon {
|
|
270
|
+
margin-bottom: 1.5rem;
|
|
271
|
+
}
|
|
272
|
+
.logo {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
justify-content: center;
|
|
276
|
+
gap: 0.5rem;
|
|
277
|
+
margin-bottom: 2rem;
|
|
278
|
+
font-weight: 600;
|
|
279
|
+
font-size: 1.125rem;
|
|
280
|
+
color: #44403c;
|
|
281
|
+
}
|
|
282
|
+
.logo svg {
|
|
283
|
+
width: 28px;
|
|
284
|
+
height: 28px;
|
|
285
|
+
}
|
|
286
|
+
h1 {
|
|
287
|
+
font-size: 1.5rem;
|
|
288
|
+
font-weight: 600;
|
|
289
|
+
color: #1c1917;
|
|
290
|
+
margin-bottom: 0.5rem;
|
|
291
|
+
}
|
|
292
|
+
.message {
|
|
293
|
+
color: #78716c;
|
|
294
|
+
margin-bottom: 1.5rem;
|
|
295
|
+
line-height: 1.5;
|
|
296
|
+
}
|
|
297
|
+
.hint {
|
|
298
|
+
font-size: 0.875rem;
|
|
299
|
+
color: #a8a29e;
|
|
300
|
+
padding-top: 1.5rem;
|
|
301
|
+
border-top: 1px solid #f5f5f4;
|
|
302
|
+
}
|
|
303
|
+
.email {
|
|
304
|
+
font-weight: 500;
|
|
305
|
+
color: #44403c;
|
|
306
|
+
}
|
|
307
|
+
</style>
|
|
308
|
+
</head>
|
|
309
|
+
<body>
|
|
310
|
+
<div class="card">
|
|
311
|
+
<div class="logo">
|
|
312
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
313
|
+
<path d="M50 50 L50 5 A45 45 0 0 1 89.0 72.5 Z" fill="#D5F1A5"/>
|
|
314
|
+
<path d="M50 50 L89.0 72.5 A45 45 0 0 1 11.0 72.5 Z" fill="#FFB87B"/>
|
|
315
|
+
<path d="M50 50 L11.0 72.5 A45 45 0 0 1 50 5 Z" fill="#B7D3FE"/>
|
|
316
|
+
</svg>
|
|
317
|
+
Event Modeler
|
|
318
|
+
</div>
|
|
319
|
+
<div class="icon">${icon}</div>
|
|
320
|
+
<h1>${title}</h1>
|
|
321
|
+
<p class="message">${message}</p>
|
|
322
|
+
<p class="hint">You can close this window and return to the terminal.</p>
|
|
323
|
+
</div>
|
|
324
|
+
</body>
|
|
325
|
+
</html>`;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Format the success message with email highlighted.
|
|
329
|
+
*/
|
|
330
|
+
function formatSuccessMessage(email) {
|
|
331
|
+
return `Signed in as <span class="email">${email}</span>`;
|
|
332
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EventModel } from '../types.js';
|
|
2
|
+
import { type CloudClient } from './cloud-client.js';
|
|
3
|
+
import { type ProjectConfig } from './project-config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Backend abstraction for CLI operations.
|
|
6
|
+
* Allows commands to work with either local files or cloud backend.
|
|
7
|
+
*/
|
|
8
|
+
export interface CliBackend {
|
|
9
|
+
/** Get the current event model */
|
|
10
|
+
getModel(): Promise<EventModel>;
|
|
11
|
+
/** Dispatch a command (for mutations) */
|
|
12
|
+
dispatch(command: unknown): Promise<void>;
|
|
13
|
+
/** Whether this is a cloud backend */
|
|
14
|
+
isCloud(): boolean;
|
|
15
|
+
/** Get the model name or file path */
|
|
16
|
+
getModelIdentifier(): string;
|
|
17
|
+
/** Append a raw event (for local backend only, used by existing commands) */
|
|
18
|
+
appendEvent?(event: unknown): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a local file-based backend.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createLocalBackend(filePath: string): CliBackend;
|
|
24
|
+
/**
|
|
25
|
+
* Create a cloud backend using the Axon backend.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createCloudBackendFromClient(client: CloudClient, modelId: string, modelName: string): CliBackend;
|
|
28
|
+
export interface ResolveBackendOptions {
|
|
29
|
+
/** Explicit file path (overrides auto-detection) */
|
|
30
|
+
filePath?: string;
|
|
31
|
+
/** Force cloud mode */
|
|
32
|
+
forceCloud?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve which backend to use based on:
|
|
36
|
+
* 1. Explicit -f <file> flag
|
|
37
|
+
* 2. .eventmodeler.json project config
|
|
38
|
+
* 3. .eventmodel file in current directory (legacy)
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveBackend(options?: ResolveBackendOptions): Promise<CliBackend>;
|
|
41
|
+
/**
|
|
42
|
+
* Check if we're in a configured project.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isInConfiguredProject(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Get the project config if available.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getProjectConfig(): ProjectConfig | null;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { loadModel, appendEvent as appendEventToFile } from './file-loader.js';
|
|
2
|
+
import { createCloudClient } from './cloud-client.js';
|
|
3
|
+
import { loadProjectConfig } from './project-config.js';
|
|
4
|
+
import { findEventModelFile } from './file-loader.js';
|
|
5
|
+
import { isAuthenticated } from './config.js';
|
|
6
|
+
/**
|
|
7
|
+
* Create a local file-based backend.
|
|
8
|
+
*/
|
|
9
|
+
export function createLocalBackend(filePath) {
|
|
10
|
+
return {
|
|
11
|
+
async getModel() {
|
|
12
|
+
return loadModel(filePath);
|
|
13
|
+
},
|
|
14
|
+
async dispatch(_command) {
|
|
15
|
+
// Local backend doesn't use dispatch - existing commands use appendEvent directly
|
|
16
|
+
throw new Error('Local backend does not support dispatch. Use appendEvent instead.');
|
|
17
|
+
},
|
|
18
|
+
isCloud() {
|
|
19
|
+
return false;
|
|
20
|
+
},
|
|
21
|
+
getModelIdentifier() {
|
|
22
|
+
return filePath;
|
|
23
|
+
},
|
|
24
|
+
async appendEvent(event) {
|
|
25
|
+
appendEventToFile(filePath, event);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a cloud backend using the Axon backend.
|
|
31
|
+
*/
|
|
32
|
+
export function createCloudBackendFromClient(client, modelId, modelName) {
|
|
33
|
+
return {
|
|
34
|
+
async getModel() {
|
|
35
|
+
const model = await client.getModel(modelId);
|
|
36
|
+
if (!model) {
|
|
37
|
+
throw new Error(`Event model not found: ${modelId}`);
|
|
38
|
+
}
|
|
39
|
+
return model;
|
|
40
|
+
},
|
|
41
|
+
async dispatch(command) {
|
|
42
|
+
const result = await client.dispatch(modelId, command);
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
throw new Error('Command dispatch failed');
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
isCloud() {
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
getModelIdentifier() {
|
|
51
|
+
return `${modelName} (${modelId})`;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve which backend to use based on:
|
|
57
|
+
* 1. Explicit -f <file> flag
|
|
58
|
+
* 2. .eventmodeler.json project config
|
|
59
|
+
* 3. .eventmodel file in current directory (legacy)
|
|
60
|
+
*/
|
|
61
|
+
export async function resolveBackend(options = {}) {
|
|
62
|
+
// 1. Explicit file path always wins
|
|
63
|
+
if (options.filePath) {
|
|
64
|
+
return createLocalBackend(options.filePath);
|
|
65
|
+
}
|
|
66
|
+
// 2. Check for project config
|
|
67
|
+
const projectConfig = loadProjectConfig();
|
|
68
|
+
if (projectConfig) {
|
|
69
|
+
if (projectConfig.type === 'local') {
|
|
70
|
+
return createLocalBackend(projectConfig.file);
|
|
71
|
+
}
|
|
72
|
+
if (projectConfig.type === 'cloud') {
|
|
73
|
+
if (!isAuthenticated()) {
|
|
74
|
+
throw new Error('Not authenticated. Run "eventmodeler login" first.');
|
|
75
|
+
}
|
|
76
|
+
const client = await createCloudClient();
|
|
77
|
+
return createCloudBackendFromClient(client, projectConfig.modelId, projectConfig.modelName);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// 3. Legacy: look for .eventmodel file in current directory
|
|
81
|
+
const localFile = await findEventModelFile();
|
|
82
|
+
if (localFile) {
|
|
83
|
+
return createLocalBackend(localFile);
|
|
84
|
+
}
|
|
85
|
+
// 4. No backend found
|
|
86
|
+
throw new Error('No event model found.\n\n' +
|
|
87
|
+
'Options:\n' +
|
|
88
|
+
' - Run "eventmodeler init" to set up a project\n' +
|
|
89
|
+
' - Use "-f <file>" to specify a local .eventmodel file\n' +
|
|
90
|
+
' - Create a .eventmodel file in the current directory');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if we're in a configured project.
|
|
94
|
+
*/
|
|
95
|
+
export function isInConfiguredProject() {
|
|
96
|
+
return loadProjectConfig() !== null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get the project config if available.
|
|
100
|
+
*/
|
|
101
|
+
export function getProjectConfig() {
|
|
102
|
+
return loadProjectConfig();
|
|
103
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { EventModel, RawEvent } from '../types.js';
|
|
2
|
+
export interface ImportResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
modelId: string;
|
|
5
|
+
eventCount: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CloudClient {
|
|
8
|
+
listModels(): Promise<CloudModelInfo[]>;
|
|
9
|
+
getModel(modelId: string): Promise<EventModel | null>;
|
|
10
|
+
dispatch(modelId: string, command: unknown): Promise<{
|
|
11
|
+
success: boolean;
|
|
12
|
+
events: unknown[];
|
|
13
|
+
}>;
|
|
14
|
+
importModel(modelId: string, modelName: string, events: RawEvent[]): Promise<ImportResult>;
|
|
15
|
+
addField(modelId: string, elementType: string, elementName: string, field: Record<string, unknown>): Promise<{
|
|
16
|
+
success: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
removeField(modelId: string, elementType: string, elementName: string, fieldName: string): Promise<{
|
|
19
|
+
success: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
updateField(modelId: string, elementType: string, elementName: string, fieldName: string, updates: Record<string, unknown>): Promise<{
|
|
22
|
+
success: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
createStateChangeSlice(modelId: string, sliceName: string, after: string, before: string, screen: any, command: any, event: any): Promise<{
|
|
25
|
+
sliceId: string;
|
|
26
|
+
screenId: string;
|
|
27
|
+
commandId: string;
|
|
28
|
+
eventId: string;
|
|
29
|
+
}>;
|
|
30
|
+
createStateViewSlice(modelId: string, sliceName: string, after: string, before: string, readModel: any): Promise<{
|
|
31
|
+
sliceId: string;
|
|
32
|
+
readModelId: string;
|
|
33
|
+
}>;
|
|
34
|
+
createAutomationSlice(modelId: string, sliceName: string, after: string, before: string, readModel: any, processor: any, command: any, event: any): Promise<{
|
|
35
|
+
sliceId: string;
|
|
36
|
+
readModelId: string;
|
|
37
|
+
processorId: string;
|
|
38
|
+
commandId: string;
|
|
39
|
+
eventId: string;
|
|
40
|
+
}>;
|
|
41
|
+
createFlow(modelId: string, fromName: string, toName: string): Promise<{
|
|
42
|
+
flowId: string;
|
|
43
|
+
}>;
|
|
44
|
+
markSliceStatus(modelId: string, sliceName: string, status: string): Promise<{
|
|
45
|
+
success: boolean;
|
|
46
|
+
}>;
|
|
47
|
+
createScenario(modelId: string, sliceName: string, scenario: {
|
|
48
|
+
name: string;
|
|
49
|
+
}): Promise<{
|
|
50
|
+
scenarioId: string;
|
|
51
|
+
}>;
|
|
52
|
+
removeScenario(modelId: string, scenarioName: string, sliceName?: string): Promise<{
|
|
53
|
+
success: boolean;
|
|
54
|
+
}>;
|
|
55
|
+
mapFields(modelId: string, sourceName: string, targetName: string, mappings: Array<{
|
|
56
|
+
from: string;
|
|
57
|
+
to: string;
|
|
58
|
+
}>): Promise<{
|
|
59
|
+
success: boolean;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
export interface CloudModelInfo {
|
|
63
|
+
modelId: string;
|
|
64
|
+
name: string;
|
|
65
|
+
updatedAt?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create a cloud client for interacting with the Axon backend.
|
|
69
|
+
*/
|
|
70
|
+
export declare function createCloudClient(): Promise<CloudClient>;
|