statelyai 0.4.1 → 0.5.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
@@ -26,6 +26,8 @@ statelyai init
26
26
  statelyai init --scan
27
27
  ```
28
28
 
29
+ If you have an older `statelyai.json` that still uses the legacy `sources` shape, the CLI will rewrite simple single-source configs automatically when it reads them.
30
+
29
31
  That writes a `statelyai.json` like:
30
32
 
31
33
  ```json
@@ -36,7 +38,8 @@ That writes a `statelyai.json` like:
36
38
  "studioUrl": "https://stately.ai",
37
39
  "defaultXStateVersion": 5,
38
40
  "include": ["src/**/*.ts"],
39
- "exclude": ["**/*.test.*", "**/*.spec.*"]
41
+ "exclude": ["**/*.test.*", "**/*.spec.*"],
42
+ "newMachinesDir": "src"
40
43
  }
41
44
  ```
42
45
 
@@ -54,13 +57,13 @@ statelyai push --dry-run
54
57
 
55
58
  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
59
 
57
- 6. Pull remote changes back into linked local files:
60
+ 6. Pull remote changes back into linked local files and create new local files for remote-only project machines when `newMachinesDir` is configured:
58
61
 
59
62
  ```bash
60
63
  statelyai pull
61
64
  ```
62
65
 
63
- `pull` skips locally modified files unless you pass `--force`.
66
+ `pull` skips locally modified linked files unless you pass `--force`. New remote-only machines are written as `<machine-name>.machine.ts` inside `newMachinesDir`.
64
67
 
65
68
  Run it without installing it globally:
66
69
 
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 { p as run } from "./cli-D4Id6WsS.mjs";
3
3
 
4
4
  //#region src/bin.ts
5
5
  run();
@@ -975,17 +975,45 @@ 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.newMachinesDir ? { newMachinesDir: config.newMachinesDir } : {}
980
+ };
981
+ }
982
+ function getConfigRewriteStatus(config) {
983
+ if (!config.sources) return { needsRewrite: false };
984
+ if (config.sources.length > 1) return {
985
+ needsRewrite: true,
986
+ reason: "multiple-legacy-sources"
987
+ };
988
+ return {
989
+ needsRewrite: true,
990
+ reason: "legacy-sources"
979
991
  };
980
992
  }
981
993
  async function readStatelyProjectConfig(options = {}) {
982
994
  const rootDir = path.resolve(options.cwd ?? process.cwd());
983
995
  const configPath = path.resolve(rootDir, options.configPath ?? STATELY_CONFIG_FILE);
984
996
  const raw = await fsPromises.readFile(configPath, "utf8");
997
+ const parsed = JSON.parse(raw);
998
+ const config = normalizeProjectConfig(parsed);
999
+ const rewriteStatus = getConfigRewriteStatus(parsed);
1000
+ if (options.rewriteIfNeeded && rewriteStatus.reason === "legacy-sources") {
1001
+ await fsPromises.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
1002
+ return {
1003
+ config,
1004
+ configPath,
1005
+ rootDir,
1006
+ rewriteStatus: {
1007
+ needsRewrite: false,
1008
+ rewritten: true
1009
+ }
1010
+ };
1011
+ }
985
1012
  return {
986
- config: normalizeProjectConfig(JSON.parse(raw)),
1013
+ config,
987
1014
  configPath,
988
- rootDir
1015
+ rootDir,
1016
+ rewriteStatus
989
1017
  };
990
1018
  }
991
1019
  async function walkFiles(rootDir, currentDir = rootDir) {
@@ -1060,6 +1088,9 @@ function matchesGlob(relativePath, pattern) {
1060
1088
  function matchesAny(patterns, relativePath) {
1061
1089
  return (patterns ?? []).some((pattern) => matchesGlob(relativePath, pattern));
1062
1090
  }
1091
+ function isTrackedByStatelyProjectConfig(config, relativePath) {
1092
+ return matchesAny(config.include, relativePath) && !matchesAny(config.exclude ?? DEFAULT_SOURCE_EXCLUDES, relativePath);
1093
+ }
1063
1094
  async function discoverCodeSourceFiles(options = {}) {
1064
1095
  const rootDir = path.resolve(options.cwd ?? process.cwd());
1065
1096
  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 +1188,13 @@ async function discoverStatelySourceFiles(options = {}) {
1157
1188
  //#region src/cli.ts
1158
1189
  const execFileAsync = promisify(execFile);
1159
1190
  const STATELY_API_KEY_SETTINGS_URL = "https://stately.ai/registry/user/my-settings?tab=API+Key";
1191
+ const DEFAULT_NEW_MACHINE_FILE_SUFFIX = ".machine.ts";
1192
+ const ANSI_RESET = "\x1B[0m";
1193
+ const ANSI_BOLD = "\x1B[1m";
1194
+ const ANSI_DIM = "\x1B[2m";
1195
+ const ANSI_CYAN = "\x1B[36m";
1196
+ const ANSI_GREEN = "\x1B[32m";
1197
+ const ANSI_YELLOW = "\x1B[33m";
1160
1198
  function loadLocalEnv() {
1161
1199
  if (typeof process.loadEnvFile !== "function") return;
1162
1200
  const cwdEnvPath = path.join(process.cwd(), ".env.local");
@@ -1310,6 +1348,43 @@ function normalizeApiKey(value) {
1310
1348
  function pluralize(count, singular, plural = `${singular}s`) {
1311
1349
  return count === 1 ? singular : plural;
1312
1350
  }
1351
+ function supportsColor() {
1352
+ return Boolean(process.stdout.isTTY) && process.env.NO_COLOR == null && process.env.CI !== "true";
1353
+ }
1354
+ function colorize(text, ...codes) {
1355
+ if (!supportsColor()) return text;
1356
+ return `${codes.join("")}${text}${ANSI_RESET}`;
1357
+ }
1358
+ function formatSectionLabel(label, tone = "info") {
1359
+ return colorize(label, ANSI_BOLD, tone === "success" ? ANSI_GREEN : tone === "warning" ? ANSI_YELLOW : ANSI_CYAN);
1360
+ }
1361
+ function formatSecondaryText(text) {
1362
+ return colorize(text, ANSI_DIM);
1363
+ }
1364
+ function inferSuggestedNewMachineDir(config) {
1365
+ const include = config.include[0];
1366
+ if (!include) return "src";
1367
+ const groupedRootMatch = include.match(/^(src|packages\/[^/]+\/src|apps\/[^/]+\/src)(?:\/|$)/);
1368
+ if (groupedRootMatch?.[1]) return groupedRootMatch[1];
1369
+ const globIndex = include.indexOf("**/");
1370
+ if (globIndex > 0) return include.slice(0, globIndex).replace(/\/$/, "") || "src";
1371
+ const wildcardIndex = include.indexOf("*");
1372
+ if (wildcardIndex > 0) return path.posix.dirname(include.slice(0, wildcardIndex)) || "src";
1373
+ return path.posix.dirname(include) === "." ? "src" : path.posix.dirname(include);
1374
+ }
1375
+ function formatMissingNewMachineDirMessage(options) {
1376
+ const suggestedDir = inferSuggestedNewMachineDir(options.config);
1377
+ const countLabel = pluralize(options.remoteMachineCount, "remote-only project machine");
1378
+ return [
1379
+ `Found ${options.remoteMachineCount} ${countLabel}, but statelyai.json does not set newMachinesDir, so they will be skipped.`,
1380
+ "To import new machines created in Studio, add this to statelyai.json:",
1381
+ ` "newMachinesDir": "${suggestedDir}"`,
1382
+ "Then run `statelyai pull` again."
1383
+ ].join("\n");
1384
+ }
1385
+ function toMachineFileStem(machineName) {
1386
+ return (typeof machineName === "string" ? machineName : "").trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "machine";
1387
+ }
1313
1388
  function getMissingApiKeyMessage() {
1314
1389
  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
1390
  }
@@ -1321,6 +1396,15 @@ function buildMachineEditorUrl(studioUrl, projectId, machineId) {
1321
1396
  url.searchParams.set("machineId", machineId);
1322
1397
  return url.toString();
1323
1398
  }
1399
+ function inferNewMachineDirFromSuggestions(suggestions) {
1400
+ if (suggestions.length !== 1) return;
1401
+ const include = suggestions[0]?.include[0];
1402
+ if (!include) return;
1403
+ const globIndex = include.indexOf("**/");
1404
+ if (globIndex >= 0) return include.slice(0, globIndex).replace(/\/$/, "") || void 0;
1405
+ const directory = path.posix.dirname(include);
1406
+ return directory === "." ? void 0 : directory;
1407
+ }
1324
1408
  function isRelinkableLinkedMachineError(error) {
1325
1409
  return error instanceof StudioApiError && (error.status === 403 || error.status === 404);
1326
1410
  }
@@ -1400,6 +1484,46 @@ async function discoverLinkedPullTargets(files) {
1400
1484
  }
1401
1485
  return linkedTargets;
1402
1486
  }
1487
+ async function discoverRemoteProjectMachineTargets(options) {
1488
+ const newMachineDir = options.config.newMachinesDir?.trim();
1489
+ if (!newMachineDir) return [];
1490
+ const targets = [];
1491
+ const reservedRelativePaths = /* @__PURE__ */ new Set();
1492
+ for (const machine of options.project.machines) {
1493
+ const candidate = machine;
1494
+ const machineId = typeof candidate.machineId === "string" ? candidate.machineId : typeof candidate.id === "string" ? candidate.id : void 0;
1495
+ if (!machineId || options.linkedMachineIds.has(machineId)) continue;
1496
+ const baseDirectory = path.join(options.rootDir, newMachineDir);
1497
+ let machineName = typeof candidate.name === "string" && candidate.name.trim().length > 0 ? candidate.name : typeof candidate.key === "string" && candidate.key.trim().length > 0 ? candidate.key : void 0;
1498
+ if ((!machineName || machineName === machineId) && options.client?.machines?.get) try {
1499
+ const remoteMachine = await options.client.machines.get(machineId);
1500
+ const fetchedName = typeof remoteMachine?.name === "string" && remoteMachine.name.trim().length > 0 ? remoteMachine.name : void 0;
1501
+ if (fetchedName) machineName = fetchedName;
1502
+ } catch {}
1503
+ machineName ??= machineId;
1504
+ const stem = toMachineFileStem(machineName);
1505
+ let candidateFileName = `${stem}${DEFAULT_NEW_MACHINE_FILE_SUFFIX}`;
1506
+ let attempt = 2;
1507
+ for (;;) {
1508
+ const filePath = path.join(baseDirectory, candidateFileName);
1509
+ const relativePath = path.relative(options.rootDir, filePath).replace(/\\/g, "/");
1510
+ if (!isTrackedByStatelyProjectConfig(options.config, relativePath)) throw new Error(`Generated path ${relativePath} for remote machine ${machineName} is not covered by include/exclude globs.`);
1511
+ if (!reservedRelativePaths.has(relativePath) && !await fileExists(filePath)) {
1512
+ reservedRelativePaths.add(relativePath);
1513
+ targets.push({
1514
+ machineId,
1515
+ machineName,
1516
+ filePath,
1517
+ relativePath
1518
+ });
1519
+ break;
1520
+ }
1521
+ candidateFileName = `${stem}-${attempt}${DEFAULT_NEW_MACHINE_FILE_SUFFIX}`;
1522
+ attempt += 1;
1523
+ }
1524
+ }
1525
+ return targets;
1526
+ }
1403
1527
  async function initProject(options) {
1404
1528
  const cwd = path.resolve(options.cwd ?? process.cwd());
1405
1529
  const studioUrl = getResolvedStudioUrl(options.baseUrl);
@@ -1407,7 +1531,8 @@ async function initProject(options) {
1407
1531
  const defaultXStateVersion = Math.max(5, options.defaultXStateVersion ?? 5);
1408
1532
  const existingConfig = !options.force ? await readStatelyProjectConfig({
1409
1533
  cwd,
1410
- configPath: options.configPath
1534
+ configPath: options.configPath,
1535
+ rewriteIfNeeded: true
1411
1536
  }).catch(() => void 0) : void 0;
1412
1537
  const client = options.client ?? createStatelyClient({
1413
1538
  apiKey: options.apiKey,
@@ -1475,9 +1600,10 @@ function supportsMachineDiscovery(file) {
1475
1600
  return file.source.format === "xstate" || file.source.format === "auto";
1476
1601
  }
1477
1602
  async function resolveConfiguredProject(options) {
1478
- const { config, configPath, rootDir } = await readStatelyProjectConfig({
1603
+ const { config, configPath, rootDir, rewriteStatus } = await readStatelyProjectConfig({
1479
1604
  cwd: options.cwd,
1480
- configPath: options.configPath
1605
+ configPath: options.configPath,
1606
+ rewriteIfNeeded: true
1481
1607
  });
1482
1608
  const studioUrl = options.baseUrl ?? config.studioUrl;
1483
1609
  return {
@@ -1490,7 +1616,37 @@ async function resolveConfiguredProject(options) {
1490
1616
  files: await discoverStatelySourceFiles({
1491
1617
  cwd: rootDir,
1492
1618
  config
1493
- })
1619
+ }),
1620
+ rewriteStatus
1621
+ };
1622
+ }
1623
+ async function resolveConfiguredPullTargets(options) {
1624
+ const { client, files, config, configPath, rewriteStatus } = await resolveConfiguredProject(options);
1625
+ const linkedTargets = await discoverLinkedPullTargets(files);
1626
+ const linkedMachineIds = new Set(linkedTargets.map((linkedTarget) => linkedTarget.machineId));
1627
+ const project = await client.projects.get(config.projectId);
1628
+ const skipped = [];
1629
+ let remoteOnlyTargets = [];
1630
+ try {
1631
+ remoteOnlyTargets = await discoverRemoteProjectMachineTargets({
1632
+ rootDir: path.dirname(configPath),
1633
+ config,
1634
+ project,
1635
+ linkedMachineIds,
1636
+ client
1637
+ });
1638
+ } catch (error) {
1639
+ skipped.push(error instanceof Error ? error.message : String(error));
1640
+ }
1641
+ return {
1642
+ client,
1643
+ config,
1644
+ configPath,
1645
+ linkedTargets,
1646
+ project,
1647
+ remoteOnlyTargets,
1648
+ skipped,
1649
+ rewriteStatus
1494
1650
  };
1495
1651
  }
1496
1652
  const sharedFlags = {
@@ -1608,27 +1764,42 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1608
1764
  const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
1609
1765
  if (!args.source) {
1610
1766
  if (!apiKey) this.error(getMissingApiKeyMessage());
1611
- this.log("Resolving configured project and linked source files...");
1612
- const { files, config } = await resolveConfiguredProject({
1767
+ this.log(colorize("Resolving configured project and linked source files...", ANSI_CYAN));
1768
+ const { config, linkedTargets, project, remoteOnlyTargets, skipped, rewriteStatus } = await resolveConfiguredPullTargets({
1613
1769
  apiKey,
1614
1770
  baseUrl: flags["base-url"],
1615
1771
  configPath: flags.config
1616
1772
  });
1617
- const linkedTargets = await discoverLinkedPullTargets(files);
1618
- if (linkedTargets.length === 0) {
1619
- this.log("No linked local machine files were discovered.");
1773
+ if (rewriteStatus.reason === "multiple-legacy-sources") this.log("Legacy statelyai.json detected with multiple sources entries. It cannot be rewritten automatically without losing information, so please update it manually.");
1774
+ if (linkedTargets.length === 0 && remoteOnlyTargets.length === 0) {
1775
+ if (project.machines.length > 0 && !config.newMachinesDir) {
1776
+ this.log(formatSectionLabel("Skipped:", "warning"));
1777
+ this.log(formatMissingNewMachineDirMessage({
1778
+ config,
1779
+ remoteMachineCount: project.machines.length
1780
+ }));
1781
+ return;
1782
+ }
1783
+ this.log("No linked local machine files or remote-only project machines were discovered.");
1620
1784
  return;
1621
1785
  }
1622
- this.log(`Processing ${linkedTargets.length} ${pluralize(linkedTargets.length, "linked file")}...`);
1786
+ if (project.machines.length > linkedTargets.length && !config.newMachinesDir) {
1787
+ this.log(formatSectionLabel("Skipped:", "warning"));
1788
+ this.log(formatMissingNewMachineDirMessage({
1789
+ config,
1790
+ remoteMachineCount: project.machines.length - linkedTargets.length
1791
+ }));
1792
+ }
1793
+ this.log(colorize(`Processing ${linkedTargets.length} ${pluralize(linkedTargets.length, "linked file")}${remoteOnlyTargets.length > 0 ? ` and ${remoteOnlyTargets.length} ${pluralize(remoteOnlyTargets.length, "new remote machine")}` : ""}...`, ANSI_CYAN));
1623
1794
  const pulled = [];
1624
- const skipped = [];
1795
+ const created = [];
1625
1796
  for (const linkedTarget of linkedTargets) {
1626
1797
  const targetPath = linkedTarget.file.filePath;
1627
1798
  if (!flags.force && await isFileGitDirty(targetPath)) {
1628
1799
  skipped.push(`${linkedTarget.file.relativePath}: local git changes detected (pass --force to overwrite)`);
1629
1800
  continue;
1630
1801
  }
1631
- this.log(`Pulling ${linkedTarget.file.relativePath}...`);
1802
+ this.log(colorize(`Pulling ${linkedTarget.file.relativePath}...`, ANSI_CYAN));
1632
1803
  try {
1633
1804
  const result = await pullSync({
1634
1805
  source: linkedTarget.machineId,
@@ -1636,13 +1807,29 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1636
1807
  apiKey,
1637
1808
  baseUrl: flags["base-url"] ?? config.studioUrl
1638
1809
  });
1639
- pulled.push(`${linkedTarget.file.relativePath}: ${result.source.locator}`);
1810
+ pulled.push(`${linkedTarget.file.relativePath}: ${formatSecondaryText(result.source.locator)}`);
1640
1811
  } catch (error) {
1641
1812
  skipped.push(`${linkedTarget.file.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
1642
1813
  }
