statelyai 0.13.0 → 0.14.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
@@ -32,6 +32,8 @@ npm install --global statelyai
32
32
  | Set up local discovery | `statelyai init --local --scan` | Local `statelyai.json` |
33
33
  | Set up project sync | `statelyai init --scan` | Remote project and local `statelyai.json` |
34
34
  | Inspect project machines | `statelyai status` | Nothing |
35
+ | Connect to a Studio project | `statelyai link --project <id>` | Project association in `statelyai.json` |
36
+ | Associate an existing machine | `statelyai link <file> --machine <id>` | Selected source machine ID comment |
35
37
  | Preview project uploads | `statelyai push --dry-run` | Nothing |
36
38
  | Upload local machines | `statelyai push [file]` | Remote machines and local `@statelyai` IDs |
37
39
  | Download project machines | `statelyai pull` | Linked local files and new files under `newMachinesDir` |
@@ -40,7 +42,7 @@ npm install --global statelyai
40
42
 
41
43
  ## Quick start
42
44
 
43
- <!-- project initialization and sync flow from InitCommand, PushCommand, PullCommand, and projectConfig.ts -->
45
+ <!-- project initialization and sync flow from InitCommand, LinkCommand, PushCommand, PullCommand, and projectConfig.ts -->
44
46
 
45
47
  For hosted Stately, authenticate once:
46
48
 
@@ -48,10 +50,11 @@ For hosted Stately, authenticate once:
48
50
  statelyai login
49
51
  ```
50
52
 
51
- Initialize the current repository and scan for XState files:
53
+ Initialize local discovery, then choose the Studio project:
52
54
 
53
55
  ```bash
54
- statelyai init --scan
56
+ statelyai init --local --scan
57
+ statelyai link --project <project-id-or-url>
55
58
  ```
56
59
 
57
60
  `--scan` suggests `include` globs and asks before saving them. Without
@@ -66,8 +69,8 @@ statelyai init --local --scan
66
69
  statelyai status
67
70
  ```
68
71
 
69
- Run `statelyai init` later to connect the local config to a Studio project
70
- without losing its discovery globs.
72
+ Run `statelyai link` later to choose a Studio project without losing discovery
73
+ globs. Plain `init` still supports the existing connected setup for compatibility.
71
74
 
72
75
  To list machines without creating any config:
73
76
 
@@ -114,6 +117,67 @@ migrated to the shared machine key on the next save.
114
117
  New states keep their generated coordinates when clear, or move beside
115
118
  existing states when they would overlap restored layout.
116
119
 
120
+ ## Local setup and Studio linking
121
+
122
+ <!-- association workflow from src/link.ts and LinkCommand in src/cli.ts -->
123
+
124
+ Local configuration, Studio association, and data sync are separate steps:
125
+
126
+ ```bash
127
+ statelyai init --local --scan
128
+ statelyai login
129
+ statelyai link --project <project-id-or-url>
130
+ statelyai push --dry-run
131
+ statelyai push
132
+ ```
133
+
134
+ `link` without flags offers a project picker in an interactive terminal. In
135
+ scripts, pass `--project` or explicitly create a project:
136
+
137
+ ```bash
138
+ statelyai link --create --name "Checkout" --visibility Private
139
+ ```
140
+
141
+ Link an existing remote machine to local source without uploading or downloading
142
+ its definition:
143
+
144
+ ```bash
145
+ statelyai link ./test.ts --machine <machine-id-or-url>
146
+ ```
147
+
148
+ The remote machine must belong to the connected project. For a file containing
149
+ multiple machines, select one with `--machine-index 0` (zero-based). This changes
150
+ only the selected machine's `@statelyai id` comment. It does not compare or
151
+ reconcile the two definitions. Review `push --dry-run` and choose `push` or `pull`
152
+ when ready to sync; `linked` describes association, not equality.
153
+
154
+ Use `--force` to replace an existing project or machine association. Changing the
155
+ project retains existing source machine IDs. `link` preserves discovery globs and
156
+ local writeback settings; it does not scan or include new files automatically.
157
+ Plain `init` retains its existing Studio-connected behavior for compatibility.
158
+ The executable remains `statelyai`.
159
+
160
+ ```mermaid
161
+ stateDiagram-v2
162
+ [*] --> Unconfigured
163
+ Unconfigured --> Local: init --local
164
+ Unconfigured --> Connected: init (compatibility)
165
+ Unconfigured --> Connected: link
166
+ Local --> Connected: link
167
+ state Connected {
168
+ [*] --> LocalOnly
169
+ LocalOnly --> Linked: link file --machine (association only)
170
+ LocalOnly --> Linked: push (create remote + upload + ID)
171
+ Linked --> Linked: push (upload)
172
+ Linked --> Linked: pull (download)
173
+ }
174
+ ```
175
+
176
+ Authentication is independent of this state machine. `open`, `scan`, and
177
+ `push --dry-run` do not establish machine links. `--scan` proposes source globs;
178
+ a new config without it has an empty `include` list. Project-wide `pull` creates
179
+ remote-only files only when `newMachinesDir` is configured.
180
+
117
181
  ## Authentication
118
182
 
119
183
  <!-- authentication flags, environment resolution, and credential storage from src/cli.ts and src/credentials.ts -->
@@ -252,6 +316,20 @@ Use `--json` for automation or `--auth` to show only credential resolution.
252
316
 
253
317
  Flags: `--config <path>`, `--base-url <url>`, `--json`, `--auth`.
254
318
 
319
+ ### `link`
320
+
321
+ Connect to Studio without moving machine definitions:
322
+
323
+ ```bash
324
+ statelyai link --project <project-id-or-url>
325
+ statelyai link --create --name "Checkout"
326
+ statelyai link ./test.ts --machine <machine-id-or-url>
327
+ ```
328
+
329
+ Flags: `--project <id|url>` or `--create`, `--name`, `--visibility`,
330
+ `--machine <id|url>`, `--machine-index <zero-based-index>`, `--config`,
331
+ `--base-url`, `--force`. See [Local setup and Studio linking](#local-setup-and-studio-linking).
332
+
255
333
  ### `init`
256
334
 
257
335
  Create a local config or reuse a Studio project and write `statelyai.json`.
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as run } from "./cli-b1m4txJ7.mjs";
2
+ import { C as run } from "./cli-a0Z1igmj.mjs";
3
3
 
4
4
  //#region src/bin.ts
5
5
  run();
@@ -7,22 +7,23 @@ import { z, z as z$1 } from "zod";
7
7
  import { execFile, execFileSync, spawn } from "node:child_process";
8
8
  import { promisify } from "node:util";
9
9
  import * as crypto from "node:crypto";
10
+ import { randomUUID } from "node:crypto";
11
+ import { StudioApiError, buildAuthorizeUrl, createPkcePair, createStatelyClient, discoverAuthorizationServer, discoverProtectedResource, exchangeCodeForToken, refreshToken, registerOAuthClient } from "@statelyai/sdk";
12
+ import * as ts$1 from "typescript";
13
+ import ts from "typescript";
10
14
  import fs, { promises, statSync, watch } from "node:fs";
11
15
  import { createInterface } from "node:readline/promises";
12
16
  import { Writable } from "node:stream";
13
17
  import { fileURLToPath, pathToFileURL } from "node:url";
14
18
  import * as http from "node:http";
15
- import * as ts$1 from "typescript";
16
- import ts from "typescript";
17
19
  import * as fs$1 from "fs";
18
20
  import * as path$2 from "path";
19
21
  import { fileURLToPath as fileURLToPath$1, pathToFileURL as pathToFileURL$1 } from "url";
20
22
  import { fromStudioMachine, toStudioMachine } from "@statelyai/sdk/graph";
21
23
  import { EDITOR_PROTOCOL_VERSION } from "@statelyai/sdk/editor-host-protocol";
22
- import { createStatelyClient, createStatelyClient as createStatelyClient$1$1 } from "@statelyai/sdk/studio";
24
+ import { createStatelyClient as createStatelyClient$1, createStatelyClient as createStatelyClient$1$1 } from "@statelyai/sdk/studio";
23
25
  import { WebSocket, WebSocketServer } from "ws";
24
26
  import os from "node:os";
25
- import { StudioApiError, buildAuthorizeUrl, createPkcePair, createStatelyClient as createStatelyClient$1, discoverAuthorizationServer, discoverProtectedResource, exchangeCodeForToken, refreshToken, registerOAuthClient } from "@statelyai/sdk";
26
27
 
