@statelyai/sdk 0.6.0 → 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
@@ -137,6 +137,8 @@ import {
137
137
  createStatelyClient,
138
138
  createStatelyEmbed,
139
139
  createStatelyInspector,
140
+ createS3AssetUploadAdapter,
141
+ createSupabaseAssetUploadAdapter,
140
142
  fromStudioMachine,
141
143
  graphToMachineConfig,
142
144
  graphToXStateTS,
@@ -156,6 +158,10 @@ import {
156
158
  } from '@statelyai/sdk/api';
157
159
  import { fromStudioMachine, toStudioMachine } from '@statelyai/sdk/graph';
158
160
  import { planSync, pullSync, pushSync } from '@statelyai/sdk/sync';
161
+ import {
162
+ createS3AssetUploadAdapter,
163
+ createSupabaseAssetUploadAdapter,
164
+ } from '@statelyai/sdk/assetStorage';
159
165
  import type { GraphPatch } from '@statelyai/sdk/patchTypes';
160
166
  ```
161
167
 
@@ -200,12 +206,15 @@ Available client methods:
200
206
  | `studio.projects.get(projectId)` | Fetch a project and its machines |
201
207
  | `studio.machines.create({ projectVersionId, ... })` | Create a machine through the published REST API |
202
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 |
203
210
  | `studio.machines.get(machineId, { version? })` | Fetch a machine, optionally pinned to a version |
204
211
  | `studio.code.extractMachines(code, { apiKey? })` | Extract machine configs from source text |
205
212
 
206
213
  ## Sync Helpers
207
214
 
208
- `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.
209
218
 
210
219
  ```ts
211
220
  import { pushSync } from '@statelyai/sdk/sync';
@@ -226,7 +235,7 @@ const result = await pushSync({
226
235
  });
227
236
  ```
228
237
 
229
- 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`.
230
239
 
231
240
  ```json
232
241
  {
@@ -235,17 +244,7 @@ Projects can also check in a `statelyai.json` file to describe which local sourc
235
244
  "projectId": "project_123",
236
245
  "studioUrl": "https://stately.ai",
237
246
  "defaultXStateVersion": 5,
238
- "sources": [
239
- {
240
- "include": ["src/**/*.ts", "src/**/*.tsx"],
241
- "exclude": ["**/*.test.ts", "**/dist/**"],
242
- "format": "xstate"
243
- },
244
- {
245
- "include": ["docs/**/*.mmd"],
246
- "format": "mermaid"
247
- }
248
- ]
247
+ "sources": []
249
248
  }
250
249
  ```
251
250
 
@@ -417,7 +416,7 @@ const aslYaml = await embed.export('asl-yaml');
417
416
 
418
417
  <!-- supported export formats from ExportFormatMap in src/protocol.ts -->
419
418
 
420
- 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`
421
420
 
422
421
  #### `embed.on(event, handler)` / `embed.off(event, handler)`
423
422
 
@@ -462,9 +461,54 @@ const embed = createStatelyEmbed({
462
461
  });
463
462
  ```
464
463
 
464
+ For reusable integrations, pass an upload adapter instead of wiring the callback inline:
465
+
466
+ ```ts
467
+ import { createStatelyEmbed, createS3AssetUploadAdapter } from '@statelyai/sdk';
468
+
469
+ const adapter = createS3AssetUploadAdapter({
470
+ bucket: 'assets',
471
+ publicBaseUrl: 'https://cdn.example.com/assets',
472
+ async getUploadTarget({ key, contentType }) {
473
+ const response = await fetch('/api/assets/presign', {
474
+ method: 'POST',
475
+ headers: { 'Content-Type': 'application/json' },
476
+ body: JSON.stringify({ key, contentType }),
477
+ });
478
+ return response.json();
479
+ },
480
+ });
481
+
482
+ const embed = createStatelyEmbed({
483
+ baseUrl: 'https://stately.ai',
484
+ assets: { adapter },
485
+ });
486
+ ```
487
+
488
+ Supabase storage can use the same adapter contract:
489
+
490
+ ```ts
491
+ import {
492
+ createStatelyEmbed,
493
+ createSupabaseAssetUploadAdapter,
494
+ } from '@statelyai/sdk';
495
+
496
+ const adapter = createSupabaseAssetUploadAdapter({
497
+ client: supabaseClient,
498
+ bucket: 'assets',
499
+ projectVersionId,
500
+ });
501
+
502
+ const embed = createStatelyEmbed({
503
+ baseUrl: 'https://stately.ai',
504
+ assets: { adapter },
505
+ });
506
+ ```
507
+
465
508
  | Option | Type | Description |
466
509
  | ----------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
467
- | `onUploadRequest` | `(file: File, context: { stateNodeId: string }) => Promise<UploadResult>` | **Required.** Called when the editor needs to upload a file |
510
+ | `onUploadRequest` | `(file: File, context: { stateNodeId: string }) => Promise<UploadResult>` | Called when the editor needs to upload a file |
511
+ | `adapter` | `AssetUploadAdapter` | Provider-specific uploader, such as the S3 or Supabase adapter |
468
512
  | `accept` | `string[]` | Accepted MIME types. Supports wildcards like `image/*` |
469
513
  | `maxFileSize` | `number` | Max file size in bytes. Defaults to `10_485_760` |
470
514
 
@@ -554,11 +598,15 @@ Installing the package also exposes a `statelyai` binary:
554
598
  npx statelyai open ./checkout.machine.ts
555
599
 
556
600
  statelyai init
601
+ statelyai init --scan
557
602
  statelyai login
558
603
  statelyai auth status
559
604
  statelyai plan ./checkout.machine.ts machine-id
560
605
  statelyai diff ./checkout.machine.ts machine-id --fail-on-changes
606
+ statelyai push
607
+ statelyai push ./checkout.machine.ts
561
608
  statelyai pull machine-id ./checkout.machine.ts
609
+ statelyai pull ./checkout.machine.ts
562
610
  statelyai open ./checkout.machine.ts
563
611
  ```
564
612
 
@@ -568,13 +616,15 @@ Available commands:
568
616
 
569
617
  | Command | Description |
570
618
  | ---------------------------------- | --------------------------------------------------------------------------- |
571
- | `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 |
572
620
  | `statelyai login` | Store an API key for future CLI use |
573
621
  | `statelyai logout` | Remove a stored API key |
574
622
  | `statelyai auth status` | Show whether the CLI would use an environment variable or stored credential |
575
623
  | `statelyai plan <source> <target>` | Print a semantic sync summary |
576
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 |
577
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 |
578
628
  | `statelyai open <file>` | Open a local file in a browser-backed visual editor session |
579
629
 
580
630
  Common flags:
@@ -585,6 +635,8 @@ Common flags:
585
635
 
586
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.
587
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
+
588
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.
589
641
 
590
642
  ## Transport Helpers
@@ -0,0 +1,73 @@
1
+ import { m as UploadResult } from "./protocol-DN4mH4jR.mjs";
2
+
3
+ //#region src/assetStorage.d.ts
4
+ interface AssetUploadContext {
5
+ stateNodeId: string;
6
+ }
7
+ interface AssetUploadRequest extends AssetUploadContext {
8
+ file: File;
9
+ }
10
+ interface AssetUploadAdapter {
11
+ upload(request: AssetUploadRequest): Promise<UploadResult>;
12
+ accept?: string[];
13
+ maxFileSize?: number;
14
+ }
15
+ type AssetObjectKeyFactory = (request: AssetUploadRequest) => string | Promise<string>;
16
+ interface S3UploadTarget {
17
+ uploadUrl: string;
18
+ publicUrl?: string;
19
+ method?: 'PUT' | 'POST';
20
+ headers?: Record<string, string>;
21
+ fields?: Record<string, string>;
22
+ }
23
+ interface CreateS3AssetUploadAdapterOptions {
24
+ bucket?: string;
25
+ publicBaseUrl?: string;
26
+ accept?: string[];
27
+ maxFileSize?: number;
28
+ key?: AssetObjectKeyFactory;
29
+ getUploadTarget: (request: AssetUploadRequest & {
30
+ key: string;
31
+ contentType: string;
32
+ bucket?: string;
33
+ }) => Promise<S3UploadTarget>;
34
+ }
35
+ interface SupabaseStorageBucket {
36
+ upload(path: string, file: File, options?: {
37
+ cacheControl?: string;
38
+ contentType?: string;
39
+ upsert?: boolean;
40
+ }): Promise<{
41
+ data: {
42
+ path: string;
43
+ } | null;
44
+ error: Error | {
45
+ message: string;
46
+ } | null;
47
+ }>;
48
+ getPublicUrl?(path: string): {
49
+ data: {
50
+ publicUrl: string;
51
+ };
52
+ };
53
+ }
54
+ interface SupabaseStorageClient {
55
+ storage: {
56
+ from(bucket: string): SupabaseStorageBucket;
57
+ };
58
+ }
59
+ interface CreateSupabaseAssetUploadAdapterOptions {
60
+ client: SupabaseStorageClient;
61
+ bucket?: string;
62
+ publicBaseUrl?: string;
63
+ projectVersionId?: string;
64
+ accept?: string[];
65
+ maxFileSize?: number;
66
+ cacheControl?: string;
67
+ upsert?: boolean;
68
+ key?: AssetObjectKeyFactory;
69
+ }
70
+ declare function createS3AssetUploadAdapter(options: CreateS3AssetUploadAdapterOptions): AssetUploadAdapter;
71
+ declare function createSupabaseAssetUploadAdapter(options: CreateSupabaseAssetUploadAdapterOptions): AssetUploadAdapter;
72
+ //#endregion
73
+ export { AssetObjectKeyFactory, AssetUploadAdapter, AssetUploadContext, AssetUploadRequest, CreateS3AssetUploadAdapterOptions, CreateSupabaseAssetUploadAdapterOptions, S3UploadTarget, SupabaseStorageClient, createS3AssetUploadAdapter, createSupabaseAssetUploadAdapter };
@@ -0,0 +1,99 @@
1
+ //#region src/assetStorage.ts
2
+ function sanitizeFileName(name) {
3
+ return name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4
+ }
5
+ function defaultObjectKey({ file, stateNodeId }) {
6
+ const fileName = sanitizeFileName(file.name) || "asset";
7
+ return `${stateNodeId}/${crypto.randomUUID()}-${fileName}`;
8
+ }
9
+ function createProjectObjectKey(projectVersionId) {
10
+ return ({ file }) => {
11
+ const fileName = sanitizeFileName(file.name) || "asset";
12
+ return `${projectVersionId}/${crypto.randomUUID()}-${fileName}`;
13
+ };
14
+ }
15
+ function joinUrl(baseUrl, key) {
16
+ return `${baseUrl.replace(/\/+$/, "")}/${key.split("/").map(encodeURIComponent).join("/")}`;
17
+ }
18
+ async function uploadToTarget(target, file) {
19
+ if (target.method === "POST" || target.fields) {
20
+ const formData = new FormData();
21
+ Object.entries(target.fields ?? {}).forEach(([name, value]) => {
22
+ formData.append(name, value);
23
+ });
24
+ formData.append("file", file);
25
+ const response = await fetch(target.uploadUrl, {
26
+ method: "POST",
27
+ body: formData
28
+ });
29
+ if (!response.ok) throw new Error(`Asset upload failed with ${response.status}`);
30
+ return;
31
+ }
32
+ const response = await fetch(target.uploadUrl, {
33
+ method: target.method ?? "PUT",
34
+ headers: {
35
+ "Content-Type": file.type || "application/octet-stream",
36
+ ...target.headers
37
+ },
38
+ body: file
39
+ });
40
+ if (!response.ok) throw new Error(`Asset upload failed with ${response.status}`);
41
+ }
42
+ function createS3AssetUploadAdapter(options) {
43
+ return {
44
+ accept: options.accept,
45
+ maxFileSize: options.maxFileSize,
46
+ async upload(request) {
47
+ const key = await (options.key ?? defaultObjectKey)(request);
48
+ const target = await options.getUploadTarget({
49
+ ...request,
50
+ key,
51
+ contentType: request.file.type || "application/octet-stream",
52
+ bucket: options.bucket
53
+ });
54
+ await uploadToTarget(target, request.file);
55
+ const url = target.publicUrl ?? (options.publicBaseUrl ? joinUrl(options.publicBaseUrl, key) : void 0);
56
+ if (!url) throw new Error("S3 asset upload adapter requires a publicUrl or publicBaseUrl");
57
+ return {
58
+ url,
59
+ name: request.file.name,
60
+ metadata: {
61
+ bucket: options.bucket,
62
+ key
63
+ }
64
+ };
65
+ }
66
+ };
67
+ }
68
+ function createSupabaseAssetUploadAdapter(options) {
69
+ const bucket = options.bucket ?? "assets";
70
+ return {
71
+ accept: options.accept,
72
+ maxFileSize: options.maxFileSize,
73
+ async upload(request) {
74
+ const storage = options.client.storage.from(bucket);
75
+ const key = await (options.key ?? (options.projectVersionId ? createProjectObjectKey(options.projectVersionId) : defaultObjectKey))(request);
76
+ const { data, error } = await storage.upload(key, request.file, {
77
+ cacheControl: options.cacheControl,
78
+ contentType: request.file.type || "application/octet-stream",
79
+ upsert: options.upsert
80
+ });
81
+ if (error) throw new Error(error.message);
82
+ if (!data?.path) throw new Error("Supabase asset upload did not return a path");
83
+ const publicUrl = storage.getPublicUrl?.(data.path)?.data.publicUrl ?? (options.publicBaseUrl ? joinUrl(options.publicBaseUrl, data.path) : void 0);
84
+ if (!publicUrl) throw new Error("Supabase asset upload adapter requires getPublicUrl or publicBaseUrl");
85
+ return {
86
+ url: publicUrl,
87
+ name: request.file.name,
88
+ metadata: {
89
+ bucket,
90
+ path: data.path,
91
+ key: data.path
92
+ }
93
+ };
94
+ }
95
+ };
96
+ }
97
+
98
+ //#endregion
99
+ export { createS3AssetUploadAdapter, createSupabaseAssetUploadAdapter };
package/dist/cli.d.mts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { ConnectedRepo, CreateProjectInput, ProjectData, StudioClient } from "./studio.mjs";
2
- import "./graph-CB-ALrdk.mjs";
2
+ import "./graph-DpBGHZwl.mjs";
3
3
  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 };