1643
1814
  }
1644
- if (pulled.length > 0) this.log(`Pulled:\n${pulled.join("\n")}`);
1645
- if (skipped.length > 0) this.log(`Skipped:\n${skipped.join("\n")}`);
1815
+ for (const remoteTarget of remoteOnlyTargets) {
1816
+ this.log(colorize(`Pulling new remote machine ${remoteTarget.machineName} into ${remoteTarget.relativePath}...`, ANSI_CYAN));
1817
+ try {
1818
+ await fsPromises.mkdir(path.dirname(remoteTarget.filePath), { recursive: true });
1819
+ const result = await pullSync({
1820
+ source: remoteTarget.machineId,
1821
+ target: remoteTarget.filePath,
1822
+ apiKey,
1823
+ baseUrl: flags["base-url"] ?? config.studioUrl
1824
+ });
1825
+ created.push(`${remoteTarget.relativePath}: ${formatSecondaryText(result.source.locator)}`);
1826
+ } catch (error) {
1827
+ skipped.push(`${remoteTarget.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
1828
+ }
1829
+ }
1830
+ if (pulled.length > 0) this.log(`${formatSectionLabel("Pulled:", "success")}\n${pulled.join("\n")}`);
1831
+ if (created.length > 0) this.log(`${formatSectionLabel("Created:", "success")}\n${created.join("\n")}`);
1832
+ if (skipped.length > 0) this.log(`${formatSectionLabel("Skipped:", "warning")}\n${skipped.join("\n")}`);
1646
1833
  return;
1647
1834
  }
1648
1835
  const localCandidate = path.resolve(process.cwd(), args.source);
@@ -1657,14 +1844,14 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1657
1844
  if (!target) this.error("Missing target path. Pass `statelyai pull <machine-id|url> <file>` or `statelyai pull <linked-file>`.");
1658
1845
  if (!apiKey) this.error(getMissingApiKeyMessage());
1659
1846
  if (!flags.force && await fileExists(target) && await isFileGitDirty(target)) this.error(`Refusing to overwrite locally modified file ${target}. Pass --force to overwrite it.`);
1660
- this.log(`Pulling ${source} into ${target}...`);
1847
+ this.log(colorize(`Pulling ${source} into ${target}...`, ANSI_CYAN));
1661
1848
  const result = await pullSync({
1662
1849
  source,
1663
1850
  target,
1664
1851
  apiKey,
1665
1852
  baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
1666
1853
  });
1667
- this.log(`Pulled: ${result.source.locator} -> ${result.outputPath}\nTarget: ${result.target.kind} (${result.target.format})`);
1854
+ this.log(`${formatSectionLabel("Pulled:", "success")} ${formatSecondaryText(result.source.locator)} -> ${result.outputPath}\nTarget: ${result.target.kind} (${result.target.format})`);
1668
1855
  }
1669
1856
  };
1670
1857
  var PushCommand = class PushCommand extends Command {
@@ -1689,22 +1876,27 @@ var PushCommand = class PushCommand extends Command {
1689
1876
  const { args, flags } = await this.parse(PushCommand);
1690
1877
  const resolvedApiKey = flags["dry-run"] ? { source: "missing" } : await resolveApiKey(flags["api-key"]);
1691
1878
  if (!flags["dry-run"] && !resolvedApiKey.apiKey) this.error(getMissingApiKeyMessage());
1692
- this.log("Resolving configured project and source files...");
1693
- const { client, config, files } = flags["dry-run"] ? await (async () => {
1694
- const { config, rootDir } = await readStatelyProjectConfig({ configPath: flags.config });
1879
+ this.log(colorize("Resolving configured project and source files...", ANSI_CYAN));
1880
+ const { client, config, files, rewriteStatus } = flags["dry-run"] ? await (async () => {
1881
+ const { config, rootDir, rewriteStatus } = await readStatelyProjectConfig({
1882
+ configPath: flags.config,
1883
+ rewriteIfNeeded: true
1884
+ });
1695
1885
  return {
1696
1886
  client: void 0,
1697
1887
  config,
1698
1888
  files: await discoverStatelySourceFiles({
1699
1889
  cwd: rootDir,
1700
1890
  config
1701
- })
1891
+ }),
1892
+ rewriteStatus
1702
1893
  };
1703
1894
  })() : await resolveConfiguredProject({
1704
1895
  apiKey: resolvedApiKey.apiKey,
1705
1896
  baseUrl: flags["base-url"],
1706
1897
  configPath: flags.config
1707
1898
  });
1899
+ if (rewriteStatus.reason === "multiple-legacy-sources") this.log("Legacy statelyai.json detected with multiple sources entries. It cannot be rewritten automatically without losing information, so please update it manually.");
1708
1900
  const candidateFiles = args.file ? [{
1709
1901
  filePath: path.resolve(args.file),
1710
1902
  relativePath: path.relative(process.cwd(), path.resolve(args.file)),
@@ -1716,33 +1908,33 @@ var PushCommand = class PushCommand extends Command {
1716
1908
  }
1717
1909
  }] : files.filter(supportsMachineDiscovery);
1718
1910
  if (candidateFiles.length === 0) {
1719
- this.log("No matching local machine source files were discovered.");
1911
+ this.log(colorize("No matching local machine source files were discovered.", ANSI_YELLOW));
1720
1912
  return;
1721
1913
  }
1722
1914
  const { machineFiles } = await classifyPushCandidates(candidateFiles);
1723
1915
  if (machineFiles.length === 0) {
1724
- this.log(`Matched ${candidateFiles.length} ${pluralize(candidateFiles.length, "file")}, but no local machine sources were discovered.`);
1916
+ this.log(colorize(`Matched ${candidateFiles.length} ${pluralize(candidateFiles.length, "file")}, but no local machine sources were discovered.`, ANSI_YELLOW));
1725
1917
  return;
1726
1918
  }
1727
- this.log(`Discovered ${machineFiles.length} ${pluralize(machineFiles.length, "file with a machine")} from ${candidateFiles.length} matched ${pluralize(candidateFiles.length, "file")}.`);
1919
+ this.log(colorize(`Discovered ${machineFiles.length} ${pluralize(machineFiles.length, "file with a machine")} from ${candidateFiles.length} matched ${pluralize(candidateFiles.length, "file")}.`, ANSI_CYAN));
1728
1920
  if (flags["dry-run"]) {
1729
1921
  const wouldLink = [];
1730
1922
  const wouldUpdate = [];
1731
1923
  for (const file of machineFiles) {
1732
1924
  const pragma = getStatelyPragma(await fsPromises.readFile(file.filePath, "utf8"), file.filePath);
1733
- if (pragma?.id) wouldUpdate.push(`${file.relativePath}: ${pragma.id}`);
1925
+ if (pragma?.id) wouldUpdate.push(`${file.relativePath}: ${formatSecondaryText(pragma.id)}`);
1734
1926
  else wouldLink.push(file.relativePath);
1735
1927
  }
1736
- if (wouldLink.length > 0) this.log(`Would link:\n${wouldLink.join("\n")}`);
1737
- if (wouldUpdate.length > 0) this.log(`Would update:\n${wouldUpdate.join("\n")}`);
1928
+ if (wouldLink.length > 0) this.log(`${formatSectionLabel("Would link:")}\n${wouldLink.join("\n")}`);
1929
+ if (wouldUpdate.length > 0) this.log(`${formatSectionLabel("Would update:")}\n${wouldUpdate.join("\n")}`);
1738
1930
  return;
1739
1931
  }
1740
- this.log(`Processing ${machineFiles.length} ${pluralize(machineFiles.length, "source file")}...`);
1932
+ this.log(colorize(`Processing ${machineFiles.length} ${pluralize(machineFiles.length, "source file")}...`, ANSI_CYAN));
1741
1933
  const linked = [];
1742
1934
  const refreshed = [];
1743
1935
  const skipped = [];
1744
1936
  for (const file of machineFiles) {
1745
- this.log(`Pushing ${file.relativePath}...`);
1937
+ this.log(colorize(`Pushing ${file.relativePath}...`, ANSI_CYAN));
1746
1938
  if (!supportsMachineDiscovery(file)) {
1747
1939
  skipped.push(`${file.relativePath}: unsupported format ${file.source.format}`);
1748
1940
  continue;
@@ -1767,7 +1959,7 @@ var PushCommand = class PushCommand extends Command {
1767
1959
  skipped.push(`${file.relativePath}: linked remote machine is missing or inaccessible.`);
1768
1960
  continue;
1769
1961
  }
1770
- this.log(`Relinking ${file.relativePath} as a new remote machine...`);
1962
+ this.log(colorize(`Relinking ${file.relativePath} as a new remote machine...`, ANSI_YELLOW));
1771
1963
  result = await pushLocalMachineLinks({
1772
1964
  source: file.filePath,
1773
1965
  apiKey: resolvedApiKey.apiKey,
@@ -1779,13 +1971,13 @@ var PushCommand = class PushCommand extends Command {
1779
1971
  });
1780
1972
  } else throw error;
1781
1973
  }
1782
- if (result.created.length > 0) linked.push(`${file.relativePath}: ${result.created.map(({ machineIndex, machine }) => `${machine.id} [${machineIndex}] ${buildMachineEditorUrl(config.studioUrl, config.projectId, machine.id)}`).join(", ")}`);
1783
- if (result.updated.length > 0) refreshed.push(`${file.relativePath}: ${result.updated.map(({ machineIndex, machine }) => `${machine.id} [${machineIndex}] ${buildMachineEditorUrl(config.studioUrl, config.projectId, machine.id)}`).join(", ")}`);
1974
+ if (result.created.length > 0) linked.push(`${file.relativePath}: ${result.created.map(({ machineIndex, machine }) => `${formatSecondaryText(`${machine.id} [${machineIndex}]`)} ${formatSecondaryText(buildMachineEditorUrl(config.studioUrl, config.projectId, machine.id))}`).join(", ")}`);
1975
+ if (result.updated.length > 0) refreshed.push(`${file.relativePath}: ${result.updated.map(({ machineIndex, machine }) => `${formatSecondaryText(`${machine.id} [${machineIndex}]`)} ${formatSecondaryText(buildMachineEditorUrl(config.studioUrl, config.projectId, machine.id))}`).join(", ")}`);
1784
1976
  for (const entry of result.skipped) skipped.push(`${file.relativePath} [${entry.machineIndex}]: ${entry.reason}`);
1785
1977
  }
1786
- if (linked.length > 0) this.log(`Linked:\n${linked.join("\n")}`);
1787
- if (refreshed.length > 0) this.log(`Updated:\n${refreshed.join("\n")}`);
1788
- if (skipped.length > 0) this.log(`Skipped:\n${skipped.join("\n")}`);
1978
+ if (linked.length > 0) this.log(`${formatSectionLabel("Linked:", "success")}\n${linked.join("\n")}`);
1979
+ if (refreshed.length > 0) this.log(`${formatSectionLabel("Updated:", "success")}\n${refreshed.join("\n")}`);
1980
+ if (skipped.length > 0) this.log(`${formatSectionLabel("Skipped:", "warning")}\n${skipped.join("\n")}`);
1789
1981
  }
1790
1982
  };
1791
1983
  var OpenCommand = class OpenCommand extends Command {
@@ -1889,13 +2081,15 @@ var InitCommand = class InitCommand extends Command {
1889
2081
  this.log(initMessage);
1890
2082
  this.log(`Suggested source globs:\n${suggestions.map((source) => `- ${source.include.join(", ")}`).join("\n")}`);
1891
2083
  if (await promptYesNo("Save these source globs to statelyai.json?", true)) {
2084
+ const inferredNewMachineDir = inferNewMachineDirFromSuggestions(suggestions);
1892
2085
  const nextConfig = {
1893
2086
  ...result.config,
1894
2087
  include: suggestions.flatMap((suggestion) => suggestion.include),
1895
- exclude: [...new Set(suggestions.flatMap((suggestion) => suggestion.exclude ?? []))]
2088
+ exclude: [...new Set(suggestions.flatMap((suggestion) => suggestion.exclude ?? []))],
2089
+ ...inferredNewMachineDir ? { newMachinesDir: inferredNewMachineDir } : {}
1896
2090
  };
1897
2091
  await fsPromises.writeFile(result.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
1898
- this.log("Saved scanned source globs to statelyai.json.");
2092
+ 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
2093
  } else this.log("Left statelyai.json unchanged. Edit it before running statelyai push.");
1900
2094
  return;
1901
2095
  }
@@ -1991,4 +2185,4 @@ function isDirectExecution() {
1991
2185
  if (isDirectExecution()) run$1();
1992
2186
 
1993
2187
  //#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 };
2188
+ export { formatMissingNewMachineDirMessage as a, inferInitProjectName as c, resolveApiKey as d, resolveConfiguredPullTargets as f, createStatelyProjectConfig as h, discoverRemoteProjectMachineTargets as i, initProject as l, scanProjectSources as m, classifyPushCandidates as n, formatPlanSummary as o, run$1 as p, discoverLinkedPullTargets as r, getEnvApiKey as s, COMMANDS as t, isFileGitDirty 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
+ newMachinesDir?: string;
21
22
  }
22
23
  interface DiscoveredSourceFile {
23
24
  filePath: string;
@@ -55,10 +56,29 @@ 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[];
61
68
  }
69
+ interface ResolvedConfiguredPullTargets {
70
+ client: StudioClient;
71
+ config: StatelyProjectConfig;
72
+ configPath: string;
73
+ linkedTargets: LinkedPullTarget[];
74
+ project: ProjectData;
75
+ remoteOnlyTargets: RemoteProjectMachineTarget[];
76
+ skipped: string[];
77
+ rewriteStatus: {
78
+ needsRewrite: boolean;
79
+ reason?: string;
80
+ };
81
+ }
62
82
  type ApiKeyResolution = {
63
83
  apiKey: string;
64
84
  detail: string;
@@ -74,11 +94,29 @@ declare function getEnvApiKey(): {
74
94
  } | undefined;
75
95
  declare function resolveApiKey(explicitApiKey?: string): Promise<ApiKeyResolution>;
76
96
  declare function inferInitProjectName(cwd: string, repo?: ConnectedRepo): string;
97
+ declare function formatMissingNewMachineDirMessage(options: {
98
+ config: Pick<StatelyProjectConfig, 'include' | 'newMachinesDir'>;
99
+ remoteMachineCount: number;
100
+ }): string;
77
101
  declare function isFileGitDirty(filePath: string): Promise<boolean>;
78
102
  declare function discoverLinkedPullTargets(files: DiscoveredSourceFile[]): Promise<LinkedPullTarget[]>;
103
+ declare function discoverRemoteProjectMachineTargets(options: {
104
+ rootDir: string;
105
+ config: StatelyProjectConfig;
106
+ project: ProjectData;
107
+ linkedMachineIds: Set<string>;
108
+ client?: Pick<StudioClient, 'machines'>;
109
+ }): Promise<RemoteProjectMachineTarget[]>;
79
110
  declare function initProject(options: InitProjectOptions): Promise<InitProjectResult>;
80
111
  declare function scanProjectSources(options: ScanProjectSourcesOptions): Promise<Array<Pick<StatelyProjectConfig, 'include' | 'exclude'>>>;
81
112
  declare function classifyPushCandidates(files: DiscoveredSourceFile[]): Promise<PushCandidateClassification>;
113
+ declare function resolveConfiguredPullTargets(options: {
114
+ cwd?: string;
115
+ configPath?: string;
116
+ apiKey: string;
117
+ baseUrl?: string;
118
+ client?: StudioClient;
119
+ }): Promise<ResolvedConfiguredPullTargets>;
82
120
  declare function formatPlanSummary(plan: SyncPlan): string;
83
121
  declare abstract class BaseSyncCommand extends Command {
84
122
  static enableJsonFlag: boolean;
@@ -227,4 +265,4 @@ declare const COMMANDS: {
227
265
  };
228
266
  declare function run(argv?: any, entryUrl?: string): Promise<void>;
229
267
  //#endregion
230
- export { ApiKeyResolution, COMMANDS, InitProjectOptions, InitProjectResult, LinkedPullTarget, PushCandidateClassification, ScanProjectSourcesOptions, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, run, scanProjectSources };
268
+ export { ApiKeyResolution, COMMANDS, InitProjectOptions, InitProjectResult, LinkedPullTarget, PushCandidateClassification, RemoteProjectMachineTarget, ResolvedConfiguredPullTargets, ScanProjectSourcesOptions, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverRemoteProjectMachineTargets, formatMissingNewMachineDirMessage, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, 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 formatMissingNewMachineDirMessage, c as inferInitProjectName, d as resolveApiKey, f as resolveConfiguredPullTargets, h as createStatelyProjectConfig, i as discoverRemoteProjectMachineTargets, l as initProject, m as scanProjectSources, n as classifyPushCandidates, o as formatPlanSummary, p as run, r as discoverLinkedPullTargets, s as getEnvApiKey, t as COMMANDS, u as isFileGitDirty } from "./cli-D4Id6WsS.mjs";
2
2
 
3
- export { COMMANDS, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, run, scanProjectSources };
3
+ export { COMMANDS, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverRemoteProjectMachineTargets, formatMissingNewMachineDirMessage, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, 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.1",
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.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsdown": "0.21.0-beta.2",