@statelyai/sdk 0.7.0 → 0.7.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/sync.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CreateProjectInput, ProjectData, StudioClient, StudioMachineRecord, XStateVersion } from "./studio.mjs";
2
- import { b as DigraphConfig, i as StatelyGraph } from "./graph-DpBGHZwl.mjs";
2
+ import { b as DigraphConfig, i as StatelyGraph } from "./graph-zuNj3kfa.mjs";
3
3
  import { GraphDiff } from "@statelyai/graph";
4
4
 
5
5
  //#region src/sync.d.ts
package/dist/sync.mjs CHANGED
@@ -1,469 +1,5 @@
1
- import { createStatelyClient } from "./studio.mjs";
2
- import { d as upsertStatelyPragma, l as findStatelyPragmaAttachments, t as graphToXStateTS, u as getStatelyPragma } from "./graphToXStateTS-Gzh0ZqbN.mjs";
3
- import { fromStudioMachine, toStudioMachine } from "./graph.mjs";
4
- import { getDiff, isEmptyDiff } from "@statelyai/graph";
5
- import fs from "node:fs/promises";
6
- import path from "node:path";
7
- import { execFile } from "node:child_process";
8
- import { promisify } from "node:util";
1
+ import "./studio.mjs";
2
+ import "./graphToXStateTS-Gzh0ZqbN.mjs";
3
+ import { i as pushSync, n as pullSync, r as pushLocalMachineLinks, t as planSync } from "./sync-DLkTmSyA.mjs";
9
4
 
10
- //#region src/sync.ts
11
- const execFileAsync = promisify(execFile);
12
- function isUrl(value) {
13
- try {
14
- const url = new URL(value);
15
- return url.protocol === "http:" || url.protocol === "https:";
16
- } catch {
17
- return false;
18
- }
19
- }
20
- async function fileExists(filePath) {
21
- try {
22
- await fs.access(filePath);
23
- return true;
24
- } catch {
25
- return false;
26
- }
27
- }
28
- function parseGitHubRemote(remoteUrl) {
29
- const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
30
- if (httpsMatch) {
31
- const [, owner, repo] = httpsMatch;
32
- if (!owner || !repo) return null;
33
- return {
34
- url: `https://github.com/${owner}/${repo}`,
35
- owner,
36
- repo
37
- };
38
- }
39
- const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
40
- if (sshMatch) {
41
- const [, owner, repo] = sshMatch;
42
- if (!owner || !repo) return null;
43
- return {
44
- url: `https://github.com/${owner}/${repo}`,
45
- owner,
46
- repo
47
- };
48
- }
49
- return null;
50
- }
51
- async function inferConnectedRepo(filePath, cwd) {
52
- const workingDir = cwd ?? path.dirname(filePath);
53
- try {
54
- const [{ stdout: repoRootStdout }, { stdout: remoteStdout }, { stdout: branchStdout }, { stdout: treeShaStdout }] = await Promise.all([
55
- execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd: workingDir }),
56
- execFileAsync("git", [
57
- "remote",
58
- "get-url",
59
- "origin"
60
- ], { cwd: workingDir }),
61
- execFileAsync("git", ["branch", "--show-current"], { cwd: workingDir }),
62
- execFileAsync("git", ["rev-parse", "HEAD"], { cwd: workingDir })
63
- ]);
64
- const repoRoot = repoRootStdout.trim();
65
- const remote = parseGitHubRemote(remoteStdout.trim());
66
- const branch = branchStdout.trim();
67
- const treeSha = treeShaStdout.trim();
68
- if (!remote || !branch || !treeSha) return;
69
- const relativePath = path.relative(repoRoot, filePath).replace(/\\/g, "/");
70
- const relativeDir = path.dirname(relativePath).replace(/\\/g, "/");
71
- const selectedPaths = relativePath && relativePath !== "." ? [relativePath] : [];
72
- return {
73
- ...remote,
74
- branch,
75
- treeSha,
76
- autoSync: false,
77
- pathForNewFiles: relativeDir && relativeDir !== "." ? relativeDir : "src/stately-studio",
78
- selectedPaths
79
- };
80
- } catch {
81
- return;
82
- }
83
- }
84
- function normalizeActions(value) {
85
- if (value == null) return [];
86
- return (Array.isArray(value) ? value : [value]).map((item) => {
87
- if (typeof item === "string") return { type: item };
88
- if (typeof item === "object" && item !== null && "type" in item) {
89
- const type = item.type;
90
- const params = "params" in item && typeof item.params === "object" && item.params ? item.params : void 0;
91
- return {
92
- type,
93
- ...params ? { params } : {}
94
- };
95
- }
96
- return { type: String(item) };
97
- });
98
- }
99
- function normalizeTags(value) {
100
- if (!value) return [];
101
- if (Array.isArray(value)) return value.map((tag) => String(tag));
102
- return [String(value)];
103
- }
104
- function normalizeInvoke(value) {
105
- if (!value) return [];
106
- return (Array.isArray(value) ? value : [value]).map((invoke, index) => ({
107
- src: invoke.src,
108
- id: invoke.id ?? `invoke[${index}]`,
109
- ...invoke.input ? { input: invoke.input } : {}
110
- }));
111
- }
112
- function resolveTargetId(sourceParentPath, target) {
113
- if (!target) return;
114
- if (target.startsWith("#")) {
115
- const stripped = target.slice(1);
116
- const machineIndex = stripped.indexOf(".");
117
- if (machineIndex >= 0) return `(machine).${stripped.slice(machineIndex + 1)}`;
118
- return `(machine).${stripped}`;
119
- }
120
- if (target.startsWith("(machine)")) return target;
121
- if (target.includes(".")) return `(machine).${target}`;
122
- return `${sourceParentPath}.${target}`;
123
- }
124
- function appendTransitionEdges(edges, sourceId, sourceParentPath, eventType, transition, edgeCounts) {
125
- const transitions = Array.isArray(transition) ? transition : [transition];
126
- for (const item of transitions) {
127
- const normalized = typeof item === "string" ? { target: item } : item;
128
- const targetId = resolveTargetId(sourceParentPath, Array.isArray(normalized.target) ? normalized.target[0] : normalized.target);
129
- const groupKey = `${sourceId}:${eventType}`;
130
- const index = edgeCounts.get(groupKey) ?? 0;
131
- edgeCounts.set(groupKey, index + 1);
132
- edges.push({
133
- type: "edge",
134
- id: `${sourceId}#${eventType}[${index}]`,
135
- sourceId,
136
- targetId: targetId ?? sourceId,
137
- label: eventType,
138
- data: {
139
- eventType,
140
- transitionType: normalized.reenter === true ? "reenter" : normalized.internal === true || !targetId ? "targetless" : "normal",
141
- ...normalized.guard ? { guard: typeof normalized.guard === "string" ? { type: normalized.guard } : {
142
- type: normalized.guard.type,
143
- ...normalized.guard.params ? { params: normalized.guard.params } : {}
144
- } } : {},
145
- actions: normalizeActions(normalized.actions),
146
- ...normalized.description ? { description: normalized.description } : {}
147
- }
148
- });
149
- }
150
- }
151
- function fromXStateConfig(config) {
152
- const nodes = [];
153
- const edges = [];
154
- const edgeCounts = /* @__PURE__ */ new Map();
155
- function visitNode(key, nodeConfig, parentId, parentPath) {
156
- const nodeId = parentPath ? `${parentPath}.${key}` : "(machine)";
157
- const currentPath = parentPath ? nodeId : "(machine)";
158
- const initialId = nodeConfig.initial ? `${currentPath}.${nodeConfig.initial}` : void 0;
159
- nodes.push({
160
- type: "node",
161
- id: nodeId,
162
- parentId,
163
- label: key,
164
- ...initialId ? { initialNodeId: initialId } : {},
165
- data: {
166
- key,
167
- ...nodeConfig.type ? { type: nodeConfig.type } : {},
168
- ...nodeConfig.history ? { history: nodeConfig.history } : {},
169
- ...initialId ? { initialId } : {},
170
- entry: normalizeActions(nodeConfig.entry),
171
- exit: normalizeActions(nodeConfig.exit),
172
- invokes: normalizeInvoke(nodeConfig.invoke),
173
- tags: normalizeTags(nodeConfig.tags),
174
- ...nodeConfig.description ? { description: nodeConfig.description } : {}
175
- }
176
- });
177
- for (const [eventType, transition] of Object.entries(nodeConfig.on ?? {})) appendTransitionEdges(edges, nodeId, parentPath ?? "(machine)", eventType, transition, edgeCounts);
178
- if (nodeConfig.always) appendTransitionEdges(edges, nodeId, parentPath ?? "(machine)", "", nodeConfig.always, edgeCounts);
179
- for (const [childKey, childConfig] of Object.entries(nodeConfig.states ?? {})) visitNode(childKey, childConfig, nodeId, currentPath);
180
- }
181
- visitNode("(machine)", config, null, null);
182
- return {
183
- id: config.id ?? "machine",
184
- nodes,
185
- edges,
186
- data: {}
187
- };
188
- }
189
- async function pushLocalMachineLinks(options) {
190
- const client = options.client ?? createStatelyClient({
191
- apiKey: options.apiKey,
192
- baseUrl: options.baseUrl,
193
- fetch: options.fetch
194
- });
195
- const sourcePath = path.resolve(options.cwd ?? process.cwd(), options.source);
196
- const project = await resolvePushProject(client, sourcePath, options);
197
- if (!project.projectVersionId) throw new Error("Resolved project is missing projectVersionId.");
198
- const contents = await fs.readFile(sourcePath, "utf8");
199
- const attachments = findStatelyPragmaAttachments(contents, sourcePath);
200
- const extracted = await client.code.extractMachines(contents);
201
- if (attachments.length === 0 || extracted.machines.length === 0) return {
202
- sourcePath,
203
- project,
204
- created: [],
205
- updated: [],
206
- skipped: [{
207
- machineIndex: 0,
208
- reason: "No local machines were discovered in this file."
209
- }]
210
- };
211
- if (attachments.length !== extracted.machines.length) return {
212
- sourcePath,
213
- project,
214
- created: [],
215
- updated: [],
216
- skipped: [{
217
- machineIndex: 0,
218
- reason: "The local machine extractor did not align with the source attachments for this file."
219
- }]
220
- };
221
- let nextContents = contents;
222
- let outputPath;
223
- const created = [];
224
- const updated = [];
225
- const skipped = [];
226
- for (const [machineIndex, attachment] of attachments.entries()) {
227
- const extractedMachine = extracted.machines[machineIndex];
228
- if (!extractedMachine?.config) {
229
- skipped.push({
230
- machineIndex,
231
- reason: "No extracted machine config was available for this source."
232
- });
233
- continue;
234
- }
235
- if (attachment.pragma?.id) {
236
- const updatedMachine = await client.machines.update({
237
- id: attachment.pragma.id,
238
- definition: toStudioMachine(fromXStateConfig(extractedMachine.config))
239
- });
240
- updated.push({
241
- machineIndex,
242
- machine: updatedMachine
243
- });
244
- continue;
245
- }
246
- const machine = await client.machines.create({
247
- projectVersionId: project.projectVersionId,
248
- definition: toStudioMachine(fromXStateConfig(extractedMachine.config)),
249
- xstateVersion: Math.max(5, options.xstateVersion ?? 5)
250
- });
251
- nextContents = upsertStatelyPragma(nextContents, machine.id, {
252
- fileName: sourcePath,
253
- machineIndex
254
- });
255
- created.push({
256
- machineIndex,
257
- machine
258
- });
259
- }
260
- if (nextContents !== contents) {
261
- await fs.writeFile(sourcePath, nextContents, "utf8");
262
- outputPath = sourcePath;
263
- }
264
- return {
265
- sourcePath,
266
- project,
267
- created,
268
- updated,
269
- skipped,
270
- ...outputPath ? { outputPath } : {}
271
- };
272
- }
273
- async function resolveLocalFile(locator, options) {
274
- const filePath = path.resolve(options.cwd ?? process.cwd(), locator);
275
- const contents = await fs.readFile(filePath, "utf8");
276
- const extension = path.extname(filePath).toLowerCase();
277
- if (extension === ".ts" || extension === ".tsx" || extension === ".js" || extension === ".jsx") {
278
- const config = (await (options.client ?? createStatelyClient({
279
- apiKey: options.apiKey,
280
- baseUrl: options.baseUrl,
281
- fetch: options.fetch
282
- })).code.extractMachines(contents)).machines[0]?.config;
283
- if (!config) throw new Error(`No machines extracted from ${filePath}`);
284
- return {
285
- kind: "local-file",
286
- locator: filePath,
287
- format: "xstate",
288
- graph: fromXStateConfig(config)
289
- };
290
- }
291
- const parsed = JSON.parse(contents);
292
- if ("rootNode" in parsed && "edges" in parsed) return {
293
- kind: "local-file",
294
- locator: filePath,
295
- format: "digraph",
296
- graph: fromStudioMachine(parsed)
297
- };
298
- if ("nodes" in parsed && "edges" in parsed) return {
299
- kind: "local-file",
300
- locator: filePath,
301
- format: "graph",
302
- graph: parsed
303
- };
304
- if ("states" in parsed) return {
305
- kind: "local-file",
306
- locator: filePath,
307
- format: "xstate",
308
- graph: fromXStateConfig(parsed)
309
- };
310
- throw new Error(`Unsupported local sync input: ${filePath}`);
311
- }
312
- function inferWritableTargetFormat(filePath) {
313
- if (filePath.endsWith(".ts") || filePath.endsWith(".tsx") || filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "xstate";
314
- if (filePath.endsWith(".digraph.json")) return "digraph";
315
- if (filePath.endsWith(".graph.json")) return "graph";
316
- return null;
317
- }
318
- function serializeGraph(graph, format, options = {}) {
319
- switch (format) {
320
- case "digraph": return `${JSON.stringify(toStudioMachine(graph), null, 2)}\n`;
321
- case "graph": return `${JSON.stringify(graph, null, 2)}\n`;
322
- case "xstate": {
323
- const source = graphToXStateTS(graph);
324
- if (!options.remoteMachineId) return source;
325
- return upsertStatelyPragma(source, options.remoteMachineId, options.targetPath ? { fileName: options.targetPath } : {});
326
- }
327
- default: {
328
- const exhaustive = format;
329
- throw new Error(`Unsupported sync output format: ${exhaustive}`);
330
- }
331
- }
332
- }
333
- async function resolveRemoteMachine(machineId, baseUrl, kind, locator, options) {
334
- return {
335
- kind,
336
- locator,
337
- format: "digraph",
338
- graph: fromStudioMachine(await (options.client ?? createStatelyClient({
339
- apiKey: options.apiKey,
340
- baseUrl,
341
- fetch: options.fetch
342
- })).machines.get(machineId)),
343
- remoteMachineId: machineId
344
- };
345
- }
346
- async function resolveSyncInput(locator, options) {
347
- if (await fileExists(path.resolve(options.cwd ?? process.cwd(), locator))) return resolveLocalFile(locator, options);
348
- if (isUrl(locator)) {
349
- const url = new URL(locator);
350
- const machineId = url.pathname.split("/").filter(Boolean).at(-1);
351
- if (!machineId) throw new Error(`Could not resolve machine ID from URL: ${locator}`);
352
- return resolveRemoteMachine(machineId, url.origin, "studio-url", locator, options);
353
- }
354
- return resolveRemoteMachine(locator, options.baseUrl, "studio-machine-id", locator, options);
355
- }
356
- function summarizeDiff(diff) {
357
- const nodeChanges = diff.nodes.added.length + diff.nodes.removed.length + diff.nodes.updated.length;
358
- const edgeChanges = diff.edges.added.length + diff.edges.removed.length + diff.edges.updated.length;
359
- return {
360
- hasChanges: !isEmptyDiff(diff),
361
- nodeChanges,
362
- edgeChanges
363
- };
364
- }
365
- async function planSync(options) {
366
- const source = await resolveSyncInput(options.source, options);
367
- const target = await resolveSyncInput(options.target, options);
368
- const diff = getDiff(source.graph, target.graph);
369
- return {
370
- source,
371
- target,
372
- diff,
373
- summary: summarizeDiff(diff),
374
- warnings: []
375
- };
376
- }
377
- async function pullSync(options) {
378
- const source = await resolveSyncInput(options.source, options);
379
- const outputPath = path.resolve(options.cwd ?? process.cwd(), options.target);
380
- const fallbackFormat = inferWritableTargetFormat(outputPath);
381
- let targetFormat = fallbackFormat;
382
- if (await fileExists(outputPath)) try {
383
- targetFormat = (await resolveLocalFile(options.target, options)).format;
384
- } catch (error) {
385
- if (!fallbackFormat) throw error;
386
- }
387
- if (!targetFormat) throw new Error(`Could not infer a writable target format from ${outputPath}. Use an existing digraph/graph file or a .digraph.json/.graph.json target.`);
388
- const serialized = serializeGraph(source.graph, targetFormat, {
389
- remoteMachineId: source.remoteMachineId,
390
- targetPath: outputPath
391
- });
392
- await fs.writeFile(outputPath, serialized, "utf8");
393
- return {
394
- source,
395
- target: {
396
- kind: "local-file",
397
- locator: outputPath,
398
- format: targetFormat,
399
- graph: source.graph
400
- },
401
- outputPath
402
- };
403
- }
404
- function inferDefaultProjectName(sourcePath, repo) {
405
- if (repo?.repo) return repo.repo;
406
- const parentDir = path.basename(path.dirname(sourcePath));
407
- if (parentDir && parentDir !== ".") return parentDir;
408
- return path.basename(sourcePath, path.extname(sourcePath));
409
- }
410
- async function resolvePushProject(client, sourcePath, options) {
411
- const explicitProject = options.project;
412
- if (explicitProject?.projectVersionId) return client.projects.get(explicitProject.projectVersionId);
413
- if (explicitProject?.projectId) return client.projects.get(explicitProject.projectId);
414
- const inferredRepo = explicitProject?.repo ?? await inferConnectedRepo(sourcePath, options.cwd);
415
- const projectInput = {
416
- name: explicitProject?.name ?? inferDefaultProjectName(sourcePath, inferredRepo),
417
- visibility: explicitProject?.visibility ?? "Private",
418
- ...explicitProject?.description ? { description: explicitProject.description } : {},
419
- ...explicitProject?.keywords ? { keywords: explicitProject.keywords } : {},
420
- ...inferredRepo ? { repo: inferredRepo } : {}
421
- };
422
- return client.projects.ensure(projectInput);
423
- }
424
- async function pushSync(options) {
425
- const client = options.client ?? createStatelyClient({
426
- apiKey: options.apiKey,
427
- baseUrl: options.baseUrl,
428
- fetch: options.fetch
429
- });
430
- const source = await resolveSyncInput(options.source, {
431
- ...options,
432
- target: options.source
433
- });
434
- if (source.kind !== "local-file") throw new Error("pushSync currently requires a local source file.");
435
- const sourcePath = source.locator;
436
- const project = await resolvePushProject(client, sourcePath, options);
437
- const existingMachineId = source.format === "xstate" ? getStatelyPragma(await fs.readFile(sourcePath, "utf8"), sourcePath)?.id : void 0;
438
- let machine;
439
- if (existingMachineId) machine = await client.machines.update({
440
- id: existingMachineId,
441
- definition: toStudioMachine(source.graph)
442
- });
443
- else {
444
- if (!project.projectVersionId) throw new Error("Resolved project is missing projectVersionId.");
445
- machine = await client.machines.create({
446
- projectVersionId: project.projectVersionId,
447
- definition: toStudioMachine(source.graph),
448
- xstateVersion: options.xstateVersion ?? 5
449
- });
450
- }
451
- let outputPath;
452
- if (source.format === "xstate") {
453
- const contents = await fs.readFile(sourcePath, "utf8");
454
- const nextContents = upsertStatelyPragma(contents, machine.id, { fileName: sourcePath });
455
- if (nextContents !== contents) {
456
- await fs.writeFile(sourcePath, nextContents, "utf8");
457
- outputPath = sourcePath;
458
- }
459
- }
460
- return {
461
- source,
462
- project,
463
- machine,
464
- ...outputPath ? { outputPath } : {}
465
- };
466
- }
467
-
468
- //#endregion
469
5
  export { planSync, pullSync, pushLocalMachineLinks, pushSync };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/sdk",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "statelyai": "./dist/cli.mjs"
@@ -61,6 +61,7 @@
61
61
  "dependencies": {
62
62
  "@oclif/core": "^4.10.3",
63
63
  "@statelyai/graph": "^0.9.0",
64
+ "esbuild": "^0.27.0",
64
65
  "typescript": "^5.9.3",
65
66
  "xstate": "^5.0.0"
66
67
  },