@statelyai/sdk 0.6.1 → 0.7.0

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
@@ -206,12 +206,15 @@ Available client methods:
206
206
  | `studio.projects.get(projectId)` | Fetch a project and its machines |
207
207
  | `studio.machines.create({ projectVersionId, ... })` | Create a machine through the published REST API |
208
208
  | `studio.machines.createMany({ projectVersionId, ... })` | Compatibility wrapper around `create()` that returns a one-item array |
209
+ | `studio.machines.update({ id, ... })` | Update a machine through the published REST API |
209
210
  | `studio.machines.get(machineId, { version? })` | Fetch a machine, optionally pinned to a version |
210
211
  | `studio.code.extractMachines(code, { apiKey? })` | Extract machine configs from source text |
211
212
 
212
213
  ## Sync Helpers
213
214
 
214
- `pushSync()` complements `planSync()` and `pullSync()` for local-to-Studio flows. It resolves a local source file, ensures there is a target project, creates the remote machine, and writes `// @statelyai id=...` back into XState source files.
215
+ `pushSync()` complements `planSync()` and `pullSync()` for local-to-Studio flows. It resolves a local source file, ensures there is a target project, updates the linked remote machine when `// @statelyai id=...` is present, otherwise creates one, and writes the pragma back into XState source files when needed.
216
+
217
+ For project-scoped discovery flows, `statelyai.json` defines which local files should be scanned. The CLI `statelyai push` command uses that config to find local XState sources, creates remote Studio machines for unlabeled ones, updates already-linked ones, and writes returned `// @statelyai id=...` pragmas back into source.
215
218
 
216
219
  ```ts
217
220
  import { pushSync } from '@statelyai/sdk/sync';
@@ -232,7 +235,7 @@ const result = await pushSync({
232
235
  });
233
236
  ```
234
237
 
235
- Projects can also check in a `statelyai.json` file to describe which local sources belong to a Studio project. The published schema lives at `@statelyai/sdk/statelyai.schema.json` and the canonical schema id is `https://stately.ai/schemas/statelyai.json`.
238
+ Projects can also check in a `statelyai.json` file to describe which local sources belong to a Studio project. The published schema lives at `@statelyai/sdk/statelyai.schema.json` and the canonical schema id is `https://stately.ai/schemas/statelyai.json`. The strict XState JSON export schema is published at `@statelyai/sdk/xstate-json.schema.json`.
236
239
 
237
240
  ```json
238
241
  {
@@ -241,17 +244,7 @@ Projects can also check in a `statelyai.json` file to describe which local sourc
241
244
  "projectId": "project_123",
242
245
  "studioUrl": "https://stately.ai",
243
246
  "defaultXStateVersion": 5,
244
- "sources": [
245
- {
246
- "include": ["src/**/*.ts", "src/**/*.tsx"],
247
- "exclude": ["**/*.test.ts", "**/dist/**"],
248
- "format": "xstate"
249
- },
250
- {
251
- "include": ["docs/**/*.mmd"],
252
- "format": "mermaid"
253
- }
254
- ]
247
+ "sources": []
255
248
  }
256
249
  ```
257
250
 
@@ -423,7 +416,7 @@ const aslYaml = await embed.export('asl-yaml');
423
416
 
424
417
  <!-- supported export formats from ExportFormatMap in src/protocol.ts -->
425
418
 
426
- Supported formats: `xstate`, `json`, `xgraph`, `digraph`, `mermaid`, `redux`, `zustand`, `asl-json`, `asl-yaml`, `scxml`
419
+ Supported formats: `xstate`, `xstate-json`, `xgraph`, `digraph`, `mermaid`, `redux`, `zustand`, `asl-json`, `asl-yaml`, `scxml`
427
420
 
428
421
  #### `embed.on(event, handler)` / `embed.off(event, handler)`
429
422
 
@@ -605,11 +598,15 @@ Installing the package also exposes a `statelyai` binary:
605
598
  npx statelyai open ./checkout.machine.ts
606
599
 
607
600
  statelyai init
601
+ statelyai init --scan
608
602
  statelyai login
609
603
  statelyai auth status
610
604
  statelyai plan ./checkout.machine.ts machine-id
611
605
  statelyai diff ./checkout.machine.ts machine-id --fail-on-changes
606
+ statelyai push
607
+ statelyai push ./checkout.machine.ts
612
608
  statelyai pull machine-id ./checkout.machine.ts
609
+ statelyai pull ./checkout.machine.ts
613
610
  statelyai open ./checkout.machine.ts
614
611
  ```
615
612
 
@@ -619,13 +616,15 @@ Available commands:
619
616
 
620
617
  | Command | Description |
621
618
  | ---------------------------------- | --------------------------------------------------------------------------- |
622
- | `statelyai init` | Create or reuse a Studio project for the current directory and write `statelyai.json` |
619
+ | `statelyai init` | Create or reuse a Studio project for the current directory and write `statelyai.json` with an empty `sources` array |
623
620
  | `statelyai login` | Store an API key for future CLI use |
624
621
  | `statelyai logout` | Remove a stored API key |
625
622
  | `statelyai auth status` | Show whether the CLI would use an environment variable or stored credential |
626
623
  | `statelyai plan <source> <target>` | Print a semantic sync summary |
627
624
  | `statelyai diff <source> <target>` | Diff two locators and optionally fail on changes |
625
+ | `statelyai push [file]` | Discover local machine sources, create remote Studio machines for unlabeled files, update linked ones, and persist returned ids |
628
626
  | `statelyai pull <source> <target>` | Materialize a source into a local target file |
627
+ | `statelyai pull <linked-file>` | Refresh a linked local file from the `@statelyai id=...` pragma |
629
628
  | `statelyai open <file>` | Open a local file in a browser-backed visual editor session |
630
629
 
631
630
  Common flags:
@@ -636,6 +635,8 @@ Common flags:
636
635
 
637
636
  The CLI resolves credentials in this order: `--api-key`, then `STATELY_API_KEY`/`NEXT_PUBLIC_STATELY_API_KEY`, then the key stored by `statelyai login`. `login` stores the key in the OS credential store when available, with a private file fallback.
638
637
 
638
+ `statelyai init --scan` walks local code files, detects machine-bearing files from their contents, and suggests `sources` globs to save into `statelyai.json`. Without `--scan`, `init` leaves `sources` empty so you can opt in explicitly before running `statelyai push`.
639
+
639
640
  `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. When the source file contains `// @statelyai id=...` and an API key is available, the editor session also reuses the referenced remote machine layout while continuing to treat the local source as the semantic source of truth. Pass `--api-key`, set `STATELY_API_KEY`, or run `statelyai login` 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.
640
641
 
641
642
  ## Transport Helpers
@@ -1,4 +1,4 @@
1
- import { m as UploadResult } from "./protocol-CDoCcaIP.mjs";
1
+ import { m as UploadResult } from "./protocol-DN4mH4jR.mjs";
2
2
 
3
3
  //#region src/assetStorage.d.ts
4
4
  interface AssetUploadContext {
package/dist/cli.d.mts CHANGED
@@ -4,11 +4,11 @@ import { SyncPlan } from "./sync.mjs";
4
4
  import { Command } from "@oclif/core";
5
5
  import * as _oclif_core_interfaces0 from "@oclif/core/interfaces";
6
6
 
7
- //#region src/cli.d.ts
7
+ //#region src/projectConfig.d.ts
8
8
  interface StatelySourceConfig {
9
9
  include: string[];
10
10
  exclude?: string[];
11
- format: 'xstate' | 'redux' | 'zustand' | 'xgraph' | 'digraph' | 'mermaid' | 'scxml' | 'json' | 'asl-json' | 'asl-yaml' | 'auto';
11
+ format: 'xstate' | 'redux' | 'zustand' | 'xgraph' | 'digraph' | 'mermaid' | 'scxml' | 'xstate-json' | 'asl-json' | 'asl-yaml' | 'auto';
12
12
  xstateVersion?: number;
13
13
  }
14
14
  interface StatelyProjectConfig {
@@ -19,6 +19,13 @@ interface StatelyProjectConfig {
19
19
  defaultXStateVersion: number;
20
20
  sources: StatelySourceConfig[];
21
21
  }
22
+ declare function createStatelyProjectConfig(options: {
23
+ projectId: string;
24
+ studioUrl: string;
25
+ defaultXStateVersion?: number;
26
+ }): StatelyProjectConfig;
27
+ //#endregion
28
+ //#region src/cli.d.ts
22
29
  interface InitProjectOptions {
23
30
  apiKey: string;
24
31
  baseUrl?: string;
@@ -34,11 +41,11 @@ interface InitProjectResult {
34
41
  configPath: string;
35
42
  project: ProjectData;
36
43
  }
37
- declare function createStatelyProjectConfig(options: {
38
- projectId: string;
39
- studioUrl: string;
44
+ interface ScanProjectSourcesOptions {
45
+ cwd?: string;
46
+ client: StudioClient;
40
47
  defaultXStateVersion?: number;
41
- }): StatelyProjectConfig;
48
+ }
42
49
  type ApiKeyResolution = {
43
50
  apiKey: string;
44
51
  detail: string;
@@ -55,6 +62,7 @@ declare function getEnvApiKey(): {
55
62
  declare function resolveApiKey(explicitApiKey?: string): Promise<ApiKeyResolution>;
56
63
  declare function inferInitProjectName(cwd: string, repo?: ConnectedRepo): string;
57
64
  declare function initProject(options: InitProjectOptions): Promise<InitProjectResult>;
65
+ declare function scanProjectSources(options: ScanProjectSourcesOptions): Promise<StatelySourceConfig[]>;
58
66
  declare function formatPlanSummary(plan: SyncPlan): string;
59
67
  declare abstract class BaseSyncCommand extends Command {
60
68
  static enableJsonFlag: boolean;
@@ -100,7 +108,22 @@ declare class PullCommand extends ParsedSyncCommand {
100
108
  static description: string;
101
109
  static args: {
102
110
  source: _oclif_core_interfaces0.Arg<string, Record<string, unknown>>;
103
- target: _oclif_core_interfaces0.Arg<string, Record<string, unknown>>;
111
+ target: _oclif_core_interfaces0.Arg<string | undefined, Record<string, unknown>>;
112
+ };
113
+ run(): Promise<void>;
114
+ }
115
+ declare class PushCommand extends Command {
116
+ static enableJsonFlag: boolean;
117
+ static summary: string;
118
+ static description: string;
119
+ static args: {
120
+ file: _oclif_core_interfaces0.Arg<string | undefined, Record<string, unknown>>;
121
+ };
122
+ static flags: {
123
+ help: _oclif_core_interfaces0.BooleanFlag<void>;
124
+ 'api-key': _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
125
+ 'base-url': _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
126
+ config: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
104
127
  };
105
128
  run(): Promise<void>;
106
129
  }
@@ -133,6 +156,7 @@ declare class InitCommand extends Command {
133
156
  name: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
134
157
  visibility: _oclif_core_interfaces0.OptionFlag<string, _oclif_core_interfaces0.CustomOptions>;
135
158
  force: _oclif_core_interfaces0.BooleanFlag<boolean>;
159
+ scan: _oclif_core_interfaces0.BooleanFlag<boolean>;
136
160
  };
137
161
  run(): Promise<void>;
138
162
  }
@@ -169,6 +193,7 @@ declare const COMMANDS: {
169
193
  plan: typeof PlanCommand;
170
194
  diff: typeof DiffCommand;
171
195
  pull: typeof PullCommand;
196
+ push: typeof PushCommand;
172
197
  open: typeof OpenCommand;
173
198
  init: typeof InitCommand;
174
199
  login: typeof LoginCommand;
@@ -177,4 +202,4 @@ declare const COMMANDS: {
177
202
  };
178
203
  declare function run(argv?: string[], entryUrl?: string): Promise<void>;
179
204
  //#endregion
180
- export { ApiKeyResolution, COMMANDS, InitProjectOptions, InitProjectResult, StatelyProjectConfig, StatelySourceConfig, createStatelyProjectConfig, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, resolveApiKey, run };
205
+ export { ApiKeyResolution, COMMANDS, InitProjectOptions, InitProjectResult, ScanProjectSourcesOptions, createStatelyProjectConfig, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, resolveApiKey, run, scanProjectSources };
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createStatelyClient } from "./studio.mjs";
3
- import "./graphToXStateTS-Gzh0ZqbN.mjs";
4
- import { planSync, pullSync } from "./sync.mjs";
3
+ import { u as getStatelyPragma } from "./graphToXStateTS-Gzh0ZqbN.mjs";
4
+ import { planSync, pullSync, pushLocalMachineLinks } from "./sync.mjs";
5
5
  import fs from "node:fs/promises";
6
6
  import * as path$1 from "node:path";
7
7
  import path from "node:path";
@@ -931,24 +931,26 @@ function describeCredentialBackend(backend, location) {
931
931
  }
932
932
 
933
933
  //#endregion
934
- //#region src/cli.ts
935
- const execFileAsync = promisify(execFile);
934
+ //#region src/projectConfig.ts
936
935
  const STATELY_CONFIG_FILE = "statelyai.json";
937
936
  const STATELY_CONFIG_SCHEMA_URL = "https://stately.ai/schemas/statelyai.json";
938
937
  const STATELY_CONFIG_VERSION = "1.0.0";
939
- function getDefaultSources(defaultXStateVersion) {
940
- return [{
941
- include: ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}"],
942
- exclude: [
943
- "**/*.test.*",
944
- "**/*.spec.*",
945
- "**/dist/**",
946
- "**/node_modules/**"
947
- ],
948
- format: "xstate",
949
- xstateVersion: defaultXStateVersion
950
- }];
951
- }
938
+ const DEFAULT_SOURCE_EXCLUDES = [
939
+ "**/*.test.*",
940
+ "**/*.spec.*",
941
+ "**/dist/**",
942
+ "**/node_modules/**"
943
+ ];
944
+ const CODE_SOURCE_EXTENSIONS = new Set([
945
+ ".ts",
946
+ ".tsx",
947
+ ".js",
948
+ ".jsx",
949
+ ".mts",
950
+ ".cts",
951
+ ".mjs",
952
+ ".cjs"
953
+ ]);
952
954
  function createStatelyProjectConfig(options) {
953
955
  const defaultXStateVersion = options.defaultXStateVersion ?? 5;
954
956
  return {
@@ -957,9 +959,145 @@ function createStatelyProjectConfig(options) {
957
959
  projectId: options.projectId,
958
960
  studioUrl: options.studioUrl,
959
961
  defaultXStateVersion,
960
- sources: getDefaultSources(defaultXStateVersion)
962
+ sources: []
961
963
  };
962
964
  }
965
+ async function readStatelyProjectConfig(options = {}) {
966
+ const rootDir = path.resolve(options.cwd ?? process.cwd());
967
+ const configPath = path.resolve(rootDir, options.configPath ?? STATELY_CONFIG_FILE);
968
+ const raw = await fs.readFile(configPath, "utf8");
969
+ return {
970
+ config: JSON.parse(raw),
971
+ configPath,
972
+ rootDir
973
+ };
974
+ }
975
+ async function walkFiles(rootDir, currentDir = rootDir) {
976
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
977
+ const files = [];
978
+ for (const entry of entries) {
979
+ if (entry.name === ".git") continue;
980
+ const absolutePath = path.join(currentDir, entry.name);
981
+ if (entry.isDirectory()) {
982
+ files.push(...await walkFiles(rootDir, absolutePath));
983
+ continue;
984
+ }
985
+ if (!entry.isFile()) continue;
986
+ files.push(path.relative(rootDir, absolutePath).replace(/\\/g, "/"));
987
+ }
988
+ return files;
989
+ }
990
+ function isCodeSourceFile(relativePath) {
991
+ return CODE_SOURCE_EXTENSIONS.has(path.extname(relativePath).toLowerCase());
992
+ }
993
+ function expandBraces(pattern) {
994
+ const match = pattern.match(/\{([^{}]+)\}/);
995
+ if (!match || match.index == null) return [pattern];
996
+ const [token, inner] = match;
997
+ return inner.split(",").flatMap((variant) => expandBraces(`${pattern.slice(0, match.index)}${variant}${pattern.slice(match.index + token.length)}`));
998
+ }
999
+ function globToRegExp(pattern) {
1000
+ let regex = "^";
1001
+ for (let index = 0; index < pattern.length; index += 1) {
1002
+ const char = pattern[index];
1003
+ const next = pattern[index + 1];
1004
+ if (char === "*") {
1005
+ if (next === "*") {
1006
+ const slashAfterGlobstar = pattern[index + 2] === "/";
1007
+ regex += slashAfterGlobstar ? "(?:.*/)?" : ".*";
1008
+ index += slashAfterGlobstar ? 2 : 1;
1009
+ continue;
1010
+ }
1011
+ regex += "[^/]*";
1012
+ continue;
1013
+ }
1014
+ if (char === "?") {
1015
+ regex += "[^/]";
1016
+ continue;
1017
+ }
1018
+ if ("\\.[]{}()+-^$|".includes(char)) {
1019
+ regex += `\\${char}`;
1020
+ continue;
1021
+ }
1022
+ regex += char;
1023
+ }
1024
+ regex += "$";
1025
+ return new RegExp(regex);
1026
+ }
1027
+ function matchesGlob(relativePath, pattern) {
1028
+ return expandBraces(pattern).some((expanded) => globToRegExp(expanded).test(relativePath));
1029
+ }
1030
+ function matchesAny(patterns, relativePath) {
1031
+ return (patterns ?? []).some((pattern) => matchesGlob(relativePath, pattern));
1032
+ }
1033
+ async function discoverCodeSourceFiles(options = {}) {
1034
+ return (await walkFiles(path.resolve(options.cwd ?? process.cwd()))).filter((relativePath) => isCodeSourceFile(relativePath)).filter((relativePath) => !matchesAny(DEFAULT_SOURCE_EXCLUDES, relativePath)).sort((left, right) => left.localeCompare(right));
1035
+ }
1036
+ function createSuggestedSource(include) {
1037
+ return {
1038
+ include: [include],
1039
+ exclude: [...DEFAULT_SOURCE_EXCLUDES],
1040
+ format: "xstate",
1041
+ xstateVersion: 5
1042
+ };
1043
+ }
1044
+ function suggestStatelySourceConfigs(relativePaths, defaultXStateVersion = 5) {
1045
+ const pending = new Set(relativePaths.map((relativePath) => relativePath.replace(/\\/g, "/")).filter(Boolean));
1046
+ const suggestions = [];
1047
+ const addSuggestion = (include, predicate) => {
1048
+ const matched = [...pending].filter(predicate);
1049
+ if (matched.length === 0) return;
1050
+ suggestions.push({
1051
+ ...createSuggestedSource(include),
1052
+ xstateVersion: defaultXStateVersion
1053
+ });
1054
+ for (const matchedPath of matched) pending.delete(matchedPath);
1055
+ };
1056
+ addSuggestion("**/*.machine.ts", (relativePath) => relativePath.endsWith(".machine.ts"));
1057
+ addSuggestion("src/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}", (relativePath) => relativePath.startsWith("src/"));
1058
+ addSuggestion("packages/*/src/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}", (relativePath) => /^packages\/[^/]+\/src\//.test(relativePath));
1059
+ addSuggestion("apps/*/src/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}", (relativePath) => /^apps\/[^/]+\/src\//.test(relativePath));
1060
+ const byDirectory = /* @__PURE__ */ new Map();
1061
+ for (const relativePath of pending) {
1062
+ const directory = path.posix.dirname(relativePath);
1063
+ const key = directory === "." ? relativePath : directory;
1064
+ const bucket = byDirectory.get(key) ?? [];
1065
+ bucket.push(relativePath);
1066
+ byDirectory.set(key, bucket);
1067
+ }
1068
+ for (const [key, matchedPaths] of [...byDirectory.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
1069
+ if (matchedPaths.length > 1 && key !== "." && key !== matchedPaths[0]) {
1070
+ addSuggestion(`${key}/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}`, (relativePath) => relativePath.startsWith(`${key}/`));
1071
+ continue;
1072
+ }
1073
+ const exactPath = matchedPaths[0];
1074
+ addSuggestion(exactPath, (relativePath) => relativePath === exactPath);
1075
+ }
1076
+ return suggestions;
1077
+ }
1078
+ async function discoverStatelySourceFiles(options = {}) {
1079
+ const { config, rootDir } = options.config ? {
1080
+ config: options.config,
1081
+ rootDir: path.resolve(options.cwd ?? process.cwd())
1082
+ } : await readStatelyProjectConfig(options);
1083
+ const relativeFiles = await walkFiles(rootDir);
1084
+ const discovered = /* @__PURE__ */ new Map();
1085
+ for (const source of config.sources) for (const relativePath of relativeFiles) {
1086
+ if (!matchesAny(source.include, relativePath)) continue;
1087
+ if (matchesAny(source.exclude, relativePath)) continue;
1088
+ const filePath = path.join(rootDir, relativePath);
1089
+ if (!discovered.has(filePath)) discovered.set(filePath, {
1090
+ filePath,
1091
+ relativePath,
1092
+ source
1093
+ });
1094
+ }
1095
+ return [...discovered.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
1096
+ }
1097
+
1098
+ //#endregion
1099
+ //#region src/cli.ts
1100
+ const execFileAsync = promisify(execFile);
963
1101
  function loadLocalEnv() {
964
1102
  if (typeof process.loadEnvFile !== "function") return;
965
1103
  const cwdEnvPath = path.join(process.cwd(), ".env.local");
@@ -1090,6 +1228,22 @@ async function promptForApiKey() {
1090
1228
  rl.close();
1091
1229
  }
1092
1230
  }
1231
+ async function promptYesNo(question, defaultValue = true) {
1232
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("No interactive terminal available.");
1233
+ const rl = createInterface({
1234
+ input: process.stdin,
1235
+ output: process.stdout,
1236
+ terminal: true
1237
+ });
1238
+ try {
1239
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
1240
+ const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
1241
+ if (!answer) return defaultValue;
1242
+ return answer === "y" || answer === "yes";
1243
+ } finally {
1244
+ rl.close();
1245
+ }
1246
+ }
1093
1247
  function normalizeApiKey(value) {
1094
1248
  const trimmed = value?.trim();
1095
1249
  return trimmed ? trimmed : void 0;
@@ -1133,6 +1287,45 @@ async function initProject(options) {
1133
1287
  project
1134
1288
  };
1135
1289
  }
1290
+ async function scanProjectSources(options) {
1291
+ const cwd = path.resolve(options.cwd ?? process.cwd());
1292
+ const candidateRelativePaths = await discoverCodeSourceFiles({ cwd });
1293
+ const machineRelativePaths = [];
1294
+ for (const relativePath of candidateRelativePaths) {
1295
+ const filePath = path.join(cwd, relativePath);
1296
+ const contents = await fs.readFile(filePath, "utf8");
1297
+ let extracted;
1298
+ try {
1299
+ extracted = await options.client.code.extractMachines(contents);
1300
+ } catch {
1301
+ continue;
1302
+ }
1303
+ if (extracted.machines.length > 0) machineRelativePaths.push(relativePath);
1304
+ }
1305
+ return suggestStatelySourceConfigs(machineRelativePaths, Math.max(5, options.defaultXStateVersion ?? 5));
1306
+ }
1307
+ function supportsMachineDiscovery(file) {
1308
+ return file.source.format === "xstate" || file.source.format === "auto";
1309
+ }
1310
+ async function resolveConfiguredProject(options) {
1311
+ const { config, configPath, rootDir } = await readStatelyProjectConfig({
1312
+ cwd: options.cwd,
1313
+ configPath: options.configPath
1314
+ });
1315
+ const studioUrl = options.baseUrl ?? config.studioUrl;
1316
+ return {
1317
+ client: options.client ?? createStatelyClient({
1318
+ apiKey: options.apiKey,
1319
+ baseUrl: studioUrl
1320
+ }),
1321
+ config,
1322
+ configPath,
1323
+ files: await discoverStatelySourceFiles({
1324
+ cwd: rootDir,
1325
+ config
1326
+ })
1327
+ };
1328
+ }
1136
1329
  const sharedFlags = {
1137
1330
  help: Flags.help({ char: "h" }),
1138
1331
  "fail-on-changes": Flags.boolean({
@@ -1225,19 +1418,99 @@ var DiffCommand = class DiffCommand extends ParsedSyncCommand {
1225
1418
  var PullCommand = class PullCommand extends ParsedSyncCommand {
1226
1419
  static summary = "Pull a source locator into a local target file.";
1227
1420
  static description = "Resolves the source, materializes it in the target format inferred from the target file, and writes the result locally.";
1228
- static args = sharedArgs;
1421
+ static args = {
1422
+ source: Args.string({
1423
+ required: true,
1424
+ description: "Source locator. Supports a local linked file, machine ID, URL, or local file path."
1425
+ }),
1426
+ target: Args.string({
1427
+ required: false,
1428
+ description: "Optional local target path. Defaults to the source file when pulling a linked local file."
1429
+ })
1430
+ };
1229
1431
  async run() {
1230
1432
  const { args, flags } = await this.parseSync(PullCommand);
1231
1433
  const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
1434
+ const localCandidate = path.resolve(process.cwd(), args.source);
1435
+ let source = args.source;
1436
+ let target = args.target;
1437
+ if (!target && await fileExists(localCandidate)) {
1438
+ const pragma = getStatelyPragma(await fs.readFile(localCandidate, "utf8"), localCandidate);
1439
+ if (!pragma?.id) this.error(`No @statelyai id found in ${localCandidate}. Pass an explicit source machine ID or URL and target path.`);
1440
+ source = pragma.id;
1441
+ target = localCandidate;
1442
+ }
1443
+ if (!target) this.error("Missing target path. Pass `statelyai pull <machine-id|url> <file>` or `statelyai pull <linked-file>`.");
1232
1444
  const result = await pullSync({
1233
- source: args.source,
1234
- target: args.target,
1445
+ source,
1446
+ target,
1235
1447
  apiKey,
1236
1448
  baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
1237
1449
  });
1238
1450
  this.log(`Pulled: ${result.source.locator} -> ${result.outputPath}\nTarget: ${result.target.kind} (${result.target.format})`);
1239
1451
  }
1240
1452
  };
1453
+ var PushCommand = class PushCommand extends Command {
1454
+ static enableJsonFlag = false;
1455
+ static summary = "Create or update remote machines for local source files.";
1456
+ static description = "Uses statelyai.json source discovery by default, creates remote machines for unlabeled local machine sources, updates already-linked machines, and writes returned @statelyai ids back into source files.";
1457
+ static args = { file: Args.string({
1458
+ required: false,
1459
+ description: "Optional local source file to push instead of scanning statelyai.json."
1460
+ }) };
1461
+ static flags = {
1462
+ help: Flags.help({ char: "h" }),
1463
+ "api-key": Flags.string({ description: "Stately API key used to create or update remote machines" }),
1464
+ "base-url": Flags.string({ description: "Base URL for Stately Studio or a self-hosted deployment" }),
1465
+ config: Flags.string({ description: "Path to statelyai.json" })
1466
+ };
1467
+ async run() {
1468
+ const { args, flags } = await this.parse(PushCommand);
1469
+ const resolvedApiKey = await resolveApiKey(flags["api-key"]);
1470
+ if (!resolvedApiKey.apiKey) this.error("No API key configured. Use `statelyai login`, set `STATELY_API_KEY`, or pass `--api-key`.");
1471
+ const { client, config, files } = await resolveConfiguredProject({
1472
+ apiKey: resolvedApiKey.apiKey,
1473
+ baseUrl: flags["base-url"],
1474
+ configPath: flags.config
1475
+ });
1476
+ const candidateFiles = args.file ? [{
1477
+ filePath: path.resolve(args.file),
1478
+ relativePath: path.relative(process.cwd(), path.resolve(args.file)),
1479
+ source: {
1480
+ include: [args.file],
1481
+ format: "xstate",
1482
+ xstateVersion: config.defaultXStateVersion
1483
+ }
1484
+ }] : files.filter(supportsMachineDiscovery);
1485
+ if (candidateFiles.length === 0) {
1486
+ this.log("No matching local machine source files were discovered.");
1487
+ return;
1488
+ }
1489
+ const linked = [];
1490
+ const refreshed = [];
1491
+ const skipped = [];
1492
+ for (const file of candidateFiles) {
1493
+ if (!supportsMachineDiscovery(file)) {
1494
+ skipped.push(`${file.relativePath}: unsupported format ${file.source.format}`);
1495
+ continue;
1496
+ }
1497
+ const result = await pushLocalMachineLinks({
1498
+ source: file.filePath,
1499
+ apiKey: resolvedApiKey.apiKey,
1500
+ baseUrl: flags["base-url"] ?? config.studioUrl,
1501
+ client,
1502
+ project: { projectId: config.projectId },
1503
+ xstateVersion: Math.max(5, file.source.xstateVersion ?? config.defaultXStateVersion)
1504
+ });
1505
+ if (result.created.length > 0) linked.push(`${file.relativePath}: ${result.created.map(({ machineIndex, machine }) => `${machine.id} [${machineIndex}]`).join(", ")}`);
1506
+ if (result.updated.length > 0) refreshed.push(`${file.relativePath}: ${result.updated.map(({ machineIndex, machine }) => `${machine.id} [${machineIndex}]`).join(", ")}`);
1507
+ for (const entry of result.skipped) skipped.push(`${file.relativePath} [${entry.machineIndex}]: ${entry.reason}`);
1508
+ }
1509
+ if (linked.length > 0) this.log(`Linked:\n${linked.join("\n")}`);
1510
+ if (refreshed.length > 0) this.log(`Updated:\n${refreshed.join("\n")}`);
1511
+ if (skipped.length > 0) this.log(`Skipped:\n${skipped.join("\n")}`);
1512
+ }
1513
+ };
1241
1514
  var OpenCommand = class OpenCommand extends Command {
1242
1515
  static enableJsonFlag = false;
1243
1516
  static summary = "Open a local file in the Stately visual editor.";
@@ -1304,6 +1577,10 @@ var InitCommand = class InitCommand extends Command {
1304
1577
  force: Flags.boolean({
1305
1578
  description: "Overwrite an existing statelyai.json file",
1306
1579
  default: false
1580
+ }),
1581
+ scan: Flags.boolean({
1582
+ description: "Scan the repo for machine-bearing files and suggest source globs to save into statelyai.json",
1583
+ default: false
1307
1584
  })
1308
1585
  };
1309
1586
  async run() {
@@ -1319,7 +1596,33 @@ var InitCommand = class InitCommand extends Command {
1319
1596
  visibility: flags.visibility
1320
1597
  }
1321
1598
  });
1322
- this.log(`Initialized project ${result.project.projectId} and wrote ${result.configPath}.`);
1599
+ if (flags.scan) {
1600
+ const scanClient = createStatelyClient({
1601
+ apiKey: resolvedApiKey.apiKey,
1602
+ baseUrl: flags["base-url"] ?? result.config.studioUrl
1603
+ });
1604
+ const suggestions = await scanProjectSources({
1605
+ cwd: path.dirname(result.configPath),
1606
+ client: scanClient,
1607
+ defaultXStateVersion: result.config.defaultXStateVersion
1608
+ });
1609
+ if (suggestions.length === 0) {
1610
+ this.log(`Initialized project ${result.project.projectId} and wrote ${result.configPath}.\nNo machine source globs were suggested. Edit statelyai.json before running statelyai push.`);
1611
+ return;
1612
+ }
1613
+ this.log(`Initialized project ${result.project.projectId} and wrote ${result.configPath}.`);
1614
+ this.log(`Suggested source globs:\n${suggestions.map((source) => `- ${source.include.join(", ")}`).join("\n")}`);
1615
+ if (await promptYesNo("Save these source globs to statelyai.json?", true)) {
1616
+ const nextConfig = {
1617
+ ...result.config,
1618
+ sources: suggestions
1619
+ };
1620
+ await fs.writeFile(result.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
1621
+ this.log("Saved scanned source globs to statelyai.json.");
1622
+ } else this.log("Left statelyai.json with an empty sources array. Edit it before running statelyai push.");
1623
+ return;
1624
+ }
1625
+ this.log(`Initialized project ${result.project.projectId} and wrote ${result.configPath}.\nNo source globs configured yet. Edit statelyai.json before running statelyai push, or rerun with --scan.`);
1323
1626
  }
1324
1627
  };
1325
1628
  var LoginCommand = class LoginCommand extends Command {
@@ -1381,6 +1684,7 @@ const COMMANDS = {
1381
1684
  plan: PlanCommand,
1382
1685
  diff: DiffCommand,
1383
1686
  pull: PullCommand,
1687
+ push: PushCommand,
1384
1688
  open: OpenCommand,
1385
1689
  init: InitCommand,
1386
1690
  login: LoginCommand,
@@ -1409,4 +1713,4 @@ function isDirectExecution() {
1409
1713
  if (isDirectExecution()) run();
1410
1714
 
1411
1715
  //#endregion
1412
- export { COMMANDS, createStatelyProjectConfig, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, resolveApiKey, run };
1716
+ export { COMMANDS, createStatelyProjectConfig, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, resolveApiKey, run, scanProjectSources };
package/dist/embed.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as EmbedMode, c as ExportFormatMap, d as MachineSourceLocations, f as ProjectEmbedMachine, i as EmbedEventName, l as InitOptions, m as UploadResult, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig, u as MachineInitOptions } from "./protocol-CDoCcaIP.mjs";
1
+ import { a as EmbedMode, c as ExportFormatMap, d as MachineSourceLocations, f as ProjectEmbedMachine, i as EmbedEventName, l as InitOptions, m as UploadResult, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig, u as MachineInitOptions } from "./protocol-DN4mH4jR.mjs";
2
2
  import { AssetUploadAdapter } from "./assetStorage.mjs";
3
3
 
4
4
  //#region src/embed.d.ts
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-C0eTgNNu.mjs";
1
+ import { a as toInitMessage, i as createPendingExportManager, r as createEventRegistry, t as createPostMessageTransport } from "./transport-C8UTS3Fa.mjs";
2
2
 
3
3
  //#region src/embed.ts
4
4
  function buildEmbedUrl(options) {
package/dist/index.d.mts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { StatelyApiClientOptions, StatelyApiError, StatelyApiUrlOptions, createStatelyApiClient, createStatelyApiUrl } from "./api.mjs";
2
- import { a as EmbedMode, c as ExportFormatMap, f as ProjectEmbedMachine, i as EmbedEventName, l as InitOptions, m as UploadResult, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig } from "./protocol-CDoCcaIP.mjs";
2
+ import { a as EmbedMode, c as ExportFormatMap, f as ProjectEmbedMachine, i as EmbedEventName, l as InitOptions, m as UploadResult, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig } from "./protocol-DN4mH4jR.mjs";
3
3
  import { AssetUploadAdapter, AssetUploadContext, AssetUploadRequest, CreateS3AssetUploadAdapterOptions, CreateSupabaseAssetUploadAdapterOptions, S3UploadTarget, SupabaseStorageClient, createS3AssetUploadAdapter, createSupabaseAssetUploadAdapter } from "./assetStorage.mjs";
4
- import { ConnectedRepo, CreateMachineFromDefinitionInput, CreateMachineFromTemplateInput, CreateMachineInput, CreateMachineTemplate, CreateProjectInput, EnsureProjectInput, ExtractMachinesResponse, ExtractedMachine, GetMachineOptions, ProjectData, ProjectMachine, ProjectVisibility, RepoType, StudioApiError, StudioClient, StudioClientOptions, StudioMachineRecord, VerifyApiKeyResponse, XStateVersion, createStatelyClient } from "./studio.mjs";
4
+ import { ConnectedRepo, CreateMachineFromDefinitionInput, CreateMachineFromTemplateInput, CreateMachineInput, CreateMachineTemplate, CreateProjectInput, EnsureProjectInput, ExtractMachinesResponse, ExtractedMachine, GetMachineOptions, ProjectData, ProjectMachine, ProjectVisibility, RepoType, StudioApiError, StudioClient, StudioClientOptions, StudioMachineRecord, UpdateMachineInput, VerifyApiKeyResponse, XStateVersion, createStatelyClient } from "./studio.mjs";
5
5
  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-DpBGHZwl.mjs";
6
- import { PlanSyncOptions, PullSyncResult, PushSyncOptions, PushSyncProjectOptions, PushSyncResult, ResolvedSyncInput, SyncInputFormat, SyncPlan, SyncPlanSummary } from "./sync.mjs";
6
+ import { PlanSyncOptions, PullSyncResult, PushLocalMachineLinksResult, PushSyncOptions, PushSyncProjectOptions, PushSyncResult, ResolvedSyncInput, SyncInputFormat, SyncPlan, SyncPlanSummary } from "./sync.mjs";
7
7
  import { AssetConfig, StatelyEmbed, StatelyEmbedOptions, createStatelyEmbed } from "./embed.mjs";
8
- 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-BMIJcsFh.mjs";
8
+ 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-Bg9FTvb3.mjs";
9
9
  import { ActionLocation, GraphPatch } from "./patchTypes.mjs";
10
10
  import { JSONSchema7 } from "json-schema";
11
11
  import { UnknownMachineConfig } from "xstate";
@@ -185,4 +185,4 @@ declare function contextSchemaToTSType(context: Record<string, JSONSchema7> | nu
185
185
  */
186
186
  declare function eventsSchemaToTSType(events: Record<string, JSONSchema7> | null | undefined): string | null;
187
187
  //#endregion
188
- export { type ActionLocation, type AdoptedActor, type AssetConfig, type AssetUploadAdapter, type AssetUploadContext, type AssetUploadRequest, type CodeGenGraph, type CommentsConfig, type ConnectedRepo, type CreateInspectorOptions, type CreateMachineFromDefinitionInput, type CreateMachineFromTemplateInput, type CreateMachineInput, type CreateMachineTemplate, type CreateProjectInput, type CreateS3AssetUploadAdapterOptions, type CreateSupabaseAssetUploadAdapterOptions, type DigraphAction, type DigraphConfig, type DigraphEdgeConfig, type DigraphNodeConfig, type EmbedEventHandler, type EmbedEventMap, type EmbedEventName, type EmbedMode, type EnsureProjectInput, type EventTypeData, type ExportCallOptions, type ExportFormat, type ExportFormatMap, type ExtractMachinesResponse, type ExtractedMachine, type GetMachineOptions, type GraphPatch, type InitOptions, type Inspector, type InspectorEvents, type MachineConfigOptions, type ManualActorOptions, type PlanSyncOptions, type ProjectData, type ProjectEmbedMachine, type ProjectMachine, type ProjectVisibility, type PullSyncResult, type PushSyncOptions, type PushSyncProjectOptions, type PushSyncResult, RawCode, type RepoType, type ResolvedSyncInput, type S3UploadTarget, type StateNodeJSONData, type StatelyAction, type StatelyApiClientOptions, StatelyApiError, type StatelyApiUrlOptions, type StatelyEdgeData, type StatelyEmbed, type StatelyEmbedOptions, type StatelyGraph, type StatelyGraphData, type StatelyGuard, type StatelyInvoke, type StatelyNodeData, type StatelyPragma, type StatelyPragmaAttachment, type StudioAction, StudioApiError, type StudioClient, type StudioClientOptions, type StudioEdge, type StudioMachine, type StudioMachineRecord, type StudioNode, type SupabaseStorageClient, type SyncInputFormat, type SyncPlan, type SyncPlanSummary, type Transport, type UploadResult, type UpsertStatelyPragmaOptions, type VerifyApiKeyResponse, type XStateTSOptions, type XStateVersion, contextSchemaToTSType, createPostMessageTransport, createS3AssetUploadAdapter, createStatelyApiClient, createStatelyApiUrl, createStatelyClient, createStatelyEmbed, createStatelyInspector, createSupabaseAssetUploadAdapter, createWebSocketTransport, eventsSchemaToTSType, findStatelyPragmaAttachments, fromStudioMachine, getStatelyPragma, graphToMachineConfig, graphToXStateTS, jsonSchemaToTSType, raw, serializeJS, studioMachineConverter, toStudioMachine, upsertStatelyPragma };
188
+ export { type ActionLocation, type AdoptedActor, type AssetConfig, type AssetUploadAdapter, type AssetUploadContext, type AssetUploadRequest, type CodeGenGraph, type CommentsConfig, type ConnectedRepo, type CreateInspectorOptions, type CreateMachineFromDefinitionInput, type CreateMachineFromTemplateInput, type CreateMachineInput, type CreateMachineTemplate, type CreateProjectInput, type CreateS3AssetUploadAdapterOptions, type CreateSupabaseAssetUploadAdapterOptions, type DigraphAction, type DigraphConfig, type DigraphEdgeConfig, type DigraphNodeConfig, type EmbedEventHandler, type EmbedEventMap, type EmbedEventName, type EmbedMode, type EnsureProjectInput, type EventTypeData, type ExportCallOptions, type ExportFormat, type ExportFormatMap, type ExtractMachinesResponse, type ExtractedMachine, type GetMachineOptions, type GraphPatch, type InitOptions, type Inspector, type InspectorEvents, type MachineConfigOptions, type ManualActorOptions, type PlanSyncOptions, type ProjectData, type ProjectEmbedMachine, type ProjectMachine, type ProjectVisibility, type PullSyncResult, type PushLocalMachineLinksResult, type PushSyncOptions, type PushSyncProjectOptions, type PushSyncResult, RawCode, type RepoType, type ResolvedSyncInput, type S3UploadTarget, type StateNodeJSONData, type StatelyAction, type StatelyApiClientOptions, StatelyApiError, type StatelyApiUrlOptions, type StatelyEdgeData, type StatelyEmbed, type StatelyEmbedOptions, type StatelyGraph, type StatelyGraphData, type StatelyGuard, type StatelyInvoke, type StatelyNodeData, type StatelyPragma, type StatelyPragmaAttachment, type StudioAction, StudioApiError, type StudioClient, type StudioClientOptions, type StudioEdge, type StudioMachine, type StudioMachineRecord, type StudioNode, type SupabaseStorageClient, type SyncInputFormat, type SyncPlan, type SyncPlanSummary, type Transport, type UpdateMachineInput, type UploadResult, type UpsertStatelyPragmaOptions, type VerifyApiKeyResponse, type XStateTSOptions, type XStateVersion, contextSchemaToTSType, createPostMessageTransport, createS3AssetUploadAdapter, createStatelyApiClient, createStatelyApiUrl, createStatelyClient, createStatelyEmbed, createStatelyInspector, createSupabaseAssetUploadAdapter, createWebSocketTransport, eventsSchemaToTSType, findStatelyPragmaAttachments, fromStudioMachine, getStatelyPragma, graphToMachineConfig, graphToXStateTS, jsonSchemaToTSType, raw, serializeJS, studioMachineConverter, toStudioMachine, upsertStatelyPragma };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as createWebSocketTransport, t as createPostMessageTransport } from "./transport-C0eTgNNu.mjs";
1
+ import { n as createWebSocketTransport, t as createPostMessageTransport } from "./transport-C8UTS3Fa.mjs";
2
2
  import { createStatelyEmbed } from "./embed.mjs";
3
3
  import { createS3AssetUploadAdapter, createSupabaseAssetUploadAdapter } from "./assetStorage.mjs";
4
4
  import { createStatelyInspector } from "./inspect.mjs";
@@ -1,4 +1,4 @@
1
- import { c as ExportFormatMap, o as ExportCallOptions, p as ProtocolMessage, r as EmbedEventMap, s as ExportFormat } from "./protocol-CDoCcaIP.mjs";
1
+ import { c as ExportFormatMap, o as ExportCallOptions, p as ProtocolMessage, r as EmbedEventMap, s as ExportFormat } from "./protocol-DN4mH4jR.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-CDoCcaIP.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-BMIJcsFh.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-DN4mH4jR.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-Bg9FTvb3.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-C0eTgNNu.mjs";
1
+ import { i as createPendingExportManager, n as createWebSocketTransport, r as createEventRegistry } from "./transport-C8UTS3Fa.mjs";
2
2
 
3
3
  //#region src/inspect.ts
4
4
  const defaultSerializeSnapshot = (snapshot) => ({
@@ -60,9 +60,8 @@ interface ExportFormatMap {
60
60
  };
61
61
  result: string;
62
62
  };
63
- json: {
63
+ 'xstate-json': {
64
64
  options: {
65
- simplifyArrays?: boolean;
66
65
  version?: 4 | 5;
67
66
  };
68
67
  result: Record<string, unknown>;
package/dist/studio.d.mts CHANGED
@@ -75,6 +75,11 @@ interface CreateMachineFromTemplateInput {
75
75
  xstateVersion?: XStateVersion;
76
76
  }
77
77
  type CreateMachineInput<TDefinition = Record<string, unknown>> = CreateMachineFromDefinitionInput<TDefinition> | CreateMachineFromTemplateInput;
78
+ interface UpdateMachineInput<TDefinition = Record<string, unknown>> {
79
+ id: string;
80
+ name?: string;
81
+ definition?: TDefinition;
82
+ }
78
83
  interface StudioMachineRecord<TDefinition = Record<string, unknown>> {
79
84
  id: string;
80
85
  name: string;
@@ -99,6 +104,7 @@ interface StudioClient {
99
104
  machines: {
100
105
  create<TMachine = Record<string, unknown>>(input: CreateMachineInput<TMachine>): Promise<StudioMachineRecord<TMachine>>;
101
106
  createMany<TMachine = Record<string, unknown>>(input: CreateMachineInput<TMachine>): Promise<Array<StudioMachineRecord<TMachine>>>;
107
+ update<TMachine = Record<string, unknown>>(input: UpdateMachineInput<TMachine>): Promise<StudioMachineRecord<TMachine>>;
102
108
  get<TMachine = Record<string, unknown>>(machineId: string, options?: GetMachineOptions): Promise<TMachine>;
103
109
  };
104
110
  code: {
@@ -109,4 +115,4 @@ interface StudioClient {
109
115
  }
110
116
  declare function createStatelyClient(options?: StudioClientOptions): StudioClient;
111
117
  //#endregion
112
- export { ConnectedRepo, CreateMachineFromDefinitionInput, CreateMachineFromTemplateInput, CreateMachineInput, CreateMachineTemplate, CreateProjectInput, EnsureProjectInput, ExtractMachinesResponse, ExtractedMachine, GetMachineOptions, ProjectData, ProjectMachine, ProjectVisibility, RepoType, StudioApiError, StudioClient, StudioClientOptions, StudioMachineRecord, VerifyApiKeyResponse, XStateVersion, createStatelyClient };
118
+ export { ConnectedRepo, CreateMachineFromDefinitionInput, CreateMachineFromTemplateInput, CreateMachineInput, CreateMachineTemplate, CreateProjectInput, EnsureProjectInput, ExtractMachinesResponse, ExtractedMachine, GetMachineOptions, ProjectData, ProjectMachine, ProjectVisibility, RepoType, StudioApiError, StudioClient, StudioClientOptions, StudioMachineRecord, UpdateMachineInput, VerifyApiKeyResponse, XStateVersion, createStatelyClient };
package/dist/studio.mjs CHANGED
@@ -102,6 +102,13 @@ function createStatelyClient(options = {}) {
102
102
  async createMany(input) {
103
103
  return [await this.create(input)];
104
104
  },
105
+ update(input) {
106
+ return request(`/machines/${encodeURIComponent(input.id)}`, {
107
+ method: "PATCH",
108
+ headers: { "Content-Type": "application/json" },
109
+ body: JSON.stringify(input)
110
+ });
111
+ },
105
112
  get(machineId, getOptions = {}) {
106
113
  const search = new URLSearchParams();
107
114
  if (getOptions.version) search.set("version", getOptions.version);
package/dist/sync.d.mts CHANGED
@@ -52,8 +52,26 @@ interface PushSyncResult {
52
52
  machine: StudioMachineRecord<DigraphConfig>;
53
53
  outputPath?: string;
54
54
  }
55
+ interface PushLocalMachineLinksResult {
56
+ sourcePath: string;
57
+ project: ProjectData;
58
+ created: Array<{
59
+ machineIndex: number;
60
+ machine: StudioMachineRecord<DigraphConfig>;
61
+ }>;
62
+ updated: Array<{
63
+ machineIndex: number;
64
+ machine: StudioMachineRecord<DigraphConfig>;
65
+ }>;
66
+ skipped: Array<{
67
+ machineIndex: number;
68
+ reason: string;
69
+ }>;
70
+ outputPath?: string;
71
+ }
72
+ declare function pushLocalMachineLinks(options: PushSyncOptions): Promise<PushLocalMachineLinksResult>;
55
73
  declare function planSync(options: PlanSyncOptions): Promise<SyncPlan>;
56
74
  declare function pullSync(options: PlanSyncOptions): Promise<PullSyncResult>;
57
75
  declare function pushSync(options: PushSyncOptions): Promise<PushSyncResult>;
58
76
  //#endregion
59
- export { PlanSyncOptions, PullSyncResult, PushSyncOptions, PushSyncProjectOptions, PushSyncResult, ResolvedSyncInput, SyncInputFormat, SyncPlan, SyncPlanSummary, planSync, pullSync, pushSync };
77
+ export { PlanSyncOptions, PullSyncResult, PushLocalMachineLinksResult, PushSyncOptions, PushSyncProjectOptions, PushSyncResult, ResolvedSyncInput, SyncInputFormat, SyncPlan, SyncPlanSummary, planSync, pullSync, pushLocalMachineLinks, pushSync };
package/dist/sync.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createStatelyClient } from "./studio.mjs";
2
- import { d as upsertStatelyPragma, t as graphToXStateTS } from "./graphToXStateTS-Gzh0ZqbN.mjs";
2
+ import { d as upsertStatelyPragma, l as findStatelyPragmaAttachments, t as graphToXStateTS, u as getStatelyPragma } from "./graphToXStateTS-Gzh0ZqbN.mjs";
3
3
  import { fromStudioMachine, toStudioMachine } from "./graph.mjs";
4
4
  import { getDiff, isEmptyDiff } from "@statelyai/graph";
5
5
  import fs from "node:fs/promises";
@@ -186,6 +186,90 @@ function fromXStateConfig(config) {
186
186
  data: {}
187
187
  };
188
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
+ }
189
273
  async function resolveLocalFile(locator, options) {
190
274
  const filePath = path.resolve(options.cwd ?? process.cwd(), locator);
191
275
  const contents = await fs.readFile(filePath, "utf8");
@@ -350,12 +434,20 @@ async function pushSync(options) {
350
434
  if (source.kind !== "local-file") throw new Error("pushSync currently requires a local source file.");
351
435
  const sourcePath = source.locator;
352
436
  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
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)
358
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
+ }
359
451
  let outputPath;
360
452
  if (source.format === "xstate") {
361
453
  const contents = await fs.readFile(sourcePath, "utf8");
@@ -374,4 +466,4 @@ async function pushSync(options) {
374
466
  }
375
467
 
376
468
  //#endregion
377
- export { planSync, pullSync, pushSync };
469
+ export { planSync, pullSync, pushLocalMachineLinks, pushSync };
@@ -1,7 +1,7 @@
1
1
  //#region src/clientUtils.ts
2
2
  const jsonResultFormats = new Set([
3
3
  "digraph",
4
- "json",
4
+ "xstate-json",
5
5
  "xgraph"
6
6
  ]);
7
7
  function createRequestId() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/sdk",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "statelyai": "./dist/cli.mjs"
@@ -54,7 +54,9 @@
54
54
  "import": "./dist/assetStorage.mjs"
55
55
  },
56
56
  "./statelyai.schema.json": "./schemas/statelyai.schema.json",
57
- "./schemas/statelyai.schema.json": "./schemas/statelyai.schema.json"
57
+ "./schemas/statelyai.schema.json": "./schemas/statelyai.schema.json",
58
+ "./xstate-json.schema.json": "./schemas/xstate-json.schema.json",
59
+ "./schemas/xstate-json.schema.json": "./schemas/xstate-json.schema.json"
58
60
  },
59
61
  "dependencies": {
60
62
  "@oclif/core": "^4.10.3",
@@ -54,7 +54,7 @@
54
54
  "enum": [
55
55
  "auto",
56
56
  "xstate",
57
- "json",
57
+ "xstate-json",
58
58
  "xgraph",
59
59
  "digraph",
60
60
  "mermaid",
@@ -0,0 +1,214 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://stately.ai/schemas/xstate-json.json",
4
+ "title": "XState JSON machine config",
5
+ "description": "Strict JSON subset of an XState machine config that can be passed to createMachine(config). Shorthand string forms are normalized to object and array forms.",
6
+ "$ref": "#/$defs/machineConfig",
7
+ "$defs": {
8
+ "jsonValue": {
9
+ "description": "Any JSON value.",
10
+ "oneOf": [
11
+ { "type": "null" },
12
+ { "type": "boolean" },
13
+ { "type": "number" },
14
+ { "type": "string" },
15
+ {
16
+ "type": "array",
17
+ "items": { "$ref": "#/$defs/jsonValue" }
18
+ },
19
+ {
20
+ "type": "object",
21
+ "additionalProperties": { "$ref": "#/$defs/jsonValue" }
22
+ }
23
+ ]
24
+ },
25
+ "jsonObject": {
26
+ "type": "object",
27
+ "additionalProperties": { "$ref": "#/$defs/jsonValue" }
28
+ },
29
+ "parameterizedObject": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "required": ["type"],
33
+ "properties": {
34
+ "type": { "type": "string", "minLength": 1 },
35
+ "params": { "$ref": "#/$defs/jsonValue" }
36
+ }
37
+ },
38
+ "actionObject": {
39
+ "$ref": "#/$defs/parameterizedObject"
40
+ },
41
+ "guardObject": {
42
+ "$ref": "#/$defs/parameterizedObject"
43
+ },
44
+ "transitionObject": {
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "properties": {
48
+ "target": {
49
+ "type": "array",
50
+ "items": { "type": "string", "minLength": 1 }
51
+ },
52
+ "guard": { "$ref": "#/$defs/guardObject" },
53
+ "actions": {
54
+ "type": "array",
55
+ "items": { "$ref": "#/$defs/actionObject" }
56
+ },
57
+ "reenter": { "type": "boolean" },
58
+ "meta": { "$ref": "#/$defs/jsonObject" },
59
+ "description": { "type": "string" }
60
+ }
61
+ },
62
+ "transitionArray": {
63
+ "type": "array",
64
+ "items": { "$ref": "#/$defs/transitionObject" }
65
+ },
66
+ "transitionsByEvent": {
67
+ "type": "object",
68
+ "additionalProperties": { "$ref": "#/$defs/transitionArray" }
69
+ },
70
+ "invokeObject": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": ["src"],
74
+ "properties": {
75
+ "id": { "type": "string" },
76
+ "systemId": { "type": "string" },
77
+ "src": { "type": "string", "minLength": 1 },
78
+ "input": { "$ref": "#/$defs/jsonValue" },
79
+ "onDone": { "$ref": "#/$defs/transitionArray" },
80
+ "onError": { "$ref": "#/$defs/transitionArray" },
81
+ "onSnapshot": { "$ref": "#/$defs/transitionArray" }
82
+ }
83
+ },
84
+ "stateNodeConfig": {
85
+ "type": "object",
86
+ "additionalProperties": false,
87
+ "properties": {
88
+ "id": { "type": "string" },
89
+ "type": {
90
+ "enum": ["atomic", "compound", "parallel", "final", "history"]
91
+ },
92
+ "history": {
93
+ "anyOf": [
94
+ { "enum": ["shallow", "deep"] },
95
+ { "type": "boolean" }
96
+ ]
97
+ },
98
+ "states": {
99
+ "type": "object",
100
+ "additionalProperties": { "$ref": "#/$defs/stateNodeConfig" }
101
+ },
102
+ "invoke": {
103
+ "type": "array",
104
+ "items": { "$ref": "#/$defs/invokeObject" }
105
+ },
106
+ "on": { "$ref": "#/$defs/transitionsByEvent" },
107
+ "entry": {
108
+ "type": "array",
109
+ "items": { "$ref": "#/$defs/actionObject" }
110
+ },
111
+ "exit": {
112
+ "type": "array",
113
+ "items": { "$ref": "#/$defs/actionObject" }
114
+ },
115
+ "onDone": { "$ref": "#/$defs/transitionArray" },
116
+ "after": { "$ref": "#/$defs/transitionsByEvent" },
117
+ "always": { "$ref": "#/$defs/transitionArray" },
118
+ "meta": { "$ref": "#/$defs/jsonObject" },
119
+ "output": { "$ref": "#/$defs/jsonValue" },
120
+ "order": { "type": "number" },
121
+ "tags": {
122
+ "type": "array",
123
+ "items": { "type": "string" }
124
+ },
125
+ "description": { "type": "string" },
126
+ "target": { "type": "string" },
127
+ "initial": { "type": "string" }
128
+ }
129
+ },
130
+ "machineConfig": {
131
+ "type": "object",
132
+ "additionalProperties": false,
133
+ "properties": {
134
+ "id": { "type": "string" },
135
+ "type": {
136
+ "enum": ["atomic", "compound", "parallel", "final", "history"]
137
+ },
138
+ "history": {
139
+ "anyOf": [
140
+ { "enum": ["shallow", "deep"] },
141
+ { "type": "boolean" }
142
+ ]
143
+ },
144
+ "states": {
145
+ "type": "object",
146
+ "additionalProperties": { "$ref": "#/$defs/stateNodeConfig" }
147
+ },
148
+ "invoke": {
149
+ "type": "array",
150
+ "items": { "$ref": "#/$defs/invokeObject" }
151
+ },
152
+ "on": { "$ref": "#/$defs/transitionsByEvent" },
153
+ "entry": {
154
+ "type": "array",
155
+ "items": { "$ref": "#/$defs/actionObject" }
156
+ },
157
+ "exit": {
158
+ "type": "array",
159
+ "items": { "$ref": "#/$defs/actionObject" }
160
+ },
161
+ "onDone": { "$ref": "#/$defs/transitionArray" },
162
+ "after": { "$ref": "#/$defs/transitionsByEvent" },
163
+ "always": { "$ref": "#/$defs/transitionArray" },
164
+ "meta": { "$ref": "#/$defs/jsonObject" },
165
+ "output": { "$ref": "#/$defs/jsonValue" },
166
+ "order": { "type": "number" },
167
+ "tags": {
168
+ "type": "array",
169
+ "items": { "type": "string" }
170
+ },
171
+ "description": { "type": "string" },
172
+ "target": { "type": "string" },
173
+ "initial": { "type": "string" },
174
+ "schemas": { "$ref": "#/$defs/jsonValue" },
175
+ "version": { "type": "string" },
176
+ "context": { "$ref": "#/$defs/jsonObject" },
177
+ "options": {
178
+ "type": "object",
179
+ "additionalProperties": false,
180
+ "properties": {
181
+ "maxIterations": { "type": "number" }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ },
187
+ "examples": [
188
+ {
189
+ "id": "trafficLight",
190
+ "initial": "green",
191
+ "states": {
192
+ "green": {
193
+ "tags": ["go"],
194
+ "on": {
195
+ "TIMER": [
196
+ {
197
+ "target": ["yellow"],
198
+ "actions": [{ "type": "trackChange" }]
199
+ }
200
+ ]
201
+ }
202
+ },
203
+ "yellow": {
204
+ "on": {
205
+ "TIMER": [{ "target": ["red"] }]
206
+ }
207
+ },
208
+ "red": {
209
+ "type": "final"
210
+ }
211
+ }
212
+ }
213
+ ]
214
+ }