statelyai 0.9.4 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,7 @@ npm install --global statelyai
27
27
  | Goal | Command | Writes |
28
28
  | ---------------------------- | ---------------------------------------------------- | ------------------------------------------------------- |
29
29
  | List local machines | `statelyai scan` | Nothing |
30
- | Edit one local file visually | `statelyai open <file>` | Local file, when you save in the editor |
30
+ | Edit local machines visually | `statelyai open [file]` | Local source, when you save in the editor |
31
31
  | Set up local discovery | `statelyai init --local --scan` | Local `statelyai.json` |
32
32
  | Set up project sync | `statelyai init --scan` | Remote project and local `statelyai.json` |
33
33
  | Inspect project machines | `statelyai status` | Nothing |
@@ -158,22 +158,24 @@ the credential so a later command can retry.
158
158
  "projectId": "project_123",
159
159
  "studioUrl": "https://stately.ai",
160
160
  "defaultXStateVersion": 5,
161
+ "sourceUpdateStrategy": "preserve",
161
162
  "include": ["src/**/*.ts"],
162
163
  "exclude": ["**/*.test.*", "**/*.spec.*"],
163
164
  "newMachinesDir": "src/machines"
164
165
  }
165
166
  ```
166
167
 
167
- | Field | Purpose |
168
- | ---------------------- | --------------------------------------------------------------- |
169
- | `$schema` | Published JSON Schema URL. |
170
- | `version` | Config format version. Currently `1.0.0`. |
171
- | `projectId` | Remote Studio project ID. Omitted for local-only projects. |
172
- | `studioUrl` | Studio API origin. Omitted for local-only projects. |
173
- | `defaultXStateVersion` | XState version used when creating remote machines. Minimum `5`. |
174
- | `include` | Source globs used by project-wide `push` and `pull`. |
175
- | `exclude` | Globs removed from discovery. Defaults to tests and specs. |
176
- | `newMachinesDir` | Destination for remote-only machines created by `pull`. |
168
+ | Field | Purpose |
169
+ | ---------------------- | ----------------------------------------------------------------------- |
170
+ | `$schema` | Published JSON Schema URL. |
171
+ | `version` | Config format version. Currently `1.0.0`. |
172
+ | `projectId` | Remote Studio project ID. Omitted for local-only projects. |
173
+ | `studioUrl` | Studio API origin. Omitted for local-only projects. |
174
+ | `defaultXStateVersion` | XState version used when creating remote machines. Minimum `5`. |
175
+ | `sourceUpdateStrategy` | Visual writeback mode: `preserve` or `rewrite`. Defaults to `preserve`. |
176
+ | `include` | Source globs used by project-wide `push` and `pull`. |
177
+ | `exclude` | Globs removed from discovery. Defaults to tests and specs. |
178
+ | `newMachinesDir` | Destination for remote-only machines created by `pull`. |
177
179
 
178
180
  Project-wide discovery identifies configured JavaScript and TypeScript files
179
181
  that import XState and call `createMachine(...)` or `.createMachine(...)`.
@@ -261,12 +263,21 @@ ID when present.
261
263
 
262
264
  ### `open`
263
265
 
264
- Start a local bridge and open one source file in the browser editor:
266
+ Start a local bridge and open every machine in one source file as a workspace:
265
267
 
266
268
  ```bash
267
269
  statelyai open src/checkout.machine.ts
268
270
  ```
269
271
 
272
+ Omit the file to discover every machine in the current worktree:
273
+
274
+ ```bash
275
+ statelyai open
276
+ ```
277
+
278
+ The workspace sidebar starts collapsed. Select a machine there without opening
279
+ a separate browser tab.
280
+
270
281
  Saved file changes refresh the editor. Saving visual edits writes them back to
271
282
  the source file. Editor sync sends the opened file, its reachable relative
272
283
  imports, and ancestor package manifests; unrelated sibling files are excluded.
@@ -277,6 +288,13 @@ the editor keeps the current graph visible and offers retry, copy-details, and
277
288
  diagnostic-download actions. Use `--debug` to include the structured diagnostic
278
289
  in the CLI's redacted editor protocol log.
279
290
 
291
+ Source updates default to `preserve`, which keeps existing formatting and
292
+ comments with focused edits. Use `--source-update rewrite` or set
293
+ `"sourceUpdateStrategy": "rewrite"` in `statelyai.json` to regenerate the
294
+ selected machine from its semantic model. Rewrite may change formatting and
295
+ comments inside that machine. When preserve cannot safely apply an edit, the
296
+ editor offers `Rewrite machine` without discarding the visual changes.
297
+
280
298
  Flags:
281
299
 
282
300
  - `--editor-url <url>` selects the editor origin. Default:
@@ -285,6 +303,7 @@ Flags:
285
303
  - `--port <port>` selects a port. Default: a random available port.
286
304
  - `--no-open` starts the bridge without launching a browser.
287
305
  - `--debug` logs editor protocol messages with credentials redacted.
306
+ - `--source-update <preserve|rewrite>` overrides the configured source update strategy.
288
307
 
289
308
  ### `diff`
290
309
 
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { S as run } from "./cli-DaH9lbVq.mjs";
2
+ import { S as run } from "./cli-DV60djkN.mjs";
3
3
 
4
4
  //#region src/bin.ts
5
5
  run();
@@ -11,13 +11,13 @@ import { promisify } from "node:util";
11
11
  import { Args, Command, Flags, flush, handle, run } from "@oclif/core";
12
12
  import * as http from "node:http";
13
13
  import * as ts from "typescript";
14
- import { EditorHostSession, StudioApiError, buildAuthorizeUrl, createPkcePair, createRemoteEditorSyncBackend, createStatelyClient, discoverAuthorizationServer, discoverProtectedResource, exchangeCodeForToken, findStatelyPragmaAttachments, getStatelyPragma, refreshToken, registerOAuthClient } from "@statelyai/sdk";
14
+ import { EditorHostWorkspaceSession, StudioApiError, buildAuthorizeUrl, createPkcePair, createRemoteEditorSyncBackend, createStatelyClient, discoverAuthorizationServer, discoverProtectedResource, exchangeCodeForToken, findStatelyPragmaAttachments, getStatelyPragma, refreshToken, registerOAuthClient } from "@statelyai/sdk";
15
15
  import { WebSocket, WebSocketServer } from "ws";
16
16
  import os from "node:os";
17
17
  import { PullSourceReconciliationError, planSync, pullSync, pushLocalMachineLinks } from "@statelyai/sdk/sync";
18
18
 
19
19
  //#region src/version.ts
20
- const CLI_VERSION = "0.9.4";
20
+ const CLI_VERSION = "0.10.1";
21
21
 
22
22
  //#endregion
23
23
  //#region src/cliHost.ts
@@ -57,27 +57,43 @@ function rawDataToBuffer(data) {
57
57
  }
58
58
  var RemoteEditorSession = class {
59
59
  session;
60
+ workspaceWatcher;
60
61
  constructor(options, sendMessage) {
61
62
  this.options = options;
62
63
  const send = (message) => {
63
64
  this.log("send", message.type, message);
64
65
  sendMessage(message);
65
66
  };
66
- this.session = new EditorHostSession({
67
- source: {
68
- rootUri: options.rootUri,
69
- rootFileName: options.fileName,
70
- rootDir: options.rootDir,
71
- collectDocuments: (extraUris) => collectSourceDocuments({
72
- rootFileName: options.fileName,
73
- extraUris: [...extraUris ?? []]
74
- }),
75
- applyReplacements: async (replacements, rootUri) => {
76
- await applyFileReplacements(replacements, rootUri);
77
- return [...new Set(replacements.map((replacement) => replacement.uri ?? rootUri))];
78
- },
79
- watchSavedDocuments: (listener) => watchWorkspace(options.rootDir, listener)
80
- },
67
+ const machines = options.machines ?? [{
68
+ id: `${options.rootUri}#0`,
69
+ fileName: options.fileName,
70
+ machineIndex: 0
71
+ }];
72
+ this.workspaceWatcher = createSharedWorkspaceWatcher(options.rootDir);
73
+ this.session = new EditorHostWorkspaceSession({
74
+ machines: machines.map((machine) => ({
75
+ id: machine.id,
76
+ name: machine.name,
77
+ source: {
78
+ rootUri: fileNameToUri(machine.fileName),
79
+ rootFileName: machine.fileName,
80
+ rootDir: options.rootDir,
81
+ machineIndex: machine.machineIndex,
82
+ collectDocuments: (extraUris) => collectSourceDocuments({
83
+ rootFileName: machine.fileName,
84
+ extraUris: [...extraUris ?? []]
85
+ }),
86
+ applyReplacements: async (replacements, rootUri) => {
87
+ await applyFileReplacements(replacements, rootUri);
88
+ return [...new Set(replacements.map((replacement) => replacement.uri ?? rootUri))];
89
+ },
90
+ watchSavedDocuments: (listener) => this.workspaceWatcher.subscribe(listener)
91
+ }
92
+ })),
93
+ currentMachineId: machines[0]?.id,
94
+ workspaceName: path$1.basename(options.rootDir),
95
+ projectWorkspaceOpen: false,
96
+ sourceUpdateStrategy: options.sourceUpdateStrategy ?? "preserve",
81
97
  sync: createRemoteEditorSyncBackend({
82
98
  editorUrl: options.editorUrl,
83
99
  apiKey: options.apiKey,
@@ -98,6 +114,7 @@ var RemoteEditorSession = class {
98
114
  }
99
115
  dispose() {
100
116
  this.session.dispose();
117
+ this.workspaceWatcher.dispose();
101
118
  }
102
119
  log(direction, type, payload) {
103
120
  if (this.options.debug) console.error(`[${direction}] ${type}`, redactDebugPayload(payload) ?? "");
@@ -110,10 +127,18 @@ function redactDebugPayload(payload) {
110
127
  }
111
128
  async function openEditor(options) {
112
129
  const fileName = path$1.resolve(options.fileName);
113
- await fsPromises.access(fileName);
130
+ const machines = (options.machines ?? [{
131
+ id: `${fileNameToUri(fileName)}#0`,
132
+ fileName,
133
+ machineIndex: 0
134
+ }]).map((machine) => ({
135
+ ...machine,
136
+ fileName: path$1.resolve(machine.fileName)
137
+ }));
138
+ await Promise.all([...new Set(machines.map((machine) => machine.fileName))].map((target) => fsPromises.access(target)));
114
139
  await assertEditorHostAvailable(options.editorUrl);
115
140
  const rootUri = fileNameToUri(fileName);
116
- const rootDir = path$1.dirname(fileName);
141
+ const rootDir = path$1.resolve(options.rootDir ?? path$1.dirname(fileName));
117
142
  let activeClient;
118
143
  let session;
119
144
  const server = http.createServer((request, response) => {
@@ -138,6 +163,7 @@ async function openEditor(options) {
138
163
  session = new RemoteEditorSession({
139
164
  ...options,
140
165
  fileName,
166
+ machines,
141
167
  rootUri,
142
168
  rootDir
143
169
  }, (message) => activeClient?.send(message));
@@ -151,7 +177,7 @@ async function openEditor(options) {
151
177
  const port = typeof address === "object" && address ? address.port : options.port;
152
178
  const editorPageUrl = `http://${options.host}:${port}`;
153
179
  console.log(`Stately visual editor: ${editorPageUrl}`);
154
- console.log(`Editing ${fileName}`);
180
+ console.log(`Editing ${machines.length} machine${machines.length === 1 ? "" : "s"} in ${rootDir}`);
155
181
  if (options.shouldOpen) openBrowser(editorPageUrl);
156
182
  const shutdown = () => {
157
183
  session?.dispose();
@@ -306,6 +332,22 @@ function watchWorkspace(rootDir, listener) {
306
332
  timers.clear();
307
333
  } };
308
334
  }
335
+ function createSharedWorkspaceWatcher(rootDir) {
336
+ const listeners = /* @__PURE__ */ new Set();
337
+ const watcher = watchWorkspace(rootDir, (uri) => {
338
+ for (const listener of listeners) listener(uri);
339
+ });
340
+ return {
341
+ subscribe(listener) {
342
+ listeners.add(listener);
343
+ return { dispose: () => listeners.delete(listener) };
344
+ },
345
+ dispose() {
346
+ listeners.clear();
347
+ watcher.dispose();
348
+ }
349
+ };
350
+ }
309
351
  function getCliWebviewContent(options) {
310
352
  const baseUrl = normalizedBaseUrl(options.editorUrl);
311
353
  const url = new URL(`${baseUrl}/embed`);
@@ -851,6 +893,7 @@ function normalizeProjectConfig(config) {
851
893
  projectId: config.projectId ?? "",
852
894
  studioUrl: config.studioUrl ?? "https://stately.ai",
853
895
  defaultXStateVersion: Math.max(5, config.defaultXStateVersion ?? 5),
896
+ ...config.sourceUpdateStrategy === "preserve" || config.sourceUpdateStrategy === "rewrite" ? { sourceUpdateStrategy: config.sourceUpdateStrategy } : {},
854
897
  include: [...config.include ?? firstSource?.include ?? []],
855
898
  exclude: config.exclude == null ? [...firstSource?.exclude ?? DEFAULT_SOURCE_EXCLUDES] : [...config.exclude],
856
899
  ...config.newMachinesDir ? { newMachinesDir: config.newMachinesDir } : {}
@@ -1730,7 +1773,7 @@ function inferDiscoveredMachineName(options) {
1730
1773
  return `${path.basename(options.relativePath, path.extname(options.relativePath))}#${options.machineIndex + 1}`;
1731
1774
  }
1732
1775
  function discoverMachinesInSource(options) {
1733
- if (!looksLikeXStateMachineSource(options.source)) return [];
1776
+ if (!hasXStateDependency(options.source)) return [];
1734
1777
  return findStatelyPragmaAttachments(options.source, options.filePath).map((attachment, machineIndex) => ({
1735
1778
  filePath: options.filePath,
1736
1779
  ...attachment.pragma?.id ? { id: attachment.pragma.id } : {},
@@ -1746,6 +1789,9 @@ function discoverMachinesInSource(options) {
1746
1789
  relativePath: options.relativePath
1747
1790
  }));
1748
1791
  }
1792
+ function hasXStateDependency(source) {
1793
+ return /\bfrom\s*['"]xstate['"]/.test(source) || /\bimport\s*['"]xstate['"]/.test(source) || /\brequire\s*\(\s*['"]xstate['"]\s*\)/.test(source);
1794
+ }
1749
1795
  async function discoverMachinesInFiles(files) {
1750
1796
  const machines = [];
1751
1797
  for (const file of files) {
@@ -1781,11 +1827,6 @@ async function scanProjectSources(options) {
1781
1827
  const cwd = path.resolve(options.cwd ?? process.cwd());
1782
1828
  return suggestStatelySourceConfigs([...new Set((await scanProjectMachines({ cwd })).map((machine) => machine.relativePath))]);
1783
1829
  }
1784
- function looksLikeXStateMachineSource(source) {
1785
- const hasXStateImport = /from\s+['"]xstate(?:\/[^'"]+)?['"]/.test(source) || /require\(\s*['"]xstate(?:\/[^'"]+)?['"]\s*\)/.test(source);
1786
- const hasMachineFactory = /\bcreateMachine\s*\(/.test(source) || /\.createMachine\s*\(/.test(source);
1787
- return hasXStateImport && hasMachineFactory;
1788
- }
1789
1830
  async function classifyPushCandidates(files) {
1790
1831
  const machineFiles = [];
1791
1832
  const nonMachineFiles = [];
@@ -2324,11 +2365,11 @@ var PushCommand = class PushCommand extends Command {
2324
2365
  };
2325
2366
  var OpenCommand = class OpenCommand extends Command {
2326
2367
  static enableJsonFlag = false;
2327
- static summary = "Open a local file in the Stately visual editor.";
2328
- static description = "Starts a local browser-backed editor session. Saved file changes update the visual editor, and saved visual edits are written back to the source file.";
2368
+ static summary = "Open local machines in the Stately visual editor.";
2369
+ static description = "Starts a local browser-backed workspace. Pass a file to open all its machines, or omit it to discover machines in the current worktree.";
2329
2370
  static args = { file: Args.string({
2330
- required: true,
2331
- description: "Local source file to edit visually."
2371
+ required: false,
2372
+ description: "Local source file to edit visually. Defaults to the current worktree."
2332
2373
  }) };
2333
2374
  static flags = {
2334
2375
  help: Flags.help({ char: "h" }),
@@ -2350,22 +2391,61 @@ var OpenCommand = class OpenCommand extends Command {
2350
2391
  debug: Flags.boolean({
2351
2392
  description: "Log editor protocol messages",
2352
2393
  default: false
2394
+ }),
2395
+ "source-update": Flags.string({
2396
+ description: "Source update strategy: preserve keeps formatting; rewrite regenerates the machine",
2397
+ options: ["preserve", "rewrite"]
2353
2398
  })
2354
2399
  };
2355
2400
  async run() {
2356
2401
  const { args, flags } = await this.parse(OpenCommand);
2357
2402
  const apiKey = (await resolveApiKey()).apiKey;
2403
+ const cwd = process.cwd();
2404
+ const discovered = args.file ? await discoverMachinesInFile(path.resolve(args.file)) : await scanProjectMachines({ cwd });
2405
+ if (discovered.length === 0) throw new Error(args.file ? `No machines found in ${path.resolve(args.file)}.` : `No machines found in ${cwd}.`);
2406
+ const rootDir = args.file ? path.dirname(path.resolve(args.file)) : cwd;
2407
+ const machines = discovered.map((machine) => ({
2408
+ id: `${pathToFileURL(machine.filePath).toString()}#${machine.machineIndex}`,
2409
+ name: machine.name,
2410
+ fileName: machine.filePath,
2411
+ machineIndex: machine.machineIndex
2412
+ }));
2413
+ let configuredSourceUpdateStrategy;
2414
+ try {
2415
+ configuredSourceUpdateStrategy = (await readStatelyProjectConfig({ cwd })).config.sourceUpdateStrategy;
2416
+ } catch (error) {
2417
+ if (error.code !== "ENOENT") throw error;
2418
+ }
2358
2419
  await openEditor({
2359
- fileName: path.resolve(args.file),
2420
+ fileName: machines[0].fileName,
2421
+ machines,
2422
+ rootDir,
2360
2423
  editorUrl: flags["editor-url"] ?? getDefaultEditorUrl(),
2361
2424
  host: flags.host,
2362
2425
  port: flags.port,
2363
2426
  shouldOpen: flags.open,
2427
+ sourceUpdateStrategy: flags["source-update"] ?? configuredSourceUpdateStrategy ?? "preserve",
2364
2428
  apiKey,
2365
2429
  debug: flags.debug
2366
2430
  });
2367
2431
  }
2368
2432
  };
2433
+ async function discoverMachinesInFile(filePath) {
2434
+ const source = await fsPromises.readFile(filePath, "utf8");
2435
+ const discovered = discoverMachinesInSource({
2436
+ filePath,
2437
+ relativePath: path.basename(filePath),
2438
+ source
2439
+ });
2440
+ if (discovered.length > 0) return discovered;
2441
+ return [{
2442
+ filePath,
2443
+ line: 1,
2444
+ machineIndex: 0,
2445
+ name: path.basename(filePath),
2446
+ relativePath: path.basename(filePath)
2447
+ }];
2448
+ }
2369
2449
  var InitCommand = class InitCommand extends Command {
2370
2450
  static enableJsonFlag = false;
2371
2451
  static summary = "Create a local or Studio-connected statelyai.json.";
package/dist/index.d.mts CHANGED
@@ -36,6 +36,7 @@ interface StatelyProjectConfig {
36
36
  projectId: string;
37
37
  studioUrl: string;
38
38
  defaultXStateVersion: number;
39
+ sourceUpdateStrategy?: 'preserve' | 'rewrite';
39
40
  include: string[];
40
41
  exclude?: string[];
41
42
  newMachinesDir?: string;
@@ -303,7 +304,7 @@ declare class OpenCommand extends Command {
303
304
  static summary: string;
304
305
  static description: string;
305
306
  static args: {
306
- file: _oclif_core_interfaces0.Arg<string, Record<string, unknown>>;
307
+ file: _oclif_core_interfaces0.Arg<string | undefined, Record<string, unknown>>;
307
308
  };
308
309
  static flags: {
309
310
  help: _oclif_core_interfaces0.BooleanFlag<void>;
@@ -312,6 +313,7 @@ declare class OpenCommand extends Command {
312
313
  port: _oclif_core_interfaces0.OptionFlag<number, _oclif_core_interfaces0.CustomOptions>;
313
314
  open: _oclif_core_interfaces0.BooleanFlag<boolean>;
314
315
  debug: _oclif_core_interfaces0.BooleanFlag<boolean>;
316
+ 'source-update': _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
315
317
  };
316
318
  run(): Promise<void>;
317
319
  }
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { C as scanProjectMachines, S as run, T as createStatelyProjectConfig, _ as isFileGitDirty, a as discoverMachinesInSource, b as resolveCredentialStatus, c as formatDiscoveredMachines, d as formatProjectStatus, f as formatStoredCredentialSource, g as initProject, h as inferInitProjectName, i as discoverLinkedPullTargets, l as formatMissingNewMachineDirMessage, m as getEnvCredential, n as buildOAuthLoginStart, o as discoverOAuthLogin, p as getEnvApiKey, r as classifyPushCandidates, s as discoverRemoteProjectMachineTargets, t as COMMANDS, u as formatPlanSummary, v as resolveApiKey, w as scanProjectSources, x as resolveProjectStatus, y as resolveConfiguredPullTargets } from "./cli-DaH9lbVq.mjs";
1
+ import { C as scanProjectMachines, S as run, T as createStatelyProjectConfig, _ as isFileGitDirty, a as discoverMachinesInSource, b as resolveCredentialStatus, c as formatDiscoveredMachines, d as formatProjectStatus, f as formatStoredCredentialSource, g as initProject, h as inferInitProjectName, i as discoverLinkedPullTargets, l as formatMissingNewMachineDirMessage, m as getEnvCredential, n as buildOAuthLoginStart, o as discoverOAuthLogin, p as getEnvApiKey, r as classifyPushCandidates, s as discoverRemoteProjectMachineTargets, t as COMMANDS, u as formatPlanSummary, v as resolveApiKey, w as scanProjectSources, x as resolveProjectStatus, y as resolveConfiguredPullTargets } from "./cli-DV60djkN.mjs";
2
2
 
3
3
  export { COMMANDS, buildOAuthLoginStart, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverMachinesInSource, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatDiscoveredMachines, formatMissingNewMachineDirMessage, formatPlanSummary, formatProjectStatus, formatStoredCredentialSource, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, resolveCredentialStatus, resolveProjectStatus, run, scanProjectMachines, scanProjectSources };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statelyai",
3
- "version": "0.9.4",
3
+ "version": "0.10.1",
4
4
  "description": "Command-line tools for Stately",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "@oclif/core": "^4.10.3",
24
24
  "typescript": "^5.9.3",
25
25
  "ws": "^8.21.0",
26
- "@statelyai/sdk": "0.29.1"
26
+ "@statelyai/sdk": "0.31.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/ws": "^8.18.1",