statelyai 0.4.1 → 0.5.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
@@ -36,7 +36,8 @@ That writes a `statelyai.json` like:
36
36
  "studioUrl": "https://stately.ai",
37
37
  "defaultXStateVersion": 5,
38
38
  "include": ["src/**/*.ts"],
39
- "exclude": ["**/*.test.*", "**/*.spec.*"]
39
+ "exclude": ["**/*.test.*", "**/*.spec.*"],
40
+ "newMachineDir": "src"
40
41
  }
41
42
  ```
42
43
 
@@ -54,13 +55,13 @@ statelyai push --dry-run
54
55
 
55
56
  If a saved `// @statelyai id=...` points to a deleted or inaccessible remote machine, `push` will prompt to relink the file as a new remote machine and replace the local id.
56
57
 
57
- 6. Pull remote changes back into linked local files:
58
+ 6. Pull remote changes back into linked local files and create new local files for remote-only project machines when `newMachineDir` is configured:
58
59
 
59
60
  ```bash
60
61
  statelyai pull
61
62
  ```
62
63
 
63
- `pull` skips locally modified files unless you pass `--force`.
64
+ `pull` skips locally modified linked files unless you pass `--force`. New remote-only machines are written as `<machine-name>.machine.ts` inside `newMachineDir`.
64
65
 
65
66
  Run it without installing it globally:
66
67
 
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { u as run } from "./cli-DkWxDq0T.mjs";
2
+ import { d as run } from "./cli-1Nrsh5Xl.mjs";
3
3
 
4
4
  //#region src/bin.ts
5
5
  run();
@@ -975,7 +975,8 @@ function normalizeProjectConfig(config) {
975
975
  studioUrl: config.studioUrl ?? "https://stately.ai",
976
976
  defaultXStateVersion: Math.max(5, config.defaultXStateVersion ?? 5),
977
977
  include: [...config.include ?? firstSource?.include ?? []],
978
- exclude: config.exclude == null ? [...firstSource?.exclude ?? DEFAULT_SOURCE_EXCLUDES] : [...config.exclude]
978
+ exclude: config.exclude == null ? [...firstSource?.exclude ?? DEFAULT_SOURCE_EXCLUDES] : [...config.exclude],
979
+ ...config.newMachineDir ? { newMachineDir: config.newMachineDir } : {}
979
980
  };
980
981
  }
