@statelyai/sdk 0.5.0 → 0.5.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/README.md CHANGED
@@ -69,6 +69,7 @@ The common environment variables are:
69
69
  | Variable | Purpose |
70
70
  | --- | --- |
71
71
  | `AUTH_PROVIDER` | Auth strategy used by the editor host |
72
+ | `EDITOR_SYNC_AUTH_REQUIRED` | Set to `false` to disable API-key checks for editor-sync endpoints only |
72
73
  | `STATELY_API_KEY` | Server-side API key for Stately data fetching |
73
74
  | `STATELY_API_URL` | Stately API base URL override |
74
75
  | `NEXT_PUBLIC_BASE_URL` | Public-facing editor URL |
@@ -354,7 +355,7 @@ const aslYaml = await embed.export('asl-yaml');
354
355
 
355
356
  <!-- supported export formats from ExportFormatMap in src/protocol.ts -->
356
357
 
357
- Supported formats: `xstate`, `json`, `digraph`, `mermaid`, `rtk`, `zustand`, `asl-json`, `asl-yaml`, `scxml`
358
+ Supported formats: `xstate`, `json`, `xgraph`, `digraph`, `mermaid`, `rtk`, `zustand`, `asl-json`, `asl-yaml`, `scxml`
358
359
 
359
360
  #### `embed.on(event, handler)` / `embed.off(event, handler)`
360
361
 
@@ -507,11 +508,11 @@ Available commands:
507
508
 
508
509
  Common flags:
509
510
 
510
- - `--api-key` for remote machine resolution
511
+ - `--api-key` for remote machine resolution or editor servers that require auth
511
512
  - `--base-url` for self-hosted or non-default Stately deployments
512
513
  - `--fail-on-changes` to return a nonzero exit code when a diff is detected
513
514
 
514
- `statelyai open` also supports `--editor-url`, `--host`, `--port`, `--no-open`, and `--debug`. It watches the local file on disk, updates the visual editor after saved source changes, and writes saved visual edits back to source.
515
+ `statelyai open` also supports `--api-key`, `--editor-url`, `--host`, `--port`, `--no-open`, and `--debug`. It watches the local file on disk and sends source snapshots to `/api/editor-sync/*` endpoints, which return the text replacements to apply locally. Pass `--api-key` or set `STATELY_API_KEY` when the editor server requires auth. Self-hosted servers can disable editor-sync API-key checks with `EDITOR_SYNC_AUTH_REQUIRED=false`. The private source reconciliation engine is not bundled into the published CLI.
515
516
 
516
517
  ## Transport Helpers
517
518
 
package/dist/cli.mjs CHANGED
@@ -1,11 +1,651 @@
1
1
  #!/usr/bin/env node
2
2
  import { planSync, pullSync } from "./sync.mjs";
3
+ import fs from "node:fs/promises";
4
+ import * as path$1 from "node:path";
3
5
  import path from "node:path";
4
- import fs from "node:fs";
5
- import { fileURLToPath } from "node:url";
6
+ import fs$1, { watch } from "node:fs";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
8
  import { Args, Command, Flags, flush, handle, run as run$1 } from "@oclif/core";
7
- import { openEditor } from "@statelyai/editor-sync/cli-host";
9
+ import * as crypto from "node:crypto";
10
+ import { spawn } from "node:child_process";
11
+ import * as http from "node:http";
8
12
 