27
28
  //#region \0rolldown/runtime.js
28
29
  var __commonJSMin$1 = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -897,6 +898,236 @@ var VerifyCommand = class VerifyCommand extends Command {
897
898
  }
898
899
  };
899
900
 
901
+ //#endregion
902
+ //#region ../editor-sync/dist/statelyPragma-D46lczJR.mjs
903
+ function findStatelyPragmaAttachments(sourceText, fileName = "machine.ts") {
904
+ const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$4(fileName));
905
+ const attachments = [];
906
+ const visit = (node) => {
907
+ if (ts.isCallExpression(node) && isCreateMachineExpression$3(node.expression)) {
908
+ const attachNode = findAttachNode(node);
909
+ if (attachNode) attachments.push({
910
+ machineStart: getMachineExpressionStart(node.expression, sourceFile),
911
+ attachStart: attachNode.getStart(sourceFile),
912
+ pragma: findAttachedStatelyPragma(sourceText, attachNode)
913
+ });
914
+ }
915
+ ts.forEachChild(node, visit);
916
+ };
917
+ visit(sourceFile);
918
+ return attachments;
919
+ }
920
+ function getStatelyPragma(sourceText, fileName = "machine.ts", machineIndex = 0) {
921
+ return findStatelyPragmaAttachments(sourceText, fileName)[machineIndex]?.pragma;
922
+ }
923
+ function upsertStatelyPragma(sourceText, id, options = {}) {
924
+ const attachment = findStatelyPragmaAttachments(sourceText, options.fileName ?? "machine.ts")[options.machineIndex ?? 0];
925
+ if (!attachment) return sourceText;
926
+ const lineEnding = options.lineEnding ?? detectLineEnding(sourceText);
927
+ const canonicalComment = `// @statelyai id=${id}`;
928
+ if (attachment.pragma) return sourceText.slice(0, attachment.pragma.start) + canonicalComment + sourceText.slice(attachment.pragma.end);
929
+ const insertText = `${getIndentationAtOffset(sourceText, attachment.attachStart)}${canonicalComment}${lineEnding}`;
930
+ return sourceText.slice(0, attachment.attachStart) + insertText + sourceText.slice(attachment.attachStart);
931
+ }
932
+ function findAttachedStatelyPragma(sourceText, node) {
933
+ const comments = ts.getLeadingCommentRanges(sourceText, node.getFullStart()) ?? [];
934
+ for (let index = comments.length - 1; index >= 0; index -= 1) {
935
+ const comment = comments[index];
936
+ const parsed = parseStatelyPragma(sourceText, comment.pos, comment.end);
937
+ if (parsed) return parsed;
938
+ }
939
+ }
940
+ function parseStatelyPragma(sourceText, start, end) {
941
+ const match = normalizeCommentContent(sourceText.slice(start, end)).match(/^@statelyai(?:\s+(.*))?$/s);
942
+ if (!match) return;
943
+ const fields = parseFields(match[1] ?? "");
944
+ return {
945
+ tag: "@statelyai",
946
+ id: fields.id,
947
+ fields,
948
+ start,
949
+ end
950
+ };
951
+ }
952
+ function normalizeCommentContent(rawComment) {
953
+ if (rawComment.startsWith("//")) return rawComment.slice(2).trim();
954
+ if (rawComment.startsWith("/*")) return rawComment.slice(2, rawComment.endsWith("*/") ? -2 : void 0).split(/\r?\n/).map((line) => line.replace(/^\s*\*\s?/, "")).join(" ").trim();
955
+ return rawComment.trim();
956
+ }
957
+ function parseFields(input) {
958
+ const fields = {};
959
+ for (const token of input.split(/\s+/)) {
960
+ if (!token) continue;
961
+ const equalsIndex = token.indexOf("=");
962
+ if (equalsIndex <= 0) continue;
963
+ const key = token.slice(0, equalsIndex);
964
+ const value = token.slice(equalsIndex + 1);
965
+ if (!key || !value) continue;
966
+ fields[key] = value;
967
+ }
968
+ return fields;
969
+ }
970
+ function findAttachNode(node) {
971
+ let current = node;
972
+ while (current?.parent) {
973
+ if (ts.isVariableStatement(current.parent) || ts.isExpressionStatement(current.parent) || ts.isExportAssignment(current.parent)) return current.parent;
974
+ current = current.parent;
975
+ }
976
+ return current;
977
+ }
978
+ function isCreateMachineExpression$3(expression) {
979
+ const isMachineFactoryName = (name) => name === "createMachine" || name === "createMachineFromConfig";
980
+ return ts.isIdentifier(expression) && isMachineFactoryName(expression.text) || ts.isPropertyAccessExpression(expression) && isMachineFactoryName(expression.name.text);
981
+ }
982
+ function getMachineExpressionStart(expression, sourceFile) {
983
+ if (ts.isPropertyAccessExpression(expression)) return expression.name.getStart(sourceFile);
984
+ return expression.getStart(sourceFile);
985
+ }
986
+ function getIndentationAtOffset(sourceText, offset) {
987
+ const lineStart = sourceText.lastIndexOf("\n", Math.max(0, offset - 1)) + 1;
988
+ return sourceText.slice(lineStart, offset).match(/^[ \t]*/)?.[0] ?? "";
989
+ }
990
+ function detectLineEnding(sourceText) {
991
+ return sourceText.includes("\r\n") ? "\r\n" : "\n";
992
+ }
993
+ function getScriptKind$4(fileName) {
994
+ if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
995
+ if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
996
+ if (fileName.endsWith(".js")) return ts.ScriptKind.JS;
997
+ return ts.ScriptKind.TS;
998
+ }
999
+
1000
+ //#endregion
1001
+ //#region src/link.ts
1002
+ function resourceId(locator, kind) {
1003
+ let id = locator.trim();
1004
+ if (/^https?:\/\//.test(id)) {
1005
+ const url = new URL(id);
1006
+ id = kind === "machine" ? url.searchParams.get("machineId") ?? url.pathname.match(/\/machines?\/([^/]+)/)?.[1] ?? "" : url.pathname.match(/\/(?:projects?|editor)\/([^/]+)/)?.[1] ?? "";
1007
+ id = decodeURIComponent(id);
1008
+ }
1009
+ if (!/^[\w-]+$/.test(id)) throw new Error(`Pass a ${kind} ID or a URL containing a ${kind} ID.`);
1010
+ return id;
1011
+ }
1012
+ /** Association only: no definitions are uploaded, downloaded, or executed. */
1013
+ async function linkProject(options) {
1014
+ if (options.projectId && options.create) throw new Error("Choose --project or --create, not both.");
1015
+ if (Boolean(options.file) !== Boolean(options.machineId)) throw new Error("Machine association requires a file and --machine <id|url>.");
1016
+ if (options.create && options.machineId) throw new Error("Link an existing machine to its existing project with --project; --create creates an empty project.");
1017
+ if (options.visibility && !options.create) throw new Error("--visibility requires --create.");
1018
+ if (options.name && !options.create) throw new Error("--name requires --create.");
1019
+ const cwd = path.resolve(options.cwd ?? process.cwd());
1020
+ const configPath = path.resolve(cwd, options.configPath ?? STATELY_CONFIG_FILE);
1021
+ const existing = await readStatelyProjectConfig({
1022
+ cwd,
1023
+ configPath,
1024
+ rewriteIfNeeded: false
1025
+ }).catch((error) => {
1026
+ if (error.code === "ENOENT") return void 0;
1027
+ throw error;
1028
+ });
1029
+ if (existing?.rewriteStatus.reason === "multiple-legacy-sources") throw new Error("Update the legacy sources configuration manually before linking; no files were changed.");
1030
+ const studioUrl = options.baseUrl ?? existing?.config.studioUrl ?? "https://stately.ai";
1031
+ const requestedId = options.projectId ? resourceId(options.projectId, "project") : void 0;
1032
+ const sameDeployment = !existing?.config.studioUrl || resolveApiBaseUrl(studioUrl) === resolveApiBaseUrl(existing.config.studioUrl);
1033
+ if (existing?.config.projectId && (!sameDeployment || options.create || requestedId && requestedId !== existing.config.projectId) && !options.force) throw new Error(`Already linked to project ${existing.config.projectId}. Use --force to change the project association; existing machine IDs are retained.`);
1034
+ const machineId = options.machineId ? resourceId(options.machineId, "machine") : void 0;
1035
+ const filePath = options.file ? path.resolve(cwd, options.file) : void 0;
1036
+ const source = filePath ? await fsPromises.readFile(filePath, "utf8") : void 0;
1037
+ const machineIndex = options.machineIndex ?? 0;
1038
+ if (filePath && source !== void 0) {
1039
+ if (!/\.[cm]?[jt]sx?$/.test(filePath)) throw new Error("Machine association supports JavaScript and TypeScript source files.");
1040
+ const attachments = findStatelyPragmaAttachments(source, filePath);
1041
+ if (attachments.length > 1 && options.machineIndex === void 0) throw new Error("This file contains multiple machines. Select one with --machine-index (zero-based).");
1042
+ const attachment = attachments[machineIndex];
1043
+ if (!attachment || !Number.isInteger(machineIndex) || machineIndex < 0) throw new Error("No machine at the selected index.");
1044
+ if (attachment.pragma?.id && attachment.pragma.id !== machineId && !options.force) throw new Error(`Machine already linked to ${attachment.pragma.id}. Use --force to replace its association.`);
1045
+ } else if (options.machineIndex !== void 0) throw new Error("--machine-index requires a file and --machine.");
1046
+ const client = options.client ?? createStatelyClient({
1047
+ apiKey: options.apiKey,
1048
+ baseUrl: resolveApiBaseUrl(studioUrl)
1049
+ });
1050
+ let project;
1051
+ if (options.create) project = await client.projects.create({
1052
+ name: options.name ?? path.basename(cwd),
1053
+ visibility: options.visibility ?? "Private"
1054
+ });
1055
+ else {
1056
+ const projectId = requestedId ?? existing?.config.projectId;
1057
+ if (!projectId) throw new Error("Choose --project <id|url> or --create to connect this workspace.");
1058
+ project = await client.projects.get(projectId);
1059
+ }
1060
+ if (machineId) {
1061
+ if (!project.machines.some((machine) => machine.machineId === machineId)) throw new Error(`Machine ${machineId} is not in project ${project.projectId}. No files were changed.`);
1062
+ await client.machines.get(machineId);
1063
+ }
1064
+ if (filePath && await fsPromises.readFile(filePath, "utf8") !== source) throw new Error("Source changed while linking. Retry; no files were changed.");
1065
+ const config = {
1066
+ ...existing?.config ?? createStatelyProjectConfig({}),
1067
+ projectId: project.projectId,
1068
+ studioUrl
1069
+ };
1070
+ const writes = [];
1071
+ const stage = async (destination, write) => {
1072
+ const suffix = randomUUID();
1073
+ const entry = {
1074
+ path: destination,
1075
+ staged: `${destination}.${suffix}.tmp`,
1076
+ backup: `${destination}.${suffix}.bak`,
1077
+ existed: false,
1078
+ committed: false
1079
+ };
1080
+ writes.push(entry);
1081
+ try {
1082
+ await fsPromises.copyFile(destination, entry.backup);
1083
+ entry.existed = true;
1084
+ } catch (error) {
1085
+ if (error.code !== "ENOENT") throw error;
1086
+ }
1087
+ await write(entry.staged);
1088
+ if (entry.existed) await fsPromises.chmod(entry.staged, (await fsPromises.stat(destination)).mode);
1089
+ };
1090
+ let rollbackFailed = false;
1091
+ try {
1092
+ await stage(configPath, (temporary) => writeStatelyProjectConfig(temporary, config));
1093
+ if (filePath && source !== void 0 && machineId) {
1094
+ const updated = upsertStatelyPragma(source, machineId, {
1095
+ fileName: filePath,
1096
+ machineIndex
1097
+ });
1098
+ if (updated !== source) await stage(filePath, (temporary) => fsPromises.writeFile(temporary, updated, "utf8"));
1099
+ }
1100
+ if (filePath && await fsPromises.readFile(filePath, "utf8") !== source) throw new Error("Source changed while linking. Retry; no files were changed.");
1101
+ for (const entry of writes) {
1102
+ await fsPromises.rename(entry.staged, entry.path);
1103
+ entry.committed = true;
1104
+ }
1105
+ } catch (error) {
1106
+ const rollbackErrors = [];
1107
+ for (const entry of [...writes].reverse()) {
1108
+ if (!entry.committed) continue;
1109
+ try {
1110
+ if (entry.existed) await fsPromises.rename(entry.backup, entry.path);
1111
+ else await fsPromises.unlink(entry.path);
1112
+ } catch (rollbackError) {
1113
+ rollbackFailed = true;
1114
+ rollbackErrors.push(rollbackError);
1115
+ }
1116
+ }
1117
+ if (rollbackFailed) throw new AggregateError([error, ...rollbackErrors], "Link failed and could not restore every file. Original files are retained in .bak files beside their destinations.");
1118
+ throw error;
1119
+ } finally {
1120
+ await Promise.all(writes.flatMap((entry) => rollbackFailed ? [entry.staged] : [entry.staged, entry.backup]).map((temporary) => fsPromises.rm(temporary, { force: true })));
1121
+ }
1122
+ return {
1123
+ project,
1124
+ config,
1125
+ configPath,
1126
+ machineId,
1127
+ filePath
1128
+ };
1129
+ }
1130
+
900
1131
  //#endregion