981
982
  async function readStatelyProjectConfig(options = {}) {
@@ -1060,6 +1061,9 @@ function matchesGlob(relativePath, pattern) {
1060
1061
  function matchesAny(patterns, relativePath) {
1061
1062
  return (patterns ?? []).some((pattern) => matchesGlob(relativePath, pattern));
1062
1063
  }
1064
+ function isTrackedByStatelyProjectConfig(config, relativePath) {
1065
+ return matchesAny(config.include, relativePath) && !matchesAny(config.exclude ?? DEFAULT_SOURCE_EXCLUDES, relativePath);
1066
+ }
1063
1067
  async function discoverCodeSourceFiles(options = {}) {
1064
1068
  const rootDir = path.resolve(options.cwd ?? process.cwd());
1065
1069
  return (await filterGitIgnoredPaths(rootDir, await walkFiles(rootDir))).filter((relativePath) => isCodeSourceFile(relativePath)).filter((relativePath) => !matchesAny(DEFAULT_SOURCE_EXCLUDES, relativePath)).sort((left, right) => left.localeCompare(right));
@@ -1157,6 +1161,7 @@ async function discoverStatelySourceFiles(options = {}) {
1157
1161
  //#region src/cli.ts
1158
1162
  const execFileAsync = promisify(execFile);
1159
1163
  const STATELY_API_KEY_SETTINGS_URL = "https://stately.ai/registry/user/my-settings?tab=API+Key";
1164
+ const DEFAULT_NEW_MACHINE_FILE_SUFFIX = ".machine.ts";
1160
1165
  function loadLocalEnv() {
1161
1166
  if (typeof process.loadEnvFile !== "function") return;
1162
1167
  const cwdEnvPath = path.join(process.cwd(), ".env.local");
@@ -1310,6 +1315,9 @@ function normalizeApiKey(value) {
1310
1315
  function pluralize(count, singular, plural = `${singular}s`) {
1311
1316
  return count === 1 ? singular : plural;
1312
1317
  }
1318
+ function toMachineFileStem(machineName) {
1319
+ return machineName.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "machine";
1320
+ }
1313
1321
  function getMissingApiKeyMessage() {
1314
1322
  return `No API key configured. Use \`statelyai login\`, set \`STATELY_API_KEY\`, or pass \`--api-key\`.\nGet or create an API key at ${STATELY_API_KEY_SETTINGS_URL}`;
1315
1323
  }
@@ -1321,6 +1329,15 @@ function buildMachineEditorUrl(studioUrl, projectId, machineId) {
1321
1329
  url.searchParams.set("machineId", machineId);
1322
1330
  return url.toString();
1323
1331
  }
1332
+ function inferNewMachineDirFromSuggestions(suggestions) {
1333
+ if (suggestions.length !== 1) return;
1334
+ const include = suggestions[0]?.include[0];
1335
+ if (!include) return;
1336
+ const globIndex = include.indexOf("**/");
1337
+ if (globIndex >= 0) return include.slice(0, globIndex).replace(/\/$/, "") || void 0;
1338
+ const directory = path.posix.dirname(include);
1339
+ return directory === "." ? void 0 : directory;
1340
+ }
1324
1341
  function isRelinkableLinkedMachineError(error) {
1325
1342
  return error instanceof StudioApiError && (error.status === 403 || error.status === 404);
1326
1343
  }
@@ -1400,6 +1417,35 @@ async function discoverLinkedPullTargets(files) {
1400
1417
  }
1401
1418
  return linkedTargets;
1402
1419
  }
1420
+ async function discoverRemoteProjectMachineTargets(options) {
1421
+ const newMachineDir = options.config.newMachineDir?.trim();
1422
+ if (!newMachineDir) return [];
1423
+ const targets = [];
1424
+ for (const machine of options.project.machines) {
1425
+ if (options.linkedMachineIds.has(machine.machineId)) continue;
1426
+ const baseDirectory = path.join(options.rootDir, newMachineDir);
1427
+ const stem = toMachineFileStem(machine.name);
1428
+ let candidateFileName = `${stem}${DEFAULT_NEW_MACHINE_FILE_SUFFIX}`;
1429
+ let attempt = 2;
1430
+ for (;;) {
1431
+ const filePath = path.join(baseDirectory, candidateFileName);
1432
+ const relativePath = path.relative(options.rootDir, filePath).replace(/\\/g, "/");
1433
+ if (!isTrackedByStatelyProjectConfig(options.config, relativePath)) throw new Error(`Generated path ${relativePath} for remote machine ${machine.name} is not covered by include/exclude globs.`);
1434
+ if (!await fileExists(filePath)) {
1435
+ targets.push({
1436
+ machineId: machine.machineId,
1437
+ machineName: machine.name,
1438
+ filePath,
1439
+ relativePath
1440
+ });
1441
+ break;
1442
+ }
1443
+ candidateFileName = `${stem}-${attempt}${DEFAULT_NEW_MACHINE_FILE_SUFFIX}`;
1444
+ attempt += 1;
1445
+ }
1446
+ }
1447
+ return targets;
1448
+ }
1403
1449
  async function initProject(options) {
1404
1450
  const cwd = path.resolve(options.cwd ?? process.cwd());
1405
1451
  const studioUrl = getResolvedStudioUrl(options.baseUrl);
@@ -1609,19 +1655,38 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1609
1655
  if (!args.source) {
1610
1656
  if (!apiKey) this.error(getMissingApiKeyMessage());
1611
1657
  this.log("Resolving configured project and linked source files...");
1612
- const { files, config } = await resolveConfiguredProject({
1658
+ const { files, config, configPath } = await resolveConfiguredProject({
1613
1659
  apiKey,
1614
1660
  baseUrl: flags["base-url"],
1615
1661
  configPath: flags.config
1616
1662
  });
1617
1663
  const linkedTargets = await discoverLinkedPullTargets(files);
1618
- if (linkedTargets.length === 0) {
1619
- this.log("No linked local machine files were discovered.");
1664
+ const linkedMachineIds = new Set(linkedTargets.map((linkedTarget) => linkedTarget.machineId));
1665
+ const project = await client.projects.get(config.projectId);
1666
+ const skipped = [];
1667
+ let remoteOnlyTargets = [];
1668
+ try {
1669
+ remoteOnlyTargets = await discoverRemoteProjectMachineTargets({
1670
+ rootDir: path.dirname(configPath),
1671
+ config,
1672
+ project,
1673
+ linkedMachineIds
1674
+ });
1675
+ } catch (error) {
1676
+ skipped.push(error instanceof Error ? error.message : String(error));
1677
+ }
1678
+ if (linkedTargets.length === 0 && remoteOnlyTargets.length === 0) {
1679
+ if (project.machines.length > 0 && !config.newMachineDir) {
1680
+ this.log("No linked local machine files were discovered, and statelyai.json does not set newMachineDir for importing remote-only machines.");
1681
+ return;
1682
+ }
1683
+ this.log("No linked local machine files or remote-only project machines were discovered.");
1620
1684
  return;
1621
1685
  }
1622
- this.log(`Processing ${linkedTargets.length} ${pluralize(linkedTargets.length, "linked file")}...`);
1686
+ if (project.machines.length > linkedTargets.length && !config.newMachineDir) this.log("Remote-only project machines were found, but statelyai.json does not set newMachineDir, so they will be skipped.");
1687
+ this.log(`Processing ${linkedTargets.length} ${pluralize(linkedTargets.length, "linked file")}${remoteOnlyTargets.length > 0 ? ` and ${remoteOnlyTargets.length} ${pluralize(remoteOnlyTargets.length, "new remote machine")}` : ""}...`);
1623
1688
  const pulled = [];
1624
- const skipped = [];
1689
+ const created = [];
1625
1690
  for (const linkedTarget of linkedTargets) {
1626
1691
  const targetPath = linkedTarget.file.filePath;
1627
1692
  if (!flags.force && await isFileGitDirty(targetPath)) {
@@ -1641,7 +1706,23 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1641
1706
  skipped.push(`${linkedTarget.file.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
1642
1707
  }
1643
1708
  }
1709
+ for (const remoteTarget of remoteOnlyTargets) {
1710
+ this.log(`Pulling new remote machine ${remoteTarget.machineName} into ${remoteTarget.relativePath}...`);
1711
+ try {
1712
+ await fsPromises.mkdir(path.dirname(remoteTarget.filePath), { recursive: true });
1713
+ const result = await pullSync({
1714
+ source: remoteTarget.machineId,
1715
+ target: remoteTarget.filePath,
1716
+ apiKey,
1717
+ baseUrl: flags["base-url"] ?? config.studioUrl
1718
+ });
1719
+ created.push(`${remoteTarget.relativePath}: ${result.source.locator}`);
1720
+ } catch (error) {
1721
+ skipped.push(`${remoteTarget.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
1722
+ }
1723
+ }
1644
1724
  if (pulled.length > 0) this.log(`Pulled:\n${pulled.join("\n")}`);
1725
+ if (created.length > 0) this.log(`Created:\n${created.join("\n")}`);
1645
1726
  if (skipped.length > 0) this.log(`Skipped:\n${skipped.join("\n")}`);
1646
1727
  return;
1647
1728
  }
@@ -1889,13 +1970,15 @@ var InitCommand = class InitCommand extends Command {
1889
1970
  this.log(initMessage);
1890
1971
  this.log(`Suggested source globs:\n${suggestions.map((source) => `- ${source.include.join(", ")}`).join("\n")}`);
1891
1972
  if (await promptYesNo("Save these source globs to statelyai.json?", true)) {
1973
+ const inferredNewMachineDir = inferNewMachineDirFromSuggestions(suggestions);
1892
1974
  const nextConfig = {
1893
1975
  ...result.config,
1894
1976
  include: suggestions.flatMap((suggestion) => suggestion.include),
1895
- exclude: [...new Set(suggestions.flatMap((suggestion) => suggestion.exclude ?? []))]
1977
+ exclude: [...new Set(suggestions.flatMap((suggestion) => suggestion.exclude ?? []))],
1978
+ ...inferredNewMachineDir ? { newMachineDir: inferredNewMachineDir } : {}
1896
1979
  };
1897
1980
  await fsPromises.writeFile(result.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
1898
- this.log("Saved scanned source globs to statelyai.json.");
1981
+ this.log(inferredNewMachineDir ? `Saved scanned source globs to statelyai.json.\nNew remote machines will be created under ${inferredNewMachineDir}/ by default.` : "Saved scanned source globs to statelyai.json.");
1899
1982
  } else this.log("Left statelyai.json unchanged. Edit it before running statelyai push.");
1900
1983
  return;
1901
1984
  }
@@ -1991,4 +2074,4 @@ function isDirectExecution() {
1991
2074
  if (isDirectExecution()) run$1();
1992
2075
 
1993
2076
  //#endregion
1994
- export { getEnvApiKey as a, isFileGitDirty as c, scanProjectSources as d, createStatelyProjectConfig as f, formatPlanSummary as i, resolveApiKey as l, classifyPushCandidates as n, inferInitProjectName as o, discoverLinkedPullTargets as r, initProject as s, COMMANDS as t, run$1 as u };
2077
+ export { formatPlanSummary as a, initProject as c, run$1 as d, scanProjectSources as f, discoverRemoteProjectMachineTargets as i, isFileGitDirty as l, classifyPushCandidates as n, getEnvApiKey as o, createStatelyProjectConfig as p, discoverLinkedPullTargets as r, inferInitProjectName as s, COMMANDS as t, resolveApiKey as u };
package/dist/index.d.mts CHANGED
@@ -18,6 +18,7 @@ interface StatelyProjectConfig {
18
18
  defaultXStateVersion: number;
19
19
  include: string[];
20
20
  exclude?: string[];
21
+ newMachineDir?: string;
21
22
  }
22
23
  interface DiscoveredSourceFile {
23
24
  filePath: string;
@@ -55,6 +56,12 @@ interface LinkedPullTarget {
55
56
  file: DiscoveredSourceFile;
56
57
  machineId: string;
57
58
  }
59
+ interface RemoteProjectMachineTarget {
60
+ machineId: string;
61
+ machineName: string;
62
+ filePath: string;
63
+ relativePath: string;
64
+ }
58
65
  interface PushCandidateClassification {
59
66
  machineFiles: DiscoveredSourceFile[];
60
67
  nonMachineFiles: DiscoveredSourceFile[];
@@ -76,6 +83,12 @@ declare function resolveApiKey(explicitApiKey?: string): Promise<ApiKeyResolutio
76
83
  declare function inferInitProjectName(cwd: string, repo?: ConnectedRepo): string;
77
84
  declare function isFileGitDirty(filePath: string): Promise<boolean>;
78
85
  declare function discoverLinkedPullTargets(files: DiscoveredSourceFile[]): Promise<LinkedPullTarget[]>;
86
+ declare function discoverRemoteProjectMachineTargets(options: {
87
+ rootDir: string;
88
+ config: StatelyProjectConfig;
89
+ project: ProjectData;
90
+ linkedMachineIds: Set<string>;
91
+ }): Promise<RemoteProjectMachineTarget[]>;
79
92
  declare function initProject(options: InitProjectOptions): Promise<InitProjectResult>;
80
93
  declare function scanProjectSources(options: ScanProjectSourcesOptions): Promise<Array<Pick<StatelyProjectConfig, 'include' | 'exclude'>>>;
81
94
  declare function classifyPushCandidates(files: DiscoveredSourceFile[]): Promise<PushCandidateClassification>;
@@ -227,4 +240,4 @@ declare const COMMANDS: {
227
240
  };
228
241
  declare function run(argv?: any, entryUrl?: string): Promise<void>;
229
242
  //#endregion
230
- export { ApiKeyResolution, COMMANDS, InitProjectOptions, InitProjectResult, LinkedPullTarget, PushCandidateClassification, ScanProjectSourcesOptions, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, run, scanProjectSources };
243
+ export { ApiKeyResolution, COMMANDS, InitProjectOptions, InitProjectResult, LinkedPullTarget, PushCandidateClassification, RemoteProjectMachineTarget, ScanProjectSourcesOptions, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverRemoteProjectMachineTargets, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, run, scanProjectSources };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { a as getEnvApiKey, c as isFileGitDirty, d as scanProjectSources, f as createStatelyProjectConfig, i as formatPlanSummary, l as resolveApiKey, n as classifyPushCandidates, o as inferInitProjectName, r as discoverLinkedPullTargets, s as initProject, t as COMMANDS, u as run } from "./cli-DkWxDq0T.mjs";
1
+ import { a as formatPlanSummary, c as initProject, d as run, f as scanProjectSources, i as discoverRemoteProjectMachineTargets, l as isFileGitDirty, n as classifyPushCandidates, o as getEnvApiKey, p as createStatelyProjectConfig, r as discoverLinkedPullTargets, s as inferInitProjectName, t as COMMANDS, u as resolveApiKey } from "./cli-1Nrsh5Xl.mjs";
2
2
 
3
- export { COMMANDS, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, run, scanProjectSources };
3
+ export { COMMANDS, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverRemoteProjectMachineTargets, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, run, scanProjectSources };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statelyai",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Command-line tools for Stately",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@oclif/core": "^4.10.3",
24
- "@statelyai/sdk": "0.9.1"
24
+ "@statelyai/sdk": "0.10.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsdown": "0.21.0-beta.2",