13
+ //#region src/cliHost.ts
14
+ const DEFAULT_SYNC_MAX_FILES = 150;
15
+ const DEFAULT_SYNC_MAX_BYTES = 2e6;
16
+ const supportedDocumentPattern = /\.(?:c|m)?(?:jsx?|tsx?)$|\.jsonc?$|\.ya?ml$|\.mmd$|\.mermaid$/i;
17
+ var WebSocketClient = class {
18
+ buffer = Buffer.alloc(0);
19
+ listeners = /* @__PURE__ */ new Set();
20
+ constructor(socket) {
21
+ this.socket = socket;
22
+ socket.on("data", (chunk) => {
23
+ const nextChunk = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
24
+ this.buffer = Buffer.concat([this.buffer, nextChunk]);
25
+ this.readFrames();
26
+ });
27
+ }
28
+ onMessage(listener) {
29
+ this.listeners.add(listener);
30
+ }
31
+ send(message) {
32
+ this.socket.write(encodeWebSocketFrame(JSON.stringify(message)));
33
+ }
34
+ close() {
35
+ this.socket.end();
36
+ }
37
+ readFrames() {
38
+ while (this.buffer.length >= 2) {
39
+ const first = this.buffer[0];
40
+ const second = this.buffer[1];
41
+ const opcode = first & 15;
42
+ const masked = (second & 128) !== 0;
43
+ let length = second & 127;
44
+ let offset = 2;
45
+ if (length === 126) {
46
+ if (this.buffer.length < offset + 2) return;
47
+ length = this.buffer.readUInt16BE(offset);
48
+ offset += 2;
49
+ } else if (length === 127) {
50
+ if (this.buffer.length < offset + 8) return;
51
+ const bigLength = this.buffer.readBigUInt64BE(offset);
52
+ if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) {
53
+ this.socket.destroy(/* @__PURE__ */ new Error("WebSocket frame is too large."));
54
+ return;
55
+ }
56
+ length = Number(bigLength);
57
+ offset += 8;
58
+ }
59
+ const maskLength = masked ? 4 : 0;
60
+ if (this.buffer.length < offset + maskLength + length) return;
61
+ const mask = masked ? this.buffer.subarray(offset, offset + 4) : void 0;
62
+ offset += maskLength;
63
+ const payload = Buffer.from(this.buffer.subarray(offset, offset + length));
64
+ this.buffer = this.buffer.subarray(offset + length);
65
+ if (mask) for (let index = 0; index < payload.length; index++) payload[index] = payload[index] ^ mask[index % 4];
66
+ if (opcode === 8) {
67
+ this.close();
68
+ return;
69
+ }
70
+ if (opcode === 9) {
71
+ this.socket.write(encodeWebSocketFrame(payload, 10));
72
+ continue;
73
+ }
74
+ if (opcode !== 1) continue;
75
+ const message = JSON.parse(payload.toString("utf8"));
76
+ for (const listener of this.listeners) listener(message);
77
+ }
78
+ }
79
+ };
80
+ var RemoteEditorSession = class {
81
+ currentGraph;
82
+ lastPersistedGraph;
83
+ currentSourceLocations;
84
+ currentFormat;
85
+ ready = false;
86
+ pendingMessages = [];
87
+ pendingRetrievals = /* @__PURE__ */ new Map();
88
+ retrieveRequestCount = 0;
89
+ watchedSourceUris = /* @__PURE__ */ new Set();
90
+ selfEditCount = 0;
91
+ watcher;
92
+ constructor(options, sendMessage) {
93
+ this.options = options;
94
+ this.sendMessage = sendMessage;
95
+ this.watcher = watchWorkspace(this.options.rootDir, (uri) => {
96
+ this.handleSavedDocument(uri);
97
+ });
98
+ }
99
+ dispose() {
100
+ for (const pending of this.pendingRetrievals.values()) pending.reject(/* @__PURE__ */ new Error("Editor session disposed before export completed."));
101
+ this.pendingRetrievals.clear();
102
+ this.watcher?.dispose();
103
+ }
104
+ async receiveMessage(msg) {
105
+ this.log("recv", msg.type, msg);
106
+ switch (msg.type) {
107
+ case "@statelyai.ready":
108
+ await this.initializeEditor();
109
+ break;
110
+ case "@statelyai.loaded":
111
+ this.currentGraph = msg.graph;
112
+ this.lastPersistedGraph = msg.graph;
113
+ this.currentSourceLocations = msg.sourceLocations ?? this.currentSourceLocations;
114
+ this.updateSourceLocations(this.currentSourceLocations);
115
+ break;
116
+ case "@statelyai.change":
117
+ this.currentGraph = msg.graph;
118
+ this.currentSourceLocations = msg.sourceLocations ?? this.currentSourceLocations;
119
+ this.updateSourceLocations(this.currentSourceLocations);
120
+ break;
121
+ case "@statelyai.save":
122
+ await this.saveFromEditor(msg);
123
+ break;
124
+ case "@statelyai.retrieved":
125
+ this.resolveRetrievedSource(msg);
126
+ break;
127
+ case "@statelyai.clipboardRead":
128
+ this.postMessage({
129
+ type: "@statelyai.clipboardReadResponse",
130
+ requestId: msg.requestId,
131
+ text: ""
132
+ });
133
+ break;
134
+ case "@statelyai.error":
135
+ this.handleErrorMessage(msg);
136
+ break;
137
+ }
138
+ }
139
+ async initializeEditor() {
140
+ this.ready = true;
141
+ try {
142
+ const update = await this.parseRootDocument();
143
+ this.currentFormat = update.format;
144
+ this.currentSourceLocations = update.sourceLocations;
145
+ this.updateSourceLocations(update.sourceLocations);
146
+ this.postMessage({
147
+ type: "@statelyai.init",
148
+ machine: update.machine,
149
+ format: update.format,
150
+ sourceLocations: update.sourceLocations,
151
+ mode: "editing",
152
+ theme: "light",
153
+ unsavedIndicator: { enabled: true },
154
+ leftPanels: [],
155
+ rightPanels: [],
156
+ activePanels: []
157
+ });
158
+ } catch (error) {
159
+ this.showError(formatError(error, "Failed to initialize the visual editor."));
160
+ return;
161
+ }
162
+ for (const pending of this.pendingMessages) this.sendMessage(pending);
163
+ this.pendingMessages = [];
164
+ }
165
+ async handleSavedDocument(uri) {
166
+ if (uri !== this.options.rootUri && !this.watchedSourceUris.has(uri)) return;
167
+ if (this.selfEditCount > 0) return;
168
+ try {
169
+ const update = await this.parseRootDocument();
170
+ this.currentFormat = update.format;
171
+ this.currentSourceLocations = update.sourceLocations;
172
+ this.updateSourceLocations(update.sourceLocations);
173
+ this.postMessage({
174
+ type: "@statelyai.update",
175
+ machine: update.machine,
176
+ format: update.format,
177
+ sourceLocations: update.sourceLocations
178
+ });
179
+ } catch (error) {
180
+ this.showError(formatError(error, "Failed to read the saved document."));
181
+ }
182
+ }
183
+ async saveFromEditor(msg) {
184
+ const validationErrors = getValidationErrors(msg.validations);
185
+ if (validationErrors.length > 0) {
186
+ const message = formatValidationErrorMessage(validationErrors);
187
+ this.postMessage({
188
+ type: "@statelyai.toast",
189
+ message,
190
+ toastType: "error"
191
+ });
192
+ this.showError(message);
193
+ return;
194
+ }
195
+ this.currentSourceLocations = msg.sourceLocations ?? this.currentSourceLocations;
196
+ this.updateSourceLocations(this.currentSourceLocations);
197
+ try {
198
+ const serialized = await this.getSerializedPayloadForCurrentFormat();
199
+ const documents = await collectSourceDocuments({
200
+ rootFileName: this.options.fileName,
201
+ rootDir: this.options.rootDir,
202
+ extraUris: sourceLocationUris(this.currentSourceLocations)
203
+ });
204
+ const response = await this.postSync("/api/editor-sync/apply", {
205
+ rootUri: this.options.rootUri,
206
+ rootFileName: this.options.fileName,
207
+ rootDir: this.options.rootDir,
208
+ documents,
209
+ machineConfig: msg.machineConfig,
210
+ patches: msg.patches,
211
+ prevGraph: this.lastPersistedGraph ?? this.currentGraph,
212
+ nextGraph: msg.graph,
213
+ sourceLocations: this.currentSourceLocations,
214
+ serialized
215
+ });
216
+ if (response.replacements.length === 0) {
217
+ const message = "Unable to apply Viz changes to the source file automatically.";
218
+ this.postMessage({
219
+ type: "@statelyai.toast",
220
+ message,
221
+ toastType: "error"
222
+ });
223
+ this.showError(message);
224
+ return;
225
+ }
226
+ await this.runWithSelfEdit(async () => {
227
+ await applyFileReplacements(response.replacements, this.options.rootUri);
228
+ });
229
+ this.currentGraph = msg.graph;
230
+ this.lastPersistedGraph = msg.graph;
231
+ } catch (error) {
232
+ const message = formatError(error, "Failed to apply Viz changes.");
233
+ this.postMessage({
234
+ type: "@statelyai.toast",
235
+ message,
236
+ toastType: "error"
237
+ });
238
+ this.showError(message);
239
+ }
240
+ }
241
+ async parseRootDocument() {
242
+ const documents = await collectSourceDocuments({
243
+ rootFileName: this.options.fileName,
244
+ rootDir: this.options.rootDir,
245
+ extraUris: sourceLocationUris(this.currentSourceLocations)
246
+ });
247
+ return this.postSync("/api/editor-sync/parse", {
248
+ rootUri: this.options.rootUri,
249
+ rootFileName: this.options.fileName,
250
+ rootDir: this.options.rootDir,
251
+ documents
252
+ });
253
+ }
254
+ async postSync(pathname, body) {
255
+ const url = new URL(pathname, normalizedBaseUrl(this.options.editorUrl));
256
+ const headers = { "Content-Type": "application/json" };
257
+ if (this.options.apiKey) headers.Authorization = `Bearer ${this.options.apiKey}`;
258
+ const response = await fetch(url, {
259
+ method: "POST",
260
+ headers,
261
+ body: JSON.stringify(body)
262
+ });
263
+ const data = await response.json().catch(() => null);
264
+ if (!response.ok) {
265
+ const message = data && typeof data === "object" && "error" in data && typeof data.error === "string" ? data.error : `HTTP ${response.status}`;
266
+ throw new Error(message);
267
+ }
268
+ return data;
269
+ }
270
+ async getSerializedPayloadForCurrentFormat() {
271
+ const format = this.currentFormat;
272
+ if (!format || format === "xstate") return;
273
+ try {
274
+ const serialized = await this.requestSerialized(format);
275
+ return { [format]: serialized };
276
+ } catch {
277
+ return;
278
+ }
279
+ }
280
+ requestSerialized(format) {
281
+ const requestId = `retrieve-${++this.retrieveRequestCount}`;
282
+ return new Promise((resolve, reject) => {
283
+ this.pendingRetrievals.set(requestId, {
284
+ resolve,
285
+ reject
286
+ });
287
+ this.postMessage({
288
+ type: "@statelyai.retrieve",
289
+ requestId,
290
+ format
291
+ });
292
+ });
293
+ }
294
+ resolveRetrievedSource(msg) {
295
+ const pending = this.pendingRetrievals.get(msg.requestId);
296
+ if (!pending) return;
297
+ this.pendingRetrievals.delete(msg.requestId);
298
+ const serialized = typeof msg.data === "string" ? msg.data : JSON.stringify(msg.data, null, 2);
299
+ pending.resolve(serialized);
300
+ }
301
+ handleErrorMessage(msg) {
302
+ if (msg.requestId) {
303
+ const pending = this.pendingRetrievals.get(msg.requestId);
304
+ if (pending) {
305
+ this.pendingRetrievals.delete(msg.requestId);
306
+ pending.reject(new Error(msg.message));
307
+ return;
308
+ }
309
+ }
310
+ this.showError(msg.message);
311
+ }
312
+ updateSourceLocations(sourceLocations) {
313
+ this.watchedSourceUris.clear();
314
+ for (const location of sourceLocations?.states ?? []) if (location.uri !== this.options.rootUri) this.watchedSourceUris.add(location.uri);
315
+ }
316
+ async runWithSelfEdit(operation) {
317
+ this.selfEditCount++;
318
+ try {
319
+ return await operation();
320
+ } finally {
321
+ this.selfEditCount--;
322
+ }
323
+ }
324
+ postMessage(msg) {
325
+ this.log("send", msg.type, msg);
326
+ if (!this.ready) {
327
+ this.pendingMessages.push(msg);
328
+ return;
329
+ }
330
+ this.sendMessage(msg);
331
+ }
332
+ showError(message) {
333
+ console.error(`Stately Editor: ${message}`);
334
+ }
335
+ log(direction, type, payload) {
336
+ if (this.options.debug) console.error(`[${direction}] ${type}`, payload ?? "");
337
+ }
338
+ };
339
+ async function openEditor(options) {
340
+ const fileName = path$1.resolve(options.fileName);
341
+ await fs.access(fileName);
342
+ const rootUri = fileNameToUri(fileName);
343
+ const rootDir = path$1.dirname(fileName);
344
+ let activeClient;
345
+ let session;
346
+ const server = http.createServer((request, response) => {
347
+ if (new URL(request.url ?? "/", `http://${request.headers.host}`).pathname !== "/") {
348
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
349
+ response.end("Not found");
350
+ return;
351
+ }
352
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
353
+ response.end(getCliWebviewContent({
354
+ editorUrl: options.editorUrl,
355
+ apiKey: options.apiKey
356
+ }));
357
+ });
358
+ server.on("upgrade", (request, socket) => {
359
+ if (request.url !== "/ws") {
360
+ socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
361
+ return;
362
+ }
363
+ const key = request.headers["sec-websocket-key"];
364
+ if (typeof key !== "string") {
365
+ socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
366
+ return;
367
+ }
368
+ const accept = crypto.createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
369
+ socket.write([
370
+ "HTTP/1.1 101 Switching Protocols",
371
+ "Upgrade: websocket",
372
+ "Connection: Upgrade",
373
+ `Sec-WebSocket-Accept: ${accept}`,
374
+ "\r\n"
375
+ ].join("\r\n"));
376
+ activeClient?.close();
377
+ session?.dispose();
378
+ activeClient = new WebSocketClient(socket);
379
+ session = new RemoteEditorSession({
380
+ ...options,
381
+ fileName,
382
+ rootUri,
383
+ rootDir
384
+ }, (message) => activeClient?.send(message));
385
+ activeClient.onMessage((message) => {
386
+ session?.receiveMessage(message);
387
+ });
388
+ });
389
+ await listen(server, options.port, options.host);
390
+ const address = server.address();
391
+ const port = typeof address === "object" && address ? address.port : options.port;
392
+ const editorPageUrl = `http://${options.host}:${port}`;
393
+ console.log(`Stately visual editor: ${editorPageUrl}`);
394
+ console.log(`Editing ${fileName}`);
395
+ if (options.shouldOpen) openBrowser(editorPageUrl);
396
+ const shutdown = () => {
397
+ session?.dispose();
398
+ activeClient?.close();
399
+ server.close();
400
+ };
401
+ process.once("SIGINT", () => {
402
+ shutdown();
403
+ process.exit(0);
404
+ });
405
+ process.once("SIGTERM", () => {
406
+ shutdown();
407
+ process.exit(0);
408
+ });
409
+ }
410
+ async function collectSourceDocuments(options) {
411
+ const files = new Set([options.rootFileName]);
412
+ const maxFiles = Number.parseInt(process.env.STATELY_SYNC_MAX_FILES ?? "", 10) || DEFAULT_SYNC_MAX_FILES;
413
+ const maxBytes = Number.parseInt(process.env.STATELY_SYNC_MAX_BYTES ?? "", 10) || DEFAULT_SYNC_MAX_BYTES;
414
+ for (const uri of options.extraUris ?? []) files.add(sourceUriToFileName(uri));
415
+ await collectDirectoryFiles(options.rootDir, files, maxFiles);
416
+ const documents = [];
417
+ let totalBytes = 0;
418
+ for (const fileName of files) {
419
+ if (!isSupportedDocument(fileName)) continue;
420
+ const text = await fs.readFile(fileName, "utf8").catch(() => null);
421
+ if (text === null) continue;
422
+ totalBytes += Buffer.byteLength(text);
423
+ if (totalBytes > maxBytes) throw new Error(`Source payload exceeds ${maxBytes} bytes.`);
424
+ documents.push({
425
+ uri: fileNameToUri(fileName),
426
+ fileName,
427
+ text
428
+ });
429
+ }
430
+ return documents;
431
+ }
432
+ async function collectDirectoryFiles(rootDir, files, maxFiles) {
433
+ const queue = [rootDir];
434
+ while (queue.length > 0 && files.size < maxFiles) {
435
+ const dir = queue.shift();
436
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
437
+ for (const entry of entries) {
438
+ if (files.size >= maxFiles) break;
439
+ if (entry.name.startsWith(".") || shouldSkipDirectory(entry.name)) continue;
440
+ const fileName = path$1.join(dir, entry.name);
441
+ if (entry.isDirectory()) queue.push(fileName);
442
+ else if (entry.isFile() && isSupportedDocument(fileName)) files.add(fileName);
443
+ }
444
+ }
445
+ }
446
+ function shouldSkipDirectory(name) {
447
+ return [
448
+ "node_modules",
449
+ "dist",
450
+ "out",
451
+ "build",
452
+ "coverage",
453
+ ".git"
454
+ ].includes(name);
455
+ }
456
+ function isSupportedDocument(fileName) {
457
+ return supportedDocumentPattern.test(fileName);
458
+ }
459
+ async function applyFileReplacements(replacements, defaultUri) {
460
+ const replacementsByUri = /* @__PURE__ */ new Map();
461
+ for (const replacement of replacements) {
462
+ const uri = replacement.uri ?? defaultUri;
463
+ const existing = replacementsByUri.get(uri) ?? [];
464
+ existing.push(replacement);
465
+ replacementsByUri.set(uri, existing);
466
+ }
467
+ for (const [uri, uriReplacements] of replacementsByUri) {
468
+ const fileName = sourceUriToFileName(uri);
469
+ const nextText = applyReplacementsToText(await fs.readFile(fileName, "utf8"), uriReplacements);
470
+ await fs.writeFile(fileName, nextText, "utf8");
471
+ }
472
+ }
473
+ function applyReplacementsToText(text, replacements) {
474
+ let updated = text;
475
+ const sorted = [...replacements].sort(compareReplacementRangesDescending);
476
+ for (const replacement of sorted) {
477
+ const lineStarts = getLineStarts(updated);
478
+ const start = offsetAt(lineStarts, replacement.range.startLine, replacement.range.startChar);
479
+ const end = offsetAt(lineStarts, replacement.range.endLine, replacement.range.endChar);
480
+ updated = `${updated.slice(0, start)}${replacement.newText}${updated.slice(end)}`;
481
+ }
482
+ return updated;
483
+ }
484
+ function watchWorkspace(rootDir, listener) {
485
+ let watcher;
486
+ const timers = /* @__PURE__ */ new Map();
487
+ const queue = (fileName) => {
488
+ const existing = timers.get(fileName);
489
+ if (existing) clearTimeout(existing);
490
+ timers.set(fileName, setTimeout(() => {
491
+ timers.delete(fileName);
492
+ listener(fileNameToUri(fileName));
493
+ }, 75));
494
+ };
495
+ try {
496
+ watcher = watch(rootDir, { recursive: true }, (_event, fileName) => {
497
+ if (fileName) queue(path$1.join(rootDir, fileName.toString()));
498
+ });
499
+ } catch {
500
+ watcher = watch(rootDir, (_event, fileName) => {
501
+ if (fileName) queue(path$1.join(rootDir, fileName.toString()));
502
+ });
503
+ }
504
+ return { dispose() {
505
+ watcher?.close();
506
+ for (const timer of timers.values()) clearTimeout(timer);
507
+ timers.clear();
508
+ } };
509
+ }
510
+ function getCliWebviewContent(options) {
511
+ const baseUrl = normalizedBaseUrl(options.editorUrl);
512
+ const url = new URL(`${baseUrl}/embed`);
513
+ if (options.apiKey) url.searchParams.set("api_key", options.apiKey);
514
+ return `<!DOCTYPE html>
515
+ <html lang="en" style="height:100%;margin:0">
516
+ <head>
517
+ <meta charset="UTF-8">
518
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
519
+ <style>
520
+ body { margin: 0; padding: 0; height: 100vh; overflow: hidden; }
521
+ iframe { border: 0; width: 100%; height: 100%; display: block; }
522
+ </style>
523
+ </head>
524
+ <body>
525
+ <iframe
526
+ id="stately-editor"
527
+ src="${escapeAttribute(url.toString())}"
528
+ allow="clipboard-read; clipboard-write"
529
+ ></iframe>
530
+ <script>
531
+ const iframe = document.getElementById('stately-editor');
532
+ const socket = new WebSocket(
533
+ (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/ws'
534
+ );
535
+ const pending = [];
536
+
537
+ function sendToCli(message) {
538
+ if (socket.readyState === WebSocket.OPEN) {
539
+ socket.send(JSON.stringify(message));
540
+ return;
541
+ }
542
+ pending.push(message);
543
+ }
544
+
545
+ socket.addEventListener('open', () => {
546
+ while (pending.length > 0) {
547
+ socket.send(JSON.stringify(pending.shift()));
548
+ }
549
+ });
550
+
551
+ socket.addEventListener('message', (event) => {
552
+ iframe.contentWindow?.postMessage(JSON.parse(event.data), '*');
553
+ });
554
+
555
+ window.addEventListener('message', (event) => {
556
+ if (!event.data?.type?.startsWith?.('@statelyai.')) {
557
+ return;
558
+ }
559
+ sendToCli(event.data);
560
+ });
561
+ <\/script>
562
+ </body>
563
+ </html>`;
564
+ }
565
+ function encodeWebSocketFrame(payload, opcode = 1) {
566
+ const data = typeof payload === "string" ? Buffer.from(payload) : payload;
567
+ const headerLength = data.length < 126 ? 2 : data.length <= 65535 ? 4 : 10;
568
+ const frame = Buffer.alloc(headerLength + data.length);
569
+ frame[0] = 128 | opcode;
570
+ if (data.length < 126) {
571
+ frame[1] = data.length;
572
+ data.copy(frame, 2);
573
+ } else if (data.length <= 65535) {
574
+ frame[1] = 126;
575
+ frame.writeUInt16BE(data.length, 2);
576
+ data.copy(frame, 4);
577
+ } else {
578
+ frame[1] = 127;
579
+ frame.writeBigUInt64BE(BigInt(data.length), 2);
580
+ data.copy(frame, 10);
581
+ }
582
+ return frame;
583
+ }
584
+ function listen(server, port, host) {
585
+ return new Promise((resolve) => {
586
+ server.listen(port, host, resolve);
587
+ });
588
+ }
589
+ function openBrowser(url) {
590
+ spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? [
591
+ "/c",
592
+ "start",
593
+ "",
594
+ url
595
+ ] : [url], {
596
+ detached: true,
597
+ stdio: "ignore"
598
+ }).unref();
599
+ }
600
+ function getValidationErrors(validations) {
601
+ return (validations ?? []).filter((validation) => validation.level === "error");
602
+ }
603
+ function formatValidationErrorMessage(errors) {
604
+ const [firstError] = errors;
605
+ if (!firstError) return "Cannot save Viz changes because the graph has validation errors.";
606
+ if (errors.length === 1) return `Cannot save Viz changes: ${firstError.message}`;
607
+ return `Cannot save Viz changes: ${errors.length} validation errors. First: ${firstError.message}`;
608
+ }
609
+ function sourceLocationUris(sourceLocations) {
610
+ return [
611
+ ...sourceLocations?.root ? [sourceLocations.root.uri] : [],
612
+ ...(sourceLocations?.states ?? []).map((state) => state.uri),
613
+ ...(sourceLocations?.staticValues ?? []).map((value) => value.uri)
614
+ ];
615
+ }
616
+ function fileNameToUri(fileName) {
617
+ return pathToFileURL(path$1.resolve(fileName)).toString();
618
+ }
619
+ function sourceUriToFileName(uriOrPath) {
620
+ if (uriOrPath.startsWith("file://")) return fileURLToPath(uriOrPath);
621
+ if (/^[a-z][a-z\d+.-]*:/i.test(uriOrPath)) throw new Error(`Unsupported non-file URI: ${uriOrPath}`);
622
+ return path$1.resolve(uriOrPath);
623
+ }
624
+ function getLineStarts(text) {
625
+ const lineStarts = [0];
626
+ for (let index = 0; index < text.length; index += 1) if (text.charCodeAt(index) === 10) lineStarts.push(index + 1);
627
+ return lineStarts;
628
+ }
629
+ function offsetAt(lineStarts, line, character) {
630
+ const lineStart = lineStarts[line];
631
+ if (lineStart === void 0) return lineStarts[lineStarts.length - 1] + character;
632
+ return lineStart + character;
633
+ }
634
+ function compareReplacementRangesDescending(left, right) {
635
+ if (left.range.startLine !== right.range.startLine) return right.range.startLine - left.range.startLine;
636
+ return right.range.startChar - left.range.startChar;
637
+ }
638
+ function normalizedBaseUrl(value) {
639
+ return value.replace(/\/+$/, "");
640
+ }
641
+ function formatError(error, fallback) {
642
+ return error instanceof Error ? error.message : fallback;
643
+ }
644
+ function escapeAttribute(value) {
645
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
646
+ }
647
+
648
+ //#endregion
9
649
  //#region src/cli.ts
10
650
  function loadLocalEnv() {
11
651
  if (typeof process.loadEnvFile !== "function") return;
@@ -136,7 +776,7 @@ var OpenCommand = class OpenCommand extends Command {
136
776
  }) };
137
777
  static flags = {
138
778
  help: Flags.help({ char: "h" }),
139
- "api-key": Flags.string({ description: "Stately API key used by the embedded editor" }),
779
+ "api-key": Flags.string({ description: "Stately API key used when the editor server requires auth" }),
140
780
  "editor-url": Flags.string({ description: "Base URL for the Stately editor embed" }),
141
781
  host: Flags.string({
142
782
  description: "Local server host",
@@ -190,7 +830,7 @@ function isDirectExecution() {
190
830
  if (!entryPath) return false;
191
831
  const modulePath = fileURLToPath(import.meta.url);
192
832
  try {
193
- return fs.realpathSync(entryPath) === fs.realpathSync(modulePath);
833
+ return fs$1.realpathSync(entryPath) === fs$1.realpathSync(modulePath);
194
834
  } catch {
195
835
  return path.resolve(entryPath) === modulePath;
196
836
  }
package/dist/embed.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as EmbedMode, c as ExportFormatMap, f as UploadResult, i as EmbedEventName, l as InitOptions, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig, u as MachineSourceLocations } from "./protocol-B1cNV7QB.mjs";
1
+ import { a as EmbedMode, c as ExportFormatMap, f as UploadResult, i as EmbedEventName, l as InitOptions, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig, u as MachineSourceLocations } from "./protocol-BPuwbNCz.mjs";
2
2
 
3
3
  //#region src/embed.d.ts
4
4
  interface AssetConfig {
package/dist/embed.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as toInitMessage, i as createPendingExportManager, r as createEventRegistry, t as createPostMessageTransport } from "./transport-lomH7b0v.mjs";
1
+ import { a as toInitMessage, i as createPendingExportManager, r as createEventRegistry, t as createPostMessageTransport } from "./transport-DoCHBLTu.mjs";
2
2
 
3
3
  //#region src/embed.ts
4
4
  function buildEmbedUrl(options) {
@@ -5,6 +5,7 @@
5
5
  * - Single-quoted strings
6
6
  * - Omits undefined values
7
7
  * - Supports RawCode for verbatim expressions
8
+ * - Supports inline expression directives for verbatim expressions
8
9
  */
9
10
  const VALID_IDENT = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
10
11
  var RawCode = class {
@@ -17,6 +18,7 @@ function raw(code) {
17
18
  }
18
19
  function serializeJS(value, indent = 0, step = 2) {
19
20
  if (value instanceof RawCode) return value.code;
21
+ if (isInlineExpressionDirective(value)) return value.expr;
20
22
  if (value === void 0) return "undefined";
21
23
  if (value === null) return "null";
22
24
  if (typeof value === "string") return `'${escapeString(value)}'`;
@@ -45,10 +47,18 @@ function serializeObject(obj, indent, step) {
45
47
  return `{\n${entries.map(([key, val]) => {
46
48
  const k = VALID_IDENT.test(key) ? key : `'${escapeString(key)}'`;
47
49
  const serialized = serializeJS(val, inner, step);
48
- if (val instanceof RawCode && serialized.includes("\n")) return `${pad}${k}: ${indentRawCode(serialized, inner)}`;
50
+ if (isRawExpression(val) && serialized.includes("\n")) return `${pad}${k}: ${indentRawCode(serialized, inner)}`;
49
51
  return `${pad}${k}: ${serialized}`;
50
52
  }).join(",\n")},\n${closePad}}`;
51
53
  }
54
+ function isRawExpression(value) {
55
+ return value instanceof RawCode || isInlineExpressionDirective(value);
56
+ }
57
+ function isInlineExpressionDirective(value) {
58
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
59
+ const directive = value;
60
+ return directive["@type"] === "code" && typeof directive.expr === "string";
61
+ }
52
62
  function indentRawCode(code, indent) {
53
63
  const lines = code.split("\n");
54
64
  if (lines.length <= 1) return code;
@@ -250,7 +260,8 @@ function graphToMachineConfig(graph, options = {}) {
250
260
  ...node.data.invokes?.length ? { invoke: node.data.invokes.map((inv) => ({
251
261
  src: inv.src,
252
262
  id: inv.id,
253
- ...inv.input ? { input: inv.input } : void 0
263
+ ...inv.input ? { input: inv.input } : void 0,
264
+ ...inv.output ? { output: inv.output } : void 0
254
265
  })) } : void 0,
255
266
  ...tags?.length ? { tags: tags.map(tagToString) } : void 0,
256
267
  ...showDescriptions && node.data.description ? { description: node.data.description } : void 0,
package/dist/index.d.mts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { C as EventTypeData, S as DigraphNodeConfig, _ as studioMachineConverter, a as StatelyGraphData, b as DigraphConfig, c as StatelyInvoke, d as StudioAction, f as StudioEdge, g as fromStudioMachine, h as StudioNode, i as StatelyGraph, l as StatelyNodeData, m as StudioMachine, o as StatelyGuard, r as StatelyEdgeData, t as StatelyAction, v as toStudioMachine, w as StateNodeJSONData, x as DigraphEdgeConfig, y as DigraphAction } from "./graph-BfezxFKJ.mjs";
2
2
  import { a as ProjectMachine, c as StudioClientOptions, i as ProjectData, l as VerifyApiKeyResponse, n as ExtractedMachine, o as StudioApiError, r as GetMachineOptions, s as StudioClient, t as ExtractMachinesResponse, u as createStatelyClient } from "./studio-D2uQhrvX.mjs";
3
3
  import { PlanSyncOptions, PullSyncResult, ResolvedSyncInput, SyncInputFormat, SyncPlan, SyncPlanSummary } from "./sync.mjs";
4
- import { a as EmbedMode, c as ExportFormatMap, f as UploadResult, i as EmbedEventName, l as InitOptions, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig } from "./protocol-B1cNV7QB.mjs";
4
+ import { a as EmbedMode, c as ExportFormatMap, f as UploadResult, i as EmbedEventName, l as InitOptions, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig } from "./protocol-BPuwbNCz.mjs";
5
5
  import { AssetConfig, StatelyEmbed, StatelyEmbedOptions, createStatelyEmbed } from "./embed.mjs";
6
- import { a as ManualActorOptions, c as createPostMessageTransport, i as InspectorEvents, l as createWebSocketTransport, n as CreateInspectorOptions, o as createStatelyInspector, r as Inspector, s as Transport, t as AdoptedActor } from "./inspect-WUC2inmJ.mjs";
6
+ import { a as ManualActorOptions, c as createPostMessageTransport, i as InspectorEvents, l as createWebSocketTransport, n as CreateInspectorOptions, o as createStatelyInspector, r as Inspector, s as Transport, t as AdoptedActor } from "./inspect-ttRIjoCu.mjs";
7
7
  import { ActionLocation, GraphPatch } from "./patchTypes.mjs";
8
8
  import { JSONSchema7 } from "json-schema";
9
9
  import { UnknownMachineConfig } from "xstate";
@@ -18,10 +18,23 @@ interface CodeGenGuard {
18
18
  code?: string;
19
19
  params?: Record<string, unknown>;
20
20
  }
21
+ /**
22
+ * Serialized inline expression directive.
23
+ *
24
+ * Code generation treats this object as JavaScript/TypeScript source, not as a
25
+ * machine config object. It is used for fields such as invoke `input` and
26
+ * `output` where XState accepts mapper expressions.
27
+ */
28
+ interface CodeExpression {
29
+ '@type': 'code';
30
+ lang: 'js' | 'ts';
31
+ expr: string;
32
+ }
21
33
  interface CodeGenInvoke {
22
34
  src: string;
23
35
  id: string;
24
- input?: unknown;
36
+ input?: Record<string, unknown> | CodeExpression;
37
+ output?: Record<string, unknown> | CodeExpression;
25
38
  }
26
39
  interface CodeGenNodeData {
27
40
  nodeId?: string | null;
@@ -126,6 +139,7 @@ declare function graphToXStateTS(graph: CodeGenGraph, options?: XStateTSOptions)
126
139
  * - Single-quoted strings
127
140
  * - Omits undefined values
128
141
  * - Supports RawCode for verbatim expressions
142
+ * - Supports inline expression directives for verbatim expressions
129
143
  */
130
144
  declare class RawCode {
131
145
  code: string;
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { n as createWebSocketTransport, t as createPostMessageTransport } from "./transport-lomH7b0v.mjs";
1
+ import { n as createWebSocketTransport, t as createPostMessageTransport } from "./transport-DoCHBLTu.mjs";
2
2
  import { createStatelyEmbed } from "./embed.mjs";
3
3
  import { createStatelyInspector } from "./inspect.mjs";
4
4
  import { StudioApiError, createStatelyClient } from "./studio.mjs";
5
5
  import { fromStudioMachine, studioMachineConverter, toStudioMachine } from "./graph.mjs";
6
- import { a as graphToMachineConfig, c as serializeJS, i as jsonSchemaToTSType, n as contextSchemaToTSType, o as RawCode, r as eventsSchemaToTSType, s as raw, t as graphToXStateTS } from "./graphToXStateTS-BSUj97r0.mjs";
6
+ import { a as graphToMachineConfig, c as serializeJS, i as jsonSchemaToTSType, n as contextSchemaToTSType, o as RawCode, r as eventsSchemaToTSType, s as raw, t as graphToXStateTS } from "./graphToXStateTS-CtecEESq.mjs";
7
7
 
8
8
  export { RawCode, StudioApiError, contextSchemaToTSType, createPostMessageTransport, createStatelyClient, createStatelyEmbed, createStatelyInspector, createWebSocketTransport, eventsSchemaToTSType, fromStudioMachine, graphToMachineConfig, graphToXStateTS, jsonSchemaToTSType, raw, serializeJS, studioMachineConverter, toStudioMachine };
@@ -1,4 +1,4 @@
1
- import { c as ExportFormatMap, d as ProtocolMessage, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat } from "./protocol-B1cNV7QB.mjs";
1
+ import { c as ExportFormatMap, d as ProtocolMessage, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat } from "./protocol-BPuwbNCz.mjs";
2
2
 
3
3
  //#region src/transport.d.ts
4
4
  interface Transport {
@@ -1,3 +1,3 @@
1
- import { a as EmbedMode, c as ExportFormatMap, i as EmbedEventName, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat } from "./protocol-B1cNV7QB.mjs";
2
- import { a as ManualActorOptions, i as InspectorEvents, n as CreateInspectorOptions, o as createStatelyInspector, r as Inspector, s as Transport, t as AdoptedActor } from "./inspect-WUC2inmJ.mjs";
1
+ import { a as EmbedMode, c as ExportFormatMap, i as EmbedEventName, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat } from "./protocol-BPuwbNCz.mjs";
2
+ import { a as ManualActorOptions, i as InspectorEvents, n as CreateInspectorOptions, o as createStatelyInspector, r as Inspector, s as Transport, t as AdoptedActor } from "./inspect-ttRIjoCu.mjs";
3
3
  export { AdoptedActor, CreateInspectorOptions, EmbedEventHandler, EmbedEventMap, EmbedEventName, EmbedMode, ExportCallOptions, ExportFormat, ExportFormatMap, Inspector, InspectorEvents, ManualActorOptions, Transport, createStatelyInspector };
package/dist/inspect.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as createPendingExportManager, n as createWebSocketTransport, r as createEventRegistry } from "./transport-lomH7b0v.mjs";
1
+ import { i as createPendingExportManager, n as createWebSocketTransport, r as createEventRegistry } from "./transport-DoCHBLTu.mjs";
2
2
 
3
3
  //#region src/inspect.ts
4
4
  const defaultSerializeSnapshot = (snapshot) => ({
@@ -8,6 +8,7 @@ type ActionLocation = {
8
8
  edgeId: string;
9
9
  group: 'transition';
10
10
  };
11
+ type ImplementationSourceType = 'action' | 'guard' | 'actor' | 'delay';
11
12
  type GraphPatch = {
12
13
  op: 'createNode';
13
14
  description?: string;
@@ -163,6 +164,29 @@ type GraphPatch = {
163
164
  description?: string;
164
165
  nodeId: string;
165
166
  invokeId: string;
167
+ } | {
168
+ op: 'createImplementation';
169
+ description?: string;
170
+ sourceType: ImplementationSourceType;
171
+ implementation: {
172
+ id: string;
173
+ name?: string;
174
+ code?: string;
175
+ };
176
+ } | {
177
+ op: 'updateImplementation';
178
+ description?: string;
179
+ sourceType: ImplementationSourceType;
180
+ implementationId: string;
181
+ data: {
182
+ name?: string;
183
+ code?: string;
184
+ };
185
+ } | {
186
+ op: 'deleteImplementation';
187
+ description?: string;
188
+ sourceType: ImplementationSourceType;
189
+ implementationId: string;
166
190
  } | {
167
191
  op: 'setGuard';
168
192
  description?: string;
@@ -222,4 +246,4 @@ type GraphPatch = {
222
246
  };
223
247
  };
224
248
  //#endregion
225
- export { ActionLocation, CanvasColor, EditorStateType, GraphPatch };
249
+ export { ActionLocation, CanvasColor, EditorStateType, GraphPatch, ImplementationSourceType };
@@ -62,6 +62,10 @@ interface ExportFormatMap {
62
62
  };
63
63
  result: Record<string, unknown>;
64
64
  };
65
+ xgraph: {
66
+ options: object;
67
+ result: Record<string, unknown>;
68
+ };
65
69
  digraph: {
66
70
  options: object;
67
71
  result: Record<string, unknown>;
package/dist/sync.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createStatelyClient } from "./studio.mjs";
2
2
  import { fromStudioMachine, toStudioMachine } from "./graph.mjs";
3
- import { t as graphToXStateTS } from "./graphToXStateTS-BSUj97r0.mjs";
3
+ import { t as graphToXStateTS } from "./graphToXStateTS-CtecEESq.mjs";
4
4
  import { getDiff, isEmptyDiff } from "@statelyai/graph";
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
@@ -1,5 +1,9 @@
1
1
  //#region src/clientUtils.ts
2
- const jsonResultFormats = new Set(["digraph", "json"]);
2
+ const jsonResultFormats = new Set([
3
+ "digraph",
4
+ "json",
5
+ "xgraph"
6
+ ]);
3
7
  function createRequestId() {
4
8
  const cryptoObject = globalThis.crypto;
5
9
  if (cryptoObject?.randomUUID) return cryptoObject.randomUUID();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"
@@ -63,8 +63,7 @@
63
63
  "@types/json-schema": "^7.0.15",
64
64
  "jsdom": "^27.4.0",
65
65
  "tsdown": "0.21.0-beta.2",
66
- "vitest": "^3.2.4",
67
- "@statelyai/editor-sync": "0.0.0"
66
+ "vitest": "^3.2.4"
68
67
  },
69
68
  "scripts": {
70
69
  "build": "tsdown",