901
1132
  //#region ../editor-sync/dist/fs-Dw5c-Wnp.mjs
902
1133
  function comparePosition(left, right) {
@@ -7418,7 +7649,7 @@ function buildMeta(meta, color, showStatelyMeta = true) {
7418
7649
  if (meta) {
7419
7650
  for (const [k, v] of Object.entries(meta)) if (v !== void 0 && v !== null && (showStatelyMeta || k === FN_PROJECTION_META || !k.startsWith("@statelyai"))) result[k] = v;
7420
7651
  }
7421
- if (color && showStatelyMeta) result["@statelyai.color"] = color;
7652
+ if (color !== void 0 && showStatelyMeta) result["@statelyai.color"] = color;
7422
7653
  return Object.keys(result).length > 0 ? result : void 0;
7423
7654
  }
7424
7655
  /** Format event param — supports { type: string } (new) and plain string (legacy) */
@@ -10355,7 +10586,7 @@ function graphToXStateTSResult(inputGraph, options = {}) {
10355
10586
  if (xstateVersion === 6) stripStateSchemas(machineConfig);
10356
10587
  let machineExpr;
10357
10588
  if (hasSetup) {
10358
- const setupObj = buildSetupObject(schemas, impls, {
10589
+ const setupObj = buildSetupObject(graph, schemas, impls, {
10359
10590
  includeTypeSyntax: supportsTypeSyntax,
10360
10591
  xstateVersion,
10361
10592
  xstateV6SchemaFormat
@@ -11155,7 +11386,7 @@ function getActorSourceImports(graph) {
11155
11386
  }
11156
11387
  return imports;
11157
11388
  }
11158
- function buildSetupObject(schemas, impls, options) {
11389
+ function buildSetupObject(graph, schemas, impls, options) {
11159
11390
  const setup = {};
11160
11391
  if (options.xstateVersion === 6) {
11161
11392
  const schemasBlock = buildV6SchemasBlock(schemas, options);
@@ -11165,7 +11396,10 @@ function buildSetupObject(schemas, impls, options) {
11165
11396
  if (types) setup.types = types;
11166
11397
  }
11167
11398
  if (impls?.actions.length) setup.actions = buildActionsBlock(impls.actions, options);
11168
- if (impls?.guards.length) setup.guards = buildGuardsBlock(impls.guards, options);
11399
+ if (impls?.guards.length) setup.guards = buildGuardsBlock(impls.guards, {
11400
+ ...options,
11401
+ argsTypes: options.xstateVersion === 6 && options.includeTypeSyntax ? new Map(impls.guards.map((guard) => [guard.name, getV6GuardArgsType(graph, guard.name)])) : void 0
11402
+ });
11169
11403
  if (impls?.actors.length) setup.actors = buildActorsBlock(impls.actors, {
11170
11404
  ...options,
11171
11405
  xstateVersion: options.xstateVersion
@@ -11266,18 +11500,68 @@ function buildActionsBlock(actions, options) {
11266
11500
  } else block[implementation.name] = raw(options.xstateVersion === 6 ? `function () {\n // TODO: implement ${implementation.name}\n}` : `function ({ context, event }) {\n // TODO: implement ${implementation.name}\n}`);
11267
11501
  return block;
11268
11502
  }
11503
+ /**
11504
+ * XState v6 types `setup({ guards })` entries as `(...params: any[]) => boolean`
11505
+ * and injects nothing: a named guard receives exactly what its caller passes.
11506
+ * Generated transition functions call `guards[name]({ context, event }, params)`
11507
+ * (as does `createMachineFromConfig`), so that is the argument type to declare.
11508
+ */
11509
+ function getV6GuardArgsType(graph, guardName) {
11510
+ const schemas = graph.data.schemas;
11511
+ const rootContextType = contextSchemaToTSType(schemas?.context);
11512
+ const contexts = graph.edges.filter((edge) => !edge.data.temp && edge.data.guard?.type === guardName).map((edge) => {
11513
+ const types = [];
11514
+ let node = graph.nodes.find((candidate) => candidate.id === edge.sourceId);
11515
+ while (node) {
11516
+ const type = contextSchemaToTSType(node.data.schemas?.context);
11517
+ if (type) types.unshift(type);
11518
+ node = graph.nodes.find((candidate) => candidate.id === node?.parentId);
11519
+ }
11520
+ if (rootContextType) types.unshift(rootContextType);
11521
+ return types.length ? types.join(" & ") : void 0;
11522
+ });
11523
+ const contextType = contexts.some(Boolean) ? [...new Set(contexts.map((type) => type ?? "any"))].join(" | ") : rootContextType;
11524
+ const eventSchemas = {
11525
+ ...schemas?.events,
11526
+ ...schemas?.internalEvents
11527
+ };
11528
+ const declaredEventType = eventsSchemaToTSType(eventSchemas);
11529
+ if (!contextType && !declaredEventType) return void 0;
11530
+ const eventType = declaredEventType ? `${declaredEventType} | Exclude<import('xstate').ReservedMachineEvent, { type: ${Object.keys(eventSchemas).map((type) => serializeJS(type)).join(" | ")} }>` : "any";
11531
+ return `{ context: ${contextType ?? "any"}; event: ${eventType} }`;
11532
+ }
11533
+ /**
11534
+ * True when an authored guard expression destructures the generated
11535
+ * `{ context, event }` argument without annotating it, so contextual typing
11536
+ * can supply the types without contradicting an authored signature.
11537
+ */
11538
+ function usesGeneratedGuardArgs(code) {
11539
+ const expression = parseSingleExpression(code)?.expression;
11540
+ if (!expression || !ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression)) return false;
11541
+ const [first] = expression.parameters;
11542
+ if (!first || first.type || !ts.isObjectBindingPattern(first.name)) return false;
11543
+ return first.name.elements.every((element) => {
11544
+ const property = element.propertyName ?? element.name;
11545
+ return !element.dotDotDotToken && ts.isIdentifier(property) && (property.text === "context" || property.text === "event");
11546
+ });
11547
+ }
11269
11548
  function buildGuardsBlock(guards, options) {
11270
11549
  const block = {};
11271
- for (const guard of guards) if (guard.code?.body) {
11272
- const inlineExpression = getInlineExpression$1(guard.code.body);
11273
- if (inlineExpression) {
11274
- block[guard.name] = raw(inlineExpression, guard.code);
11275
- continue;
11276
- }
11277
- const params = hasSchemaProperties(guard.paramsSchema) ? options.includeTypeSyntax ? `, params: ${jsonSchemaToTSType(guard.paramsSchema)}` : ", params" : "";
11278
- const body = dedent(guard.code.body);
11279
- block[guard.name] = rawWithSourceSlice(`function ({ context, event }${params ? params : ""}) {\n `, body, "\n}", guard.code);
11280
- } else block[guard.name] = raw(`function ({ context, event }) {\n // TODO: implement ${guard.name}\n return false;\n}`);
11550
+ for (const guard of guards) {
11551
+ const argsType = options.argsTypes?.get(guard.name);
11552
+ const argsAnnotation = argsType ? `: ${argsType}` : "";
11553
+ const paramsType = hasSchemaProperties(guard.paramsSchema) ? jsonSchemaToTSType(guard.paramsSchema) : void 0;
11554
+ if (guard.code?.body) {
11555
+ const inlineExpression = getInlineExpression$1(guard.code.body);
11556
+ if (inlineExpression) {
11557
+ block[guard.name] = argsType && usesGeneratedGuardArgs(inlineExpression) ? rawWithSourceSlice("(", inlineExpression, `) satisfies (args: ${argsType}, params: ${paramsType ?? "any"}) => boolean`, guard.code) : raw(inlineExpression, guard.code);
11558
+ continue;
11559
+ }
11560
+ const params = paramsType ? options.includeTypeSyntax ? `, params: ${paramsType}` : ", params" : "";
11561
+ const body = dedent(guard.code.body);
11562
+ block[guard.name] = rawWithSourceSlice(`function ({ context, event }${argsAnnotation}${params}) {\n `, body, "\n}", guard.code);
11563
+ } else block[guard.name] = raw(`function ({ context, event }${argsAnnotation}) {\n // TODO: implement ${guard.name}\n return false;\n}`);
11564
+ }
11281
11565
  return block;
11282
11566
  }
11283
11567
  function buildActorsBlock(actors, options) {
@@ -13528,7 +13812,7 @@ function modify(text, path, value, options) {
13528
13812
  //#region ../editor-sync/dist/adapters-9XXzVfbF.mjs
13529
13813
  const sourceFileUris = /* @__PURE__ */ new WeakMap();
13530
13814
  function collectXStateSourceLocations(sourceText, document, options = {}) {
13531
- const sourceFile = ts$1.createSourceFile(document.fileName, sourceText, ts$1.ScriptTarget.Latest, true, getScriptKind$2$1(document.fileName));
13815
+ const sourceFile = ts$1.createSourceFile(document.fileName, sourceText, ts$1.ScriptTarget.Latest, true, getScriptKind$2(document.fileName));
13532
13816
  const uri = document.uri?.toString() ?? document.fileName;
13533
13817
  sourceFileUris.set(sourceFile, uri);
13534
13818
  const bindings = collectBindings(sourceFile, uri, document.fileName, options);
@@ -13590,7 +13874,7 @@ function machineFactoryPackage(sourceFile, document, machineIndex = 0, resolver)
13590
13874
  let target = modules.get(snapshot.uri);
13591
13875
  if (!target) {
13592
13876
  target = {
13593
- file: ts$1.createSourceFile(snapshot.fileName, snapshot.text, ts$1.ScriptTarget.Latest, true, getScriptKind$2$1(snapshot.fileName)),
13877
+ file: ts$1.createSourceFile(snapshot.fileName, snapshot.text, ts$1.ScriptTarget.Latest, true, getScriptKind$2(snapshot.fileName)),
13594
13878
  document: snapshot
13595
13879
  };
13596
13880
  modules.set(snapshot.uri, target);
@@ -13663,7 +13947,7 @@ function machineFactoryPackage(sourceFile, document, machineIndex = 0, resolver)
13663
13947
  let index = 0;
13664
13948
  const visit = (node) => {
13665
13949
  if (factory) return;
13666
- if (ts$1.isCallExpression(node) && isCreateMachineExpression$1$1(node.expression)) {
13950
+ if (ts$1.isCallExpression(node) && isCreateMachineExpression$1(node.expression)) {
13667
13951
  if (index++ === machineIndex) factory = node;
13668
13952
  return;
13669
13953
  }
@@ -13744,7 +14028,7 @@ function parseRangePart(value) {
13744
14028
  }
13745
14029
  function collectImportedXStateTransitionSources(sourceText, document, options = {}) {
13746
14030
  if (!options.resolver) return [];
13747
- const sourceFile = ts$1.createSourceFile(document.fileName, sourceText, ts$1.ScriptTarget.Latest, true, getScriptKind$2$1(document.fileName));
14031
+ const sourceFile = ts$1.createSourceFile(document.fileName, sourceText, ts$1.ScriptTarget.Latest, true, getScriptKind$2(document.fileName));
13748
14032
  const uri = document.uri?.toString() ?? document.fileName;
13749
14033
  sourceFileUris.set(sourceFile, uri);
13750
14034
  const bindings = collectBindings(sourceFile, uri, document.fileName, options);
@@ -13835,7 +14119,7 @@ function resolveExportedStaticValue(document, exportName, resolver, visited) {
13835
14119
  if (visited.has(visitKey)) return null;
13836
14120
  visited.add(visitKey);
13837
14121
  try {
13838
- const sourceFile = ts$1.createSourceFile(document.fileName, document.text, ts$1.ScriptTarget.Latest, true, getScriptKind$2$1(document.fileName));
14122
+ const sourceFile = ts$1.createSourceFile(document.fileName, document.text, ts$1.ScriptTarget.Latest, true, getScriptKind$2(document.fileName));
13839
14123
  sourceFileUris.set(sourceFile, document.uri);
13840
14124
  const localBindings = collectStaticValueBindings(sourceFile);
13841
14125
  const localBinding = localBindings.get(exportName);
@@ -13973,7 +14257,7 @@ function addImportedBindings(bindings, sourceFile, uri, fileName, options, cache
13973
14257
  uri
13974
14258
  }, statement.moduleSpecifier.text);
13975
14259
  if (!importedDocument) continue;
13976
- const importedSourceFile = ts$1.createSourceFile(importedDocument.fileName, importedDocument.text, ts$1.ScriptTarget.Latest, true, getScriptKind$2$1(importedDocument.fileName));
14260
+ const importedSourceFile = ts$1.createSourceFile(importedDocument.fileName, importedDocument.text, ts$1.ScriptTarget.Latest, true, getScriptKind$2(importedDocument.fileName));
13977
14261
  sourceFileUris.set(importedSourceFile, importedDocument.uri);
13978
14262
  const importedBindings = collectBindings(importedSourceFile, importedDocument.uri, importedDocument.fileName, options, cache);
13979
14263
  for (const element of namedBindings.elements) {
@@ -14044,7 +14328,7 @@ function findMachineConfigObjectLiteral$1(sourceFile, bindings, machineIndex = 0
14044
14328
  let currentMachineIndex = 0;
14045
14329
  const visit = (node) => {
14046
14330
  if (found) return;
14047
- if (ts$1.isCallExpression(node) && isCreateMachineExpression$1$1(node.expression)) {
14331
+ if (ts$1.isCallExpression(node) && isCreateMachineExpression$1(node.expression)) {
14048
14332
  const isSelectedMachine = currentMachineIndex === machineIndex;
14049
14333
  currentMachineIndex += 1;
14050
14334
  if (!isSelectedMachine) return;
@@ -14312,7 +14596,7 @@ function getEnumMemberName(name) {
14312
14596
  if (ts$1.isIdentifier(name) || ts$1.isStringLiteral(name) || ts$1.isNumericLiteral(name)) return name.text;
14313
14597
  return null;
14314
14598
  }
14315
- function isCreateMachineExpression$1$1(expression) {
14599
+ function isCreateMachineExpression$1(expression) {
14316
14600
  const isMachineFactoryName = (name) => name === "createMachine" || name === "createMachineFromConfig";
14317
14601
  return ts$1.isIdentifier(expression) && isMachineFactoryName(expression.text) || ts$1.isPropertyAccessExpression(expression) && isMachineFactoryName(expression.name.text);
14318
14602
  }
@@ -14348,7 +14632,7 @@ function toSourceRange$1(sourceFile, node) {
14348
14632
  endChar: end.character
14349
14633
  };
14350
14634
  }
14351
- function getScriptKind$2$1(fileName) {
14635
+ function getScriptKind$2(fileName) {
14352
14636
  if (fileName.endsWith(".tsx")) return ts$1.ScriptKind.TSX;
14353
14637
  if (fileName.endsWith(".jsx")) return ts$1.ScriptKind.JSX;
14354
14638
  if (fileName.endsWith(".js")) return ts$1.ScriptKind.JS;
@@ -14440,7 +14724,7 @@ function selectMachineInModule(source, machineIndex, factory, expression) {
14440
14724
  let factoryAlias = "__stately_sync_factory";
14441
14725
  while (source.includes(factoryAlias)) factoryAlias += "_";
14442
14726
  const visit = (node) => {
14443
- if (ts$1.isCallExpression(node) && isCreateMachineExpression$1$1(node.expression)) {
14727
+ if (ts$1.isCallExpression(node) && isCreateMachineExpression$1(node.expression)) {
14444
14728
  if (index++ === machineIndex) replacements.push({
14445
14729
  start: node.getStart(file),
14446
14730
  end: node.getEnd(),
@@ -21030,7 +21314,7 @@ function sourceLocationUris(sourceLocations) {
21030
21314
 
21031
21315
  //#endregion
21032
21316
  //#region src/version.ts
21033
- const CLI_VERSION = "0.13.0";
21317
+ const CLI_VERSION = "0.14.1";
21034
21318
 
21035
21319
  //#endregion
21036
21320
  //#region src/cliHost.ts
@@ -21482,7 +21766,7 @@ async function openSourceDocument(fileName) {
21482
21766
  };
21483
21767
  }
21484
21768
  function collectRelativeImportSpecifiers(text, fileName) {
21485
- const sourceFile = ts$1.createSourceFile(fileName, text, ts$1.ScriptTarget.Latest, true, getScriptKind$2(fileName));
21769
+ const sourceFile = ts$1.createSourceFile(fileName, text, ts$1.ScriptTarget.Latest, true, getScriptKind$1(fileName));
21486
21770
  const specifiers = [];
21487
21771
  for (const statement of sourceFile.statements) {
21488
21772
  const moduleSpecifier = ts$1.isImportDeclaration(statement) || ts$1.isExportDeclaration(statement) ? statement.moduleSpecifier : void 0;
@@ -21509,7 +21793,7 @@ function resolveRelativeImportCandidates(fromFileName, specifier) {
21509
21793
  ...importExtensions.map((extension) => path$1.join(basePath, `index${extension}`))
21510
21794
  ];
21511
21795
  }
21512
- function getScriptKind$2(fileName) {
21796
+ function getScriptKind$1(fileName) {
21513
21797
  if (fileName.endsWith(".tsx")) return ts$1.ScriptKind.TSX;
21514
21798
  if (fileName.endsWith(".jsx")) return ts$1.ScriptKind.JSX;
21515
21799
  if (/\.(?:c|m)?js$/.test(fileName)) return ts$1.ScriptKind.JS;
@@ -22031,105 +22315,6 @@ function describeCredentialBackend(backend, location) {
22031
22315
  }
22032
22316
  }
22033
22317
 
22034
- //#endregion
22035
- //#region ../editor-sync/dist/statelyPragma-D46lczJR.mjs
22036
- function findStatelyPragmaAttachments(sourceText, fileName = "machine.ts") {
22037
- const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(fileName));
22038
- const attachments = [];
22039
- const visit = (node) => {
22040
- if (ts.isCallExpression(node) && isCreateMachineExpression$1(node.expression)) {
22041
- const attachNode = findAttachNode(node);
22042
- if (attachNode) attachments.push({
22043
- machineStart: getMachineExpressionStart(node.expression, sourceFile),
22044
- attachStart: attachNode.getStart(sourceFile),
22045
- pragma: findAttachedStatelyPragma(sourceText, attachNode)
22046
- });
22047
- }
22048
- ts.forEachChild(node, visit);
22049
- };
22050
- visit(sourceFile);
22051
- return attachments;
22052
- }
22053
- function getStatelyPragma(sourceText, fileName = "machine.ts", machineIndex = 0) {
22054
- return findStatelyPragmaAttachments(sourceText, fileName)[machineIndex]?.pragma;
22055
- }
22056
- function upsertStatelyPragma(sourceText, id, options = {}) {
22057
- const attachment = findStatelyPragmaAttachments(sourceText, options.fileName ?? "machine.ts")[options.machineIndex ?? 0];
22058
- if (!attachment) return sourceText;
22059
- const lineEnding = options.lineEnding ?? detectLineEnding(sourceText);
22060
- const canonicalComment = `// @statelyai id=${id}`;
22061
- if (attachment.pragma) return sourceText.slice(0, attachment.pragma.start) + canonicalComment + sourceText.slice(attachment.pragma.end);
22062
- const insertText = `${getIndentationAtOffset(sourceText, attachment.attachStart)}${canonicalComment}${lineEnding}`;
22063
- return sourceText.slice(0, attachment.attachStart) + insertText + sourceText.slice(attachment.attachStart);
22064
- }
22065
- function findAttachedStatelyPragma(sourceText, node) {
22066
- const comments = ts.getLeadingCommentRanges(sourceText, node.getFullStart()) ?? [];
22067
- for (let index = comments.length - 1; index >= 0; index -= 1) {
22068
- const comment = comments[index];
22069
- const parsed = parseStatelyPragma(sourceText, comment.pos, comment.end);
22070
- if (parsed) return parsed;
22071
- }
22072
- }
22073
- function parseStatelyPragma(sourceText, start, end) {
22074
- const match = normalizeCommentContent(sourceText.slice(start, end)).match(/^@statelyai(?:\s+(.*))?$/s);
22075
- if (!match) return;
22076
- const fields = parseFields(match[1] ?? "");
22077
- return {
22078
- tag: "@statelyai",
22079
- id: fields.id,
22080
- fields,
22081
- start,
22082
- end
22083
- };
22084
- }
22085
- function normalizeCommentContent(rawComment) {
22086
- if (rawComment.startsWith("//")) return rawComment.slice(2).trim();
22087
- if (rawComment.startsWith("/*")) return rawComment.slice(2, rawComment.endsWith("*/") ? -2 : void 0).split(/\r?\n/).map((line) => line.replace(/^\s*\*\s?/, "")).join(" ").trim();
22088
- return rawComment.trim();
22089
- }
22090
- function parseFields(input) {
22091
- const fields = {};
22092
- for (const token of input.split(/\s+/)) {
22093
- if (!token) continue;
22094
- const equalsIndex = token.indexOf("=");
22095
- if (equalsIndex <= 0) continue;
22096
- const key = token.slice(0, equalsIndex);
22097
- const value = token.slice(equalsIndex + 1);
22098
- if (!key || !value) continue;
22099
- fields[key] = value;
22100
- }
22101
- return fields;
22102
- }
22103
- function findAttachNode(node) {
22104
- let current = node;
22105
- while (current?.parent) {
22106
- if (ts.isVariableStatement(current.parent) || ts.isExpressionStatement(current.parent) || ts.isExportAssignment(current.parent)) return current.parent;
22107
- current = current.parent;
22108
- }
22109
- return current;
22110
- }
22111
- function isCreateMachineExpression$1(expression) {
22112
- const isMachineFactoryName = (name) => name === "createMachine" || name === "createMachineFromConfig";
22113
- return ts.isIdentifier(expression) && isMachineFactoryName(expression.text) || ts.isPropertyAccessExpression(expression) && isMachineFactoryName(expression.name.text);
22114
- }
22115
- function getMachineExpressionStart(expression, sourceFile) {
22116
- if (ts.isPropertyAccessExpression(expression)) return expression.name.getStart(sourceFile);
22117
- return expression.getStart(sourceFile);
22118
- }
22119
- function getIndentationAtOffset(sourceText, offset) {
22120
- const lineStart = sourceText.lastIndexOf("\n", Math.max(0, offset - 1)) + 1;
22121
- return sourceText.slice(lineStart, offset).match(/^[ \t]*/)?.[0] ?? "";
22122
- }
22123
- function detectLineEnding(sourceText) {
22124
- return sourceText.includes("\r\n") ? "\r\n" : "\n";
22125
- }
22126
- function getScriptKind$1(fileName) {
22127
- if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
22128
- if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
22129
- if (fileName.endsWith(".js")) return ts.ScriptKind.JS;
22130
- return ts.ScriptKind.TS;
22131
- }
22132
-
22133
22318
  //#endregion
22134
22319
  //#region ../../node_modules/.pnpm/@statelyai+graph@2.1.0_@statelyai+layout@0.4.3_cytoscape@3.34.0_d3-force@3.0.0_d3-hiera_2afdfa88fae2fa10712f99651d0f58d6/node_modules/@statelyai/graph/dist/index.mjs
22135
22320
  const LAYOUT_KEYS = {
@@ -22582,7 +22767,7 @@ function fromXStateConfig(config) {
22582
22767
  };
22583
22768
  }
22584
22769
  async function pushLocalMachineLinks(options) {
22585
- const client = options.client ?? createStatelyClient({
22770
+ const client = options.client ?? createStatelyClient$1({
22586
22771
  apiKey: options.apiKey,
22587
22772
  baseUrl: options.baseUrl,
22588
22773
  fetch: options.fetch
@@ -22679,7 +22864,7 @@ async function resolveLocalFile(locator, options, machineIndex = 0) {
22679
22864
  let config = localExtraction.machines[0]?.config;
22680
22865
  const extractionIssue = localExtraction.error;
22681
22866
  if (!config) {
22682
- const extracted = await (options.client ?? createStatelyClient({
22867
+ const extracted = await (options.client ?? createStatelyClient$1({
22683
22868
  apiKey: options.apiKey,
22684
22869
  baseUrl: options.baseUrl,
22685
22870
  fetch: options.fetch
@@ -22751,7 +22936,7 @@ function inferXStateTargetLanguage(targetPath) {
22751
22936
  return /\.(?:c|m)?jsx?$/i.test(targetPath) ? "javascript" : "typescript";
22752
22937
  }
22753
22938
  async function resolveRemoteMachine(machineId, baseUrl, kind, locator, options) {
22754
- const digraph = (await (options.client ?? createStatelyClient({
22939
+ const digraph = (await (options.client ?? createStatelyClient$1({
22755
22940
  apiKey: options.apiKey,
22756
22941
  baseUrl,
22757
22942
  fetch: options.fetch
@@ -23523,7 +23708,7 @@ async function initProject(options) {
23523
23708
  reusedExistingConfig: true
23524
23709
  };
23525
23710
  if (existingConfig.config.projectId) {
23526
- const client = options.client ?? createStatelyClient$1({
23711
+ const client = options.client ?? createStatelyClient({
23527
23712
  apiKey: options.apiKey,
23528
23713
  baseUrl: apiBaseUrl
23529
23714
  });
@@ -23544,7 +23729,7 @@ async function initProject(options) {
23544
23729
  reusedExistingConfig: false
23545
23730
  };
23546
23731
  }
23547
- const client = options.client ?? createStatelyClient$1({
23732
+ const client = options.client ?? createStatelyClient({
23548
23733
  apiKey: options.apiKey,
23549
23734
  baseUrl: apiBaseUrl
23550
23735
  });
@@ -23670,9 +23855,9 @@ async function resolveConfiguredProject(options) {
23670
23855
  configPath: options.configPath,
23671
23856
  rewriteIfNeeded: options.rewriteIfNeeded ?? true
23672
23857
  });
23673
- if (!config.projectId) throw new Error("This is a local-only project. Run `statelyai init` to connect it to Stately Studio.");
23858
+ if (!config.projectId) throw new Error("This is a local-only project. Run `statelyai link` to connect it to Stately Studio.");
23674
23859
  return {
23675
- client: options.client ?? createStatelyClient$1({
23860
+ client: options.client ?? createStatelyClient({
23676
23861
  apiKey: options.apiKey,
23677
23862
  baseUrl: resolveApiBaseUrl(options.baseUrl ?? config.studioUrl)
23678
23863
  }),
@@ -23779,7 +23964,7 @@ async function resolveProjectStatus(options = {}) {
23779
23964
  };
23780
23965
  }
23781
23966
  const apiKey = options.apiKey ?? (options.client ? void 0 : (await resolveApiKey()).apiKey);
23782
- const client = options.client ?? createStatelyClient$1({
23967
+ const client = options.client ?? createStatelyClient({
23783
23968
  apiKey,
23784
23969
  baseUrl: resolveApiBaseUrl(studioUrl)
23785
23970
  });
@@ -24293,6 +24478,93 @@ async function discoverMachinesInFile(filePath) {
24293
24478
  relativePath: path.basename(filePath)
24294
24479
  }];
24295
24480
  }
24481
+ var LinkCommand = class LinkCommand extends Command {
24482
+ static summary = "Connect a workspace or source machine to Stately Studio without syncing definitions.";
24483
+ static args = { file: Args.string({ description: "Source file to associate with --machine" }) };
24484
+ static flags = {
24485
+ help: Flags.help({ char: "h" }),
24486
+ project: Flags.string({
24487
+ description: "Existing Studio project ID or URL",
24488
+ exclusive: ["create"]
24489
+ }),
24490
+ create: Flags.boolean({
24491
+ description: "Create a new Studio project",
24492
+ default: false
24493
+ }),
24494
+ name: Flags.string({
24495
+ description: "Name for --create",
24496
+ dependsOn: ["create"]
24497
+ }),
24498
+ visibility: Flags.string({
24499
+ description: "Visibility for --create",
24500
+ options: [
24501
+ "Private",
24502
+ "Public",
24503
+ "Unlisted"
24504
+ ]
24505
+ }),
24506
+ machine: Flags.string({ description: "Existing remote machine ID or URL; requires a source file" }),
24507
+ "machine-index": Flags.integer({
24508
+ description: "Zero-based machine index in a multi-machine source file",
24509
+ min: 0
24510
+ }),
24511
+ "base-url": Flags.string({ description: "Studio deployment origin" }),
24512
+ config: Flags.string({ description: "Path to statelyai.json" }),
24513
+ force: Flags.boolean({
24514
+ description: "Replace an existing project or machine association",
24515
+ default: false
24516
+ })
24517
+ };
24518
+ async run() {
24519
+ const { args, flags } = await this.parse(LinkCommand);
24520
+ let projectId = flags.project;
24521
+ let create = flags.create;
24522
+ const credential = await resolveApiKey();
24523
+ const config = await readStatelyProjectConfig({ configPath: flags.config }).catch((error) => {
24524
+ if (error.code === "ENOENT") return void 0;
24525
+ throw error;
24526
+ });
24527
+ const baseUrl = flags["base-url"] ?? config?.config.studioUrl ?? getResolvedStudioUrl();
24528
+ if (!projectId && !create && !config?.config.projectId && process.stdin.isTTY && process.stdout.isTTY) {
24529
+ const projects = await createStatelyClient({
24530
+ apiKey: credential.apiKey,
24531
+ baseUrl: resolveApiBaseUrl(baseUrl)
24532
+ }).projects.list();
24533
+ projects.forEach((project, index) => this.log(`${index + 1}. ${project.name} (${project.projectId})`));
24534
+ this.log("0. Create a new Studio project");
24535
+ const rl = createInterface({
24536
+ input: process.stdin,
24537
+ output: process.stdout
24538
+ });
24539
+ try {
24540
+ const answer = (await rl.question("Choose a project number: ")).trim();
24541
+ if (!/^\d+$/.test(answer)) this.error("Choose a project number, or pass --project <id> or --create.");
24542
+ const choice = Number(answer);
24543
+ if (choice === 0) create = true;
24544
+ else if (projects[choice - 1]) projectId = projects[choice - 1].projectId;
24545
+ else this.error("No project at that number.");
24546
+ } finally {
24547
+ rl.close();
24548
+ }
24549
+ }
24550
+ const result = await linkProject({
24551
+ projectId,
24552
+ create,
24553
+ apiKey: credential.apiKey,
24554
+ baseUrl,
24555
+ configPath: flags.config,
24556
+ name: flags.name,
24557
+ visibility: flags.visibility,
24558
+ force: flags.force,
24559
+ file: args.file,
24560
+ machineId: flags.machine,
24561
+ machineIndex: flags["machine-index"]
24562
+ });
24563
+ this.log(`Connected to Studio project ${result.project.name} (${result.project.projectId}).`);
24564
+ if (result.machineId) this.log(`Associated ${args.file} with machine ${result.machineId}.`);
24565
+ this.log("No machine definitions uploaded or downloaded. Use push or pull to sync.");
24566
+ }
24567
+ };
24296
24568
  var InitCommand = class InitCommand extends Command {
24297
24569
  static enableJsonFlag = false;
24298
24570
  static summary = "Create a local or Studio-connected statelyai.json.";
@@ -24343,6 +24615,7 @@ var InitCommand = class InitCommand extends Command {
24343
24615
  if (resolvedApiKey.source === "missing" && error instanceof StudioApiError && (error.status === 401 || error.status === 403)) this.error("Not logged in. Run `npx statelyai login` and retry. To initialize without an account, run `npx statelyai init --local` (add `--force` to replace an existing config).");
24344
24616
  throw error;
24345
24617
  }
24618
+ this.log("For separate local setup and Studio linking, use `statelyai init --local --scan`, then `statelyai link`.");
24346
24619
  const initMessage = result.project ? (() => {
24347
24620
  const projectEditorUrl = buildProjectEditorUrl(result.config.studioUrl, result.project.projectId);
24348
24621
  return result.reusedExistingConfig ? `Using existing statelyai.json at ${result.configPath} for project ${result.project.projectId}.\nEditor: ${projectEditorUrl}` : `Initialized project ${result.project.projectId} and wrote ${result.configPath}.\nEditor: ${projectEditorUrl}`;
@@ -24510,6 +24783,7 @@ const COMMANDS = {
24510
24783
  scan: ScanCommand,
24511
24784
  status: StatusCommand,
24512
24785
  init: InitCommand,
24786
+ link: LinkCommand,
24513
24787
  login: LoginCommand,
24514
24788
  logout: LogoutCommand,
24515
24789
  whoami: WhoamiCommand,
@@ -24539,4 +24813,4 @@ function isDirectExecution() {
24539
24813
  if (isDirectExecution()) run$1();
24540
24814
 
24541
24815
  //#endregion
24542
- export { briefReport as A, run$1 as C, verifyProject as D, resolveVerificationRequest as E, pendingReport as M, readGraphFile as N, formatVerificationJUnit as O, verifyReport as P, resolveProjectStatus as S, scanProjectSources as T, isFileGitDirty as _, discoverMachinesInSource as a, resolveCredentialStatus as b, formatDiscoveredMachines as c, formatProjectStatus as d, formatStoredCredentialSource as f, initProject as g, inferInitProjectName as h, discoverLinkedPullTargets as i, changesReport as j, createStatelyProjectConfig as k, formatMissingNewMachineDirMessage as l, getEnvCredential as m, buildOAuthLoginStart as n, discoverOAuthLogin as o, getEnvApiKey as p, classifyPushCandidates as r, discoverRemoteProjectMachineTargets as s, COMMANDS as t, formatPlanSummary as u, resolveApiKey as v, scanProjectMachines as w, resolveOpenRootDir as x, resolveConfiguredPullTargets as y };
24816
+ export { formatVerificationJUnit as A, run$1 as C, resourceId as D, linkProject as E, readGraphFile as F, verifyReport as I, briefReport as M, changesReport as N, resolveVerificationRequest as O, pendingReport as P, resolveProjectStatus as S, scanProjectSources as T, isFileGitDirty as _, discoverMachinesInSource as a, resolveCredentialStatus as b, formatDiscoveredMachines as c, formatProjectStatus as d, formatStoredCredentialSource as f, initProject as g, inferInitProjectName as h, discoverLinkedPullTargets as i, createStatelyProjectConfig as j, verifyProject as k, formatMissingNewMachineDirMessage as l, getEnvCredential as m, buildOAuthLoginStart as n, discoverOAuthLogin as o, getEnvApiKey as p, classifyPushCandidates as r, discoverRemoteProjectMachineTargets as s, COMMANDS as t, formatPlanSummary as u, resolveApiKey as v, scanProjectMachines as w, resolveOpenRootDir as x, resolveConfiguredPullTargets as y };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Command } from "@oclif/core";
2
2
  import { XGraph } from "@statelyai/mcp/loop";
3
3
  import { z } from "zod";
4
+ import { ConnectedRepo, CreateProjectInput, LegacyProjectVisibility, OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata, ProjectData, ProjectMachineSummary, ProjectVisibility, StudioClient } from "@statelyai/sdk";
4
5
  import { StatelyGraph } from "@statelyai/sdk/graph";
5
- import { ConnectedRepo, CreateProjectInput, OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata, ProjectData, ProjectMachineSummary, StudioClient } from "@statelyai/sdk";
6
6
  import * as _oclif_core_interfaces0 from "@oclif/core/interfaces";
7
7
 
8
8
  //#region src/verifyCommand.d.ts
@@ -27,6 +27,43 @@ declare class VerifyCommand extends Command {
27
27
  run(): Promise<void>;
28
28
  }
29
29
  //#endregion
30
+ //#region src/link.d.ts
31
+ interface LinkProjectOptions {
32
+ cwd?: string;
33
+ configPath?: string;
34
+ apiKey?: string;
35
+ baseUrl?: string;
36
+ client?: StudioClient;
37
+ projectId?: string;
38
+ create?: boolean;
39
+ name?: string;
40
+ visibility?: ProjectVisibility | LegacyProjectVisibility;
41
+ force?: boolean;
42
+ file?: string;
43
+ machineId?: string;
44
+ machineIndex?: number;
45
+ }
46
+ declare function resourceId(locator: string, kind: 'project' | 'machine'): string;
47
+ /** Association only: no definitions are uploaded, downloaded, or executed. */
48
+ declare function linkProject(options: LinkProjectOptions): Promise<{
49
+ project: ProjectData;
50
+ config: {
51
+ projectId: string;
52
+ studioUrl: string;
53
+ $schema: string;
54
+ version: string;
55
+ defaultXStateVersion: number;
56
+ sourceUpdateStrategy?: "preserve" | "rewrite";
57
+ include: string[];
58
+ exclude?: string[];
59
+ newMachinesDir?: string;
60
+ storage?: "local" | "hosted";
61
+ };
62
+ configPath: string;
63
+ machineId: string | undefined;
64
+ filePath: string | undefined;
65
+ }>;
66
+ //#endregion
30
67
  //#region src/loopCommands.d.ts
31
68
  /**
32
69
  * Offline loop commands over XGraph JSON files and the repository's
@@ -609,6 +646,25 @@ declare class OpenCommand extends Command {
609
646
  run(): Promise<void>;
610
647
  }
611
648
  declare function resolveOpenRootDir(cwd: string, fileArg?: string): string;
649
+ declare class LinkCommand extends Command {
650
+ static summary: string;
651
+ static args: {
652
+ file: _oclif_core_interfaces0.Arg<string | undefined, Record<string, unknown>>;
653
+ };
654
+ static flags: {
655
+ help: _oclif_core_interfaces0.BooleanFlag<void>;
656
+ project: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
657
+ create: _oclif_core_interfaces0.BooleanFlag<boolean>;
658
+ name: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
659
+ visibility: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
660
+ machine: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
661
+ 'machine-index': _oclif_core_interfaces0.OptionFlag<number | undefined, _oclif_core_interfaces0.CustomOptions>;
662
+ 'base-url': _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
663
+ config: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
664
+ force: _oclif_core_interfaces0.BooleanFlag<boolean>;
665
+ };
666
+ run(): Promise<void>;
667
+ }
612
668
  declare class InitCommand extends Command {
613
669
  static enableJsonFlag: boolean;
614
670
  static summary: string;
@@ -689,6 +745,7 @@ declare const COMMANDS: {
689
745
  scan: typeof ScanCommand;
690
746
  status: typeof StatusCommand;
691
747
  init: typeof InitCommand;
748
+ link: typeof LinkCommand;
692
749
  login: typeof LoginCommand;
693
750
  logout: typeof LogoutCommand;
694
751
  whoami: typeof WhoamiCommand;
@@ -1616,4 +1673,4 @@ declare function verifyProject(options?: VerifyProjectOptions): Promise<{
1616
1673
  /** One case per recorded path/probe. Unattributed divergences remain separate cases. */
1617
1674
  declare function formatVerificationJUnit(report: VerificationRunReport): string;
1618
1675
  //#endregion
1619
- export { ApiKeyResolution, COMMANDS, CredentialStatus, DiscoveredMachine, InitProjectOptions, InitProjectResult, LinkedPullTarget, OAuthLoginDiscovery, ProjectMachineStatus, ProjectStatusMachine, ProjectStatusResult, PushCandidateClassification, RemoteProjectMachineTarget, ResolveApiKeyOptions, ResolvedConfiguredPullTargets, ScanProjectSourcesOptions, type VerificationExecutor, type VerificationRequest, type VerificationRunReport, type VerifyProjectOptions, briefReport, buildOAuthLoginStart, changesReport, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverMachinesInSource, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatDiscoveredMachines, formatMissingNewMachineDirMessage, formatPlanSummary, formatProjectStatus, formatStoredCredentialSource, formatVerificationJUnit, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, pendingReport, readGraphFile, resolveApiKey, resolveConfiguredPullTargets, resolveCredentialStatus, resolveOpenRootDir, resolveProjectStatus, resolveVerificationRequest, run, scanProjectMachines, scanProjectSources, verifyProject, verifyReport };
1676
+ export { ApiKeyResolution, COMMANDS, CredentialStatus, DiscoveredMachine, InitProjectOptions, InitProjectResult, type LinkProjectOptions, LinkedPullTarget, OAuthLoginDiscovery, ProjectMachineStatus, ProjectStatusMachine, ProjectStatusResult, PushCandidateClassification, RemoteProjectMachineTarget, ResolveApiKeyOptions, ResolvedConfiguredPullTargets, ScanProjectSourcesOptions, type VerificationExecutor, type VerificationRequest, type VerificationRunReport, type VerifyProjectOptions, briefReport, buildOAuthLoginStart, changesReport, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverMachinesInSource, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatDiscoveredMachines, formatMissingNewMachineDirMessage, formatPlanSummary, formatProjectStatus, formatStoredCredentialSource, formatVerificationJUnit, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, linkProject, pendingReport, readGraphFile, resolveApiKey, resolveConfiguredPullTargets, resolveCredentialStatus, resolveOpenRootDir, resolveProjectStatus, resolveVerificationRequest, resourceId, run, scanProjectMachines, scanProjectSources, verifyProject, verifyReport };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { A as briefReport, C as run, D as verifyProject, E as resolveVerificationRequest, M as pendingReport, N as readGraphFile, O as formatVerificationJUnit, P as verifyReport, S as resolveProjectStatus, T as scanProjectSources, _ 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, j as changesReport, k as createStatelyProjectConfig, 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 scanProjectMachines, x as resolveOpenRootDir, y as resolveConfiguredPullTargets } from "./cli-b1m4txJ7.mjs";
1
+ import { A as formatVerificationJUnit, C as run, D as resourceId, E as linkProject, F as readGraphFile, I as verifyReport, M as briefReport, N as changesReport, O as resolveVerificationRequest, P as pendingReport, S as resolveProjectStatus, T as scanProjectSources, _ 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, j as createStatelyProjectConfig, k as verifyProject, 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 scanProjectMachines, x as resolveOpenRootDir, y as resolveConfiguredPullTargets } from "./cli-a0Z1igmj.mjs";
2
2
 
3
- export { COMMANDS, briefReport, buildOAuthLoginStart, changesReport, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverMachinesInSource, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatDiscoveredMachines, formatMissingNewMachineDirMessage, formatPlanSummary, formatProjectStatus, formatStoredCredentialSource, formatVerificationJUnit, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, pendingReport, readGraphFile, resolveApiKey, resolveConfiguredPullTargets, resolveCredentialStatus, resolveOpenRootDir, resolveProjectStatus, resolveVerificationRequest, run, scanProjectMachines, scanProjectSources, verifyProject, verifyReport };
3
+ export { COMMANDS, briefReport, buildOAuthLoginStart, changesReport, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverMachinesInSource, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatDiscoveredMachines, formatMissingNewMachineDirMessage, formatPlanSummary, formatProjectStatus, formatStoredCredentialSource, formatVerificationJUnit, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, linkProject, pendingReport, readGraphFile, resolveApiKey, resolveConfiguredPullTargets, resolveCredentialStatus, resolveOpenRootDir, resolveProjectStatus, resolveVerificationRequest, resourceId, run, scanProjectMachines, scanProjectSources, verifyProject, verifyReport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statelyai",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "Command-line tools for Stately",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -24,15 +24,15 @@
24
24
  "typescript": "^5.9.3",
25
25
  "ws": "^8.21.0",
26
26
  "zod": "^4.4.1",
27
- "@statelyai/mcp": "0.17.0",
28
- "@statelyai/sdk": "0.39.0"
27
+ "@statelyai/mcp": "0.18.0",
28
+ "@statelyai/sdk": "0.39.1"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/ws": "^8.18.1",
32
32
  "tsdown": "0.21.0-beta.2",
33
33
  "vitest": "^3.2.4",
34
- "@statelyai/api-contract": "0.6.5",
35
- "@statelyai/editor-sync": "0.2.0"
34
+ "@statelyai/api-contract": "0.7.1",
35
+ "@statelyai/editor-sync": "0.2.2"
36
36
  },
37
37
  "oclif": {
38
38
  "bin": "statelyai",