@statelyai/sdk 0.5.1 → 0.6.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.mjs CHANGED
@@ -1,11 +1,14 @@
1
1
  import { createStatelyClient } from "./studio.mjs";
2
+ import { d as upsertStatelyPragma, t as graphToXStateTS } from "./graphToXStateTS-Gzh0ZqbN.mjs";
2
3
  import { fromStudioMachine, toStudioMachine } from "./graph.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";
7
+ import { execFile } from "node:child_process";
8
+ import { promisify } from "node:util";
7
9
 
8
10
  //#region src/sync.ts
11
+ const execFileAsync = promisify(execFile);
9
12
  function isUrl(value) {
10
13
  try {
11
14
  const url = new URL(value);
@@ -22,6 +25,62 @@ async function fileExists(filePath) {
22
25
  return false;
23
26
  }
24
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
+ }
25
84
  function normalizeActions(value) {
26
85
  if (value == null) return [];
27
86
  return (Array.isArray(value) ? value : [value]).map((item) => {
@@ -172,11 +231,15 @@ function inferWritableTargetFormat(filePath) {
172
231
  if (filePath.endsWith(".graph.json")) return "graph";
173
232
  return null;
174
233
  }
175
- function serializeGraph(graph, format) {
234
+ function serializeGraph(graph, format, options = {}) {
176
235
  switch (format) {
177
236
  case "digraph": return `${JSON.stringify(toStudioMachine(graph), null, 2)}\n`;
178
237
  case "graph": return `${JSON.stringify(graph, null, 2)}\n`;
179
- case "xstate": return graphToXStateTS(graph);
238
+ case "xstate": {
239
+ const source = graphToXStateTS(graph);
240
+ if (!options.remoteMachineId) return source;
241
+ return upsertStatelyPragma(source, options.remoteMachineId, options.targetPath ? { fileName: options.targetPath } : {});
242
+ }
180
243
  default: {
181
244
  const exhaustive = format;
182
245
  throw new Error(`Unsupported sync output format: ${exhaustive}`);
@@ -192,7 +255,8 @@ async function resolveRemoteMachine(machineId, baseUrl, kind, locator, options)
192
255
  apiKey: options.apiKey,
193
256
  baseUrl,
194
257
  fetch: options.fetch
195
- })).machines.get(machineId))
258
+ })).machines.get(machineId)),
259
+ remoteMachineId: machineId
196
260
  };
197
261
  }
198
262
  async function resolveSyncInput(locator, options) {
@@ -237,7 +301,10 @@ async function pullSync(options) {
237
301
  if (!fallbackFormat) throw error;
238
302
  }
239
303
  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.`);
240
- const serialized = serializeGraph(source.graph, targetFormat);
304
+ const serialized = serializeGraph(source.graph, targetFormat, {
305
+ remoteMachineId: source.remoteMachineId,
306
+ targetPath: outputPath
307
+ });
241
308
  await fs.writeFile(outputPath, serialized, "utf8");
242
309
  return {
243
310
  source,
@@ -250,6 +317,61 @@ async function pullSync(options) {
250
317
  outputPath
251
318
  };
252
319
  }
320
+ function inferDefaultProjectName(sourcePath, repo) {
321
+ if (repo?.repo) return repo.repo;
322
+ const parentDir = path.basename(path.dirname(sourcePath));
323
+ if (parentDir && parentDir !== ".") return parentDir;
324
+ return path.basename(sourcePath, path.extname(sourcePath));
325
+ }
326
+ async function resolvePushProject(client, sourcePath, options) {
327
+ const explicitProject = options.project;
328
+ if (explicitProject?.projectVersionId) return client.projects.get(explicitProject.projectVersionId);
329
+ if (explicitProject?.projectId) return client.projects.get(explicitProject.projectId);
330
+ const inferredRepo = explicitProject?.repo ?? await inferConnectedRepo(sourcePath, options.cwd);
331
+ const projectInput = {
332
+ name: explicitProject?.name ?? inferDefaultProjectName(sourcePath, inferredRepo),
333
+ visibility: explicitProject?.visibility ?? "Private",
334
+ ...explicitProject?.description ? { description: explicitProject.description } : {},
335
+ ...explicitProject?.keywords ? { keywords: explicitProject.keywords } : {},
336
+ ...inferredRepo ? { repo: inferredRepo } : {}
337
+ };
338
+ return client.projects.ensure(projectInput);
339
+ }
340
+ async function pushSync(options) {
341
+ const client = options.client ?? createStatelyClient({
342
+ apiKey: options.apiKey,
343
+ baseUrl: options.baseUrl,
344
+ fetch: options.fetch
345
+ });
346
+ const source = await resolveSyncInput(options.source, {
347
+ ...options,
348
+ target: options.source
349
+ });
350
+ if (source.kind !== "local-file") throw new Error("pushSync currently requires a local source file.");
351
+ const sourcePath = source.locator;
352
+ const project = await resolvePushProject(client, sourcePath, options);
353
+ if (!project.projectVersionId) throw new Error("Resolved project is missing projectVersionId.");
354
+ const machine = await client.machines.create({
355
+ projectVersionId: project.projectVersionId,
356
+ definition: toStudioMachine(source.graph),
357
+ xstateVersion: options.xstateVersion ?? 5
358
+ });
359
+ let outputPath;
360
+ if (source.format === "xstate") {
361
+ const contents = await fs.readFile(sourcePath, "utf8");
362
+ const nextContents = upsertStatelyPragma(contents, machine.id, { fileName: sourcePath });
363
+ if (nextContents !== contents) {
364
+ await fs.writeFile(sourcePath, nextContents, "utf8");
365
+ outputPath = sourcePath;
366
+ }
367
+ }
368
+ return {
369
+ source,
370
+ project,
371
+ machine,
372
+ ...outputPath ? { outputPath } : {}
373
+ };
374
+ }
253
375
 
254
376
  //#endregion
255
- export { planSync, pullSync };
377
+ export { planSync, pullSync, pushSync };
@@ -89,6 +89,20 @@ function createPendingExportManager(sendRetrieve) {
89
89
  };
90
90
  }
91
91
  function toInitMessage(options) {
92
+ if ("machines" in options) return {
93
+ type: "@statelyai.project.init",
94
+ machines: options.machines,
95
+ currentMachineId: options.currentMachineId,
96
+ mode: options.mode,
97
+ theme: options.theme,
98
+ readOnly: options.readOnly,
99
+ depth: options.depth,
100
+ leftPanels: options.panels?.leftPanels,
101
+ rightPanels: options.panels?.rightPanels,
102
+ activePanels: options.panels?.activePanels,
103
+ commentsByMachineId: options.commentsByMachineId,
104
+ unsavedIndicator: options.unsavedIndicator
105
+ };
92
106
  return {
93
107
  type: "@statelyai.init",
94
108
  machine: options.machine,
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@statelyai/sdk",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "license": "MIT",
5
- "files": [
6
- "dist"
7
- ],
8
- "type": "module",
9
5
  "bin": {
10
6
  "statelyai": "./dist/cli.mjs"
11
7
  },
8
+ "files": [
9
+ "dist",
10
+ "schemas"
11
+ ],
12
+ "type": "module",
12
13
  "main": "./dist/index.mjs",
13
14
  "types": "./dist/index.d.mts",
14
15
  "exports": {
@@ -43,7 +44,17 @@
43
44
  "./studio": {
44
45
  "types": "./dist/studio.d.mts",
45
46
  "import": "./dist/studio.mjs"
46
- }
47
+ },
48
+ "./api": {
49
+ "types": "./dist/api.d.mts",
50
+ "import": "./dist/api.mjs"
51
+ },
52
+ "./assetStorage": {
53
+ "types": "./dist/assetStorage.d.mts",
54
+ "import": "./dist/assetStorage.mjs"
55
+ },
56
+ "./statelyai.schema.json": "./schemas/statelyai.schema.json",
57
+ "./schemas/statelyai.schema.json": "./schemas/statelyai.schema.json"
47
58
  },
48
59
  "dependencies": {
49
60
  "@oclif/core": "^4.10.3",
@@ -51,20 +62,21 @@
51
62
  "typescript": "^5.9.3",
52
63
  "xstate": "^5.0.0"
53
64
  },
54
- "oclif": {
55
- "bin": "statelyai",
56
- "commands": {
57
- "strategy": "explicit",
58
- "target": "./dist/cli.mjs",
59
- "identifier": "COMMANDS"
60
- }
61
- },
62
65
  "devDependencies": {
63
66
  "@types/json-schema": "^7.0.15",
64
67
  "jsdom": "^27.4.0",
65
68
  "tsdown": "0.21.0-beta.2",
66
69
  "vitest": "^3.2.4"
67
70
  },
71
+ "oclif": {
72
+ "bin": "statelyai",
73
+ "commands": {
74
+ "identifier": "COMMANDS",
75
+ "strategy": "explicit",
76
+ "target": "./dist/cli.mjs"
77
+ },
78
+ "topicSeparator": " "
79
+ },
68
80
  "scripts": {
69
81
  "build": "tsdown",
70
82
  "test": "vitest run",
@@ -0,0 +1,128 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://stately.ai/schemas/statelyai.json",
4
+ "title": "Stately CLI project configuration",
5
+ "description": "Configuration for mapping local machine sources to a Stately Studio project.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["version", "projectId", "sources"],
9
+ "properties": {
10
+ "$schema": {
11
+ "type": "string",
12
+ "format": "uri-reference",
13
+ "description": "Optional self-reference to this schema."
14
+ },
15
+ "version": {
16
+ "type": "string",
17
+ "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$",
18
+ "description": "Schema version for statelyai.json, expressed as a semantic version string."
19
+ },
20
+ "projectId": {
21
+ "type": "string",
22
+ "minLength": 1,
23
+ "description": "Stable Stately Studio project identifier."
24
+ },
25
+ "studioUrl": {
26
+ "type": "string",
27
+ "format": "uri",
28
+ "default": "https://stately.ai",
29
+ "description": "Base URL of the Studio instance that owns the project."
30
+ },
31
+ "defaultXStateVersion": {
32
+ "type": "integer",
33
+ "minimum": 5,
34
+ "default": 5,
35
+ "description": "Default XState major version for sources that do not override it. Only XState 5+ is supported."
36
+ },
37
+ "sources": {
38
+ "type": "array",
39
+ "minItems": 1,
40
+ "description": "Source groups used to discover and classify local machine files.",
41
+ "items": {
42
+ "$ref": "#/$defs/source"
43
+ }
44
+ }
45
+ },
46
+ "$defs": {
47
+ "globPattern": {
48
+ "type": "string",
49
+ "minLength": 1,
50
+ "description": "A glob pattern evaluated relative to the statelyai.json location."
51
+ },
52
+ "sourceFormat": {
53
+ "type": "string",
54
+ "enum": [
55
+ "auto",
56
+ "xstate",
57
+ "json",
58
+ "xgraph",
59
+ "digraph",
60
+ "mermaid",
61
+ "scxml",
62
+ "asl-json",
63
+ "asl-yaml",
64
+ "redux",
65
+ "zustand"
66
+ ],
67
+ "description": "Machine format for matching sources. Use xstate for JavaScript or TypeScript machine source files."
68
+ },
69
+ "source": {
70
+ "type": "object",
71
+ "additionalProperties": false,
72
+ "required": ["include"],
73
+ "properties": {
74
+ "name": {
75
+ "type": "string",
76
+ "minLength": 1,
77
+ "description": "Optional label for this source group."
78
+ },
79
+ "include": {
80
+ "type": "array",
81
+ "minItems": 1,
82
+ "items": {
83
+ "$ref": "#/$defs/globPattern"
84
+ },
85
+ "description": "Glob patterns for files to include in this source group."
86
+ },
87
+ "exclude": {
88
+ "type": "array",
89
+ "items": {
90
+ "$ref": "#/$defs/globPattern"
91
+ },
92
+ "description": "Glob patterns excluded from this source group."
93
+ },
94
+ "format": {
95
+ "$ref": "#/$defs/sourceFormat",
96
+ "default": "auto"
97
+ },
98
+ "xstateVersion": {
99
+ "type": "integer",
100
+ "minimum": 5,
101
+ "description": "Optional XState major version override for this source group. Only XState 5+ is supported."
102
+ }
103
+ }
104
+ }
105
+ },
106
+ "examples": [
107
+ {
108
+ "$schema": "https://stately.ai/schemas/statelyai.json",
109
+ "version": "1.0.0",
110
+ "projectId": "project_123",
111
+ "studioUrl": "https://stately.ai",
112
+ "defaultXStateVersion": 5,
113
+ "sources": [
114
+ {
115
+ "name": "app-machines",
116
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
117
+ "exclude": ["**/*.test.ts", "**/*.spec.ts", "**/dist/**"],
118
+ "format": "xstate"
119
+ },
120
+ {
121
+ "name": "docs",
122
+ "include": ["docs/**/*.mmd"],
123
+ "format": "mermaid"
124
+ }
125
+ ]
126
+ }
127
+ ]
128
+ }
@@ -1,54 +0,0 @@
1
- //#region src/studio.d.ts
2
- interface StudioClientOptions {
3
- baseUrl?: string;
4
- apiKey?: string;
5
- fetch?: typeof fetch;
6
- }
7
- interface VerifyApiKeyResponse {
8
- valid: boolean;
9
- }
10
- interface ProjectMachine {
11
- machineId: string;
12
- name: string;
13
- }
14
- interface ProjectData {
15
- projectId: string;
16
- machines: ProjectMachine[];
17
- }
18
- interface ExtractedMachine {
19
- id?: string;
20
- config: Record<string, unknown>;
21
- setupConfig?: Record<string, unknown>;
22
- implementations?: Record<string, unknown>;
23
- _type: 'setup.createMachine' | 'createMachine';
24
- }
25
- interface ExtractMachinesResponse {
26
- machines: ExtractedMachine[];
27
- error?: string;
28
- }
29
- interface GetMachineOptions {
30
- version?: string;
31
- }
32
- declare class StudioApiError extends Error {
33
- readonly status: number;
34
- constructor(message: string, status: number);
35
- }
36
- interface StudioClient {
37
- auth: {
38
- verify(apiKey?: string): Promise<VerifyApiKeyResponse>;
39
- };
40
- projects: {
41
- get(projectId: string): Promise<ProjectData>;
42
- };
43
- machines: {
44
- get<TMachine = Record<string, unknown>>(machineId: string, options?: GetMachineOptions): Promise<TMachine>;
45
- };
46
- code: {
47
- extractMachines(code: string, options?: {
48
- apiKey?: string | null;
49
- }): Promise<ExtractMachinesResponse>;
50
- };
51
- }
52
- declare function createStatelyClient(options?: StudioClientOptions): StudioClient;
53
- //#endregion
54
- export { ProjectMachine as a, StudioClientOptions as c, ProjectData as i, VerifyApiKeyResponse as l, ExtractedMachine as n, StudioApiError as o, GetMachineOptions as r, StudioClient as s, ExtractMachinesResponse as t, createStatelyClient as u };