statelyai 0.5.0 → 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
@@ -37,7 +39,7 @@ That writes a `statelyai.json` like:
37
39
  "defaultXStateVersion": 5,
38
40
  "include": ["src/**/*.ts"],
39
41
  "exclude": ["**/*.test.*", "**/*.spec.*"],
40
- "newMachineDir": "src"
42
+ "newMachinesDir": "src"
41
43
  }
42
44
  ```
43
45
 
@@ -55,13 +57,13 @@ statelyai push --dry-run
55
57
 
56
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.
57
59
 
58
- 6. Pull remote changes back into linked local files and create new local files for remote-only project machines when `newMachineDir` is configured:
60
+ 6. Pull remote changes back into linked local files and create new local files for remote-only project machines when `newMachinesDir` is configured:
59
61
 
60
62
  ```bash
61
63
  statelyai pull
62
64
  ```
63
65
 
64
- `pull` skips locally modified linked files unless you pass `--force`. New remote-only machines are written as `<machine-name>.machine.ts` inside `newMachineDir`.
66
+ `pull` skips locally modified linked files unless you pass `--force`. New remote-only machines are written as `<machine-name>.machine.ts` inside `newMachinesDir`.
65
67
 
66
68
  Run it without installing it globally:
67
69
 
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { d as run } from "./cli-1Nrsh5Xl.mjs";
2
+ import { p as run } from "./cli-D4Id6WsS.mjs";
3
3
 
4
4
  //#region src/bin.ts
5
5
  run();
@@ -976,17 +976,44 @@ function normalizeProjectConfig(config) {
976
976
  defaultXStateVersion: Math.max(5, config.defaultXStateVersion ?? 5),
977
977
  include: [...config.include ?? firstSource?.include ?? []],
978
978
  exclude: config.exclude == null ? [...firstSource?.exclude ?? DEFAULT_SOURCE_EXCLUDES] : [...config.exclude],
979
- ...config.newMachineDir ? { newMachineDir: config.newMachineDir } : {}
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"
980
991
  };
981
992
  }
982
993
  async function readStatelyProjectConfig(options = {}) {
983
994
  const rootDir = path.resolve(options.cwd ?? process.cwd());
984
995
  const configPath = path.resolve(rootDir, options.configPath ?? STATELY_CONFIG_FILE);
985
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
+ }
986
1012
  return {
987
- config: normalizeProjectConfig(JSON.parse(raw)),
1013
+ config,
988
1014
  configPath,
989
- rootDir
1015
+ rootDir,
1016
+ rewriteStatus
990
1017
  };
991
1018
  }
992
1019
  async function walkFiles(rootDir, currentDir = rootDir) {
@@ -1162,6 +1189,12 @@ async function discoverStatelySourceFiles(options = {}) {
1162
1189
  const execFileAsync = promisify(execFile);
1163
1190
  const STATELY_API_KEY_SETTINGS_URL = "https://stately.ai/registry/user/my-settings?tab=API+Key";
1164
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";
1165
1198
  function loadLocalEnv() {
1166
1199
  if (typeof process.loadEnvFile !== "function") return;
1167
1200
  const cwdEnvPath = path.join(process.cwd(), ".env.local");
@@ -1315,8 +1348,42 @@ function normalizeApiKey(value) {
1315
1348
  function pluralize(count, singular, plural = `${singular}s`) {
1316
1349
  return count === 1 ? singular : plural;
1317
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
+ }
1318
1385
  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";
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";
1320
1387
  }
1321
1388
  function getMissingApiKeyMessage() {
1322
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}`;
@@ -1418,23 +1485,34 @@ async function discoverLinkedPullTargets(files) {
1418
1485
  return linkedTargets;
1419
1486
  }
1420
1487
  async function discoverRemoteProjectMachineTargets(options) {
1421
- const newMachineDir = options.config.newMachineDir?.trim();
1488
+ const newMachineDir = options.config.newMachinesDir?.trim();
1422
1489
  if (!newMachineDir) return [];
1423
1490
  const targets = [];
1491
+ const reservedRelativePaths = /* @__PURE__ */ new Set();
1424
1492
  for (const machine of options.project.machines) {
1425
- if (options.linkedMachineIds.has(machine.machineId)) continue;
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;
1426
1496
  const baseDirectory = path.join(options.rootDir, newMachineDir);
1427
- const stem = toMachineFileStem(machine.name);
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);
1428
1505
  let candidateFileName = `${stem}${DEFAULT_NEW_MACHINE_FILE_SUFFIX}`;
1429
1506
  let attempt = 2;
1430
1507
  for (;;) {
1431
1508
  const filePath = path.join(baseDirectory, candidateFileName);
1432
1509
  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)) {
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);
1435
1513
  targets.push({
1436
- machineId: machine.machineId,
1437
- machineName: machine.name,
1514
+ machineId,
1515
+ machineName,
1438
1516
  filePath,
1439
1517
  relativePath
1440
1518
  });
@@ -1453,7 +1531,8 @@ async function initProject(options) {
1453
1531
  const defaultXStateVersion = Math.max(5, options.defaultXStateVersion ?? 5);
1454
1532
  const existingConfig = !options.force ? await readStatelyProjectConfig({
1455
1533
  cwd,
1456
- configPath: options.configPath
1534
+ configPath: options.configPath,
1535
+ rewriteIfNeeded: true
1457
1536
  }).catch(() => void 0) : void 0;
1458
1537
  const client = options.client ?? createStatelyClient({
1459
1538
  apiKey: options.apiKey,
@@ -1521,9 +1600,10 @@ function supportsMachineDiscovery(file) {
1521
1600
  return file.source.format === "xstate" || file.source.format === "auto";
1522
1601
  }
1523
1602
  async function resolveConfiguredProject(options) {
1524
- const { config, configPath, rootDir } = await readStatelyProjectConfig({
1603
+ const { config, configPath, rootDir, rewriteStatus } = await readStatelyProjectConfig({
1525
1604
  cwd: options.cwd,
1526
- configPath: options.configPath
1605
+ configPath: options.configPath,
1606
+ rewriteIfNeeded: true
1527
1607
  });
1528
1608
  const studioUrl = options.baseUrl ?? config.studioUrl;
1529
1609
  return {
@@ -1536,7 +1616,37 @@ async function resolveConfiguredProject(options) {
1536
1616
  files: await discoverStatelySourceFiles({
1537
1617
  cwd: rootDir,
1538
1618
  config
1539
- })
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
1540
1650
  };
1541
1651
  }
1542
1652
  const sharedFlags = {
@@ -1654,37 +1764,33 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1654
1764
  const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
1655
1765
  if (!args.source) {
1656
1766
  if (!apiKey) this.error(getMissingApiKeyMessage());
1657
- this.log("Resolving configured project and linked source files...");
1658
- const { files, config, configPath } = 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({
1659
1769
  apiKey,
1660
1770
  baseUrl: flags["base-url"],
1661
1771
  configPath: flags.config
1662
1772
  });
1663
- const linkedTargets = await discoverLinkedPullTargets(files);
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
- }
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.");
1678
1774
  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.");
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
+ }));
1681
1781
  return;
1682
1782
  }
1683
1783
  this.log("No linked local machine files or remote-only project machines were discovered.");
1684
1784
  return;
1685
1785
  }
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")}` : ""}...`);
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));
1688
1794
  const pulled = [];
1689
1795
  const created = [];
1690
1796
  for (const linkedTarget of linkedTargets) {
@@ -1693,7 +1799,7 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1693
1799
  skipped.push(`${linkedTarget.file.relativePath}: local git changes detected (pass --force to overwrite)`);
1694
1800
  continue;
1695
1801
  }
1696
- this.log(`Pulling ${linkedTarget.file.relativePath}...`);
1802
+ this.log(colorize(`Pulling ${linkedTarget.file.relativePath}...`, ANSI_CYAN));
1697
1803
  try {
1698
1804
  const result = await pullSync({
1699
1805
  source: linkedTarget.machineId,
@@ -1701,13 +1807,13 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1701
1807
  apiKey,
1702
1808
  baseUrl: flags["base-url"] ?? config.studioUrl
1703
1809
  });
1704
- pulled.push(`${linkedTarget.file.relativePath}: ${result.source.locator}`);
1810
+ pulled.push(`${linkedTarget.file.relativePath}: ${formatSecondaryText(result.source.locator)}`);
1705
1811
  } catch (error) {
1706
1812
  skipped.push(`${linkedTarget.file.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
1707
1813
  }
1708
1814
  }
1709
1815
  for (const remoteTarget of remoteOnlyTargets) {
1710
- this.log(`Pulling new remote machine ${remoteTarget.machineName} into ${remoteTarget.relativePath}...`);
1816
+ this.log(colorize(`Pulling new remote machine ${remoteTarget.machineName} into ${remoteTarget.relativePath}...`, ANSI_CYAN));
1711
1817
  try {
1712
1818
  await fsPromises.mkdir(path.dirname(remoteTarget.filePath), { recursive: true });
1713
1819
  const result = await pullSync({
@@ -1716,14 +1822,14 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1716
1822
  apiKey,
1717
1823
  baseUrl: flags["base-url"] ?? config.studioUrl
1718
1824
  });
1719
- created.push(`${remoteTarget.relativePath}: ${result.source.locator}`);
1825
+ created.push(`${remoteTarget.relativePath}: ${formatSecondaryText(result.source.locator)}`);
1720
1826
  } catch (error) {
1721
1827
  skipped.push(`${remoteTarget.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
1722
1828
  }
1723
1829
  }
1724
- if (pulled.length > 0) this.log(`Pulled:\n${pulled.join("\n")}`);
1725
- if (created.length > 0) this.log(`Created:\n${created.join("\n")}`);
1726
- if (skipped.length > 0) this.log(`Skipped:\n${skipped.join("\n")}`);
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")}`);
1727
1833
  return;
1728
1834
  }
1729
1835
  const localCandidate = path.resolve(process.cwd(), args.source);
@@ -1738,14 +1844,14 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1738
1844
  if (!target) this.error("Missing target path. Pass `statelyai pull <machine-id|url> <file>` or `statelyai pull <linked-file>`.");
1739
1845
  if (!apiKey) this.error(getMissingApiKeyMessage());
1740
1846
  if (!flags.force && await fileExists(target) && await isFileGitDirty(target)) this.error(`Refusing to overwrite locally modified file ${target}. Pass --force to overwrite it.`);
1741
- this.log(`Pulling ${source} into ${target}...`);
1847
+ this.log(colorize(`Pulling ${source} into ${target}...`, ANSI_CYAN));
1742
1848
  const result = await pullSync({
1743
1849
  source,
1744
1850
  target,
1745
1851
  apiKey,
1746
1852
  baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
1747
1853
  });
1748
- 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})`);
1749
1855
  }
1750
1856
  };
1751
1857
  var PushCommand = class PushCommand extends Command {
@@ -1770,22 +1876,27 @@ var PushCommand = class PushCommand extends Command {
1770
1876
  const { args, flags } = await this.parse(PushCommand);
1771
1877
  const resolvedApiKey = flags["dry-run"] ? { source: "missing" } : await resolveApiKey(flags["api-key"]);
1772
1878
  if (!flags["dry-run"] && !resolvedApiKey.apiKey) this.error(getMissingApiKeyMessage());
1773
- this.log("Resolving configured project and source files...");
1774
- const { client, config, files } = flags["dry-run"] ? await (async () => {
1775
- 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
+ });
1776
1885
  return {
1777
1886
  client: void 0,
1778
1887
  config,
1779
1888
  files: await discoverStatelySourceFiles({
1780
1889
  cwd: rootDir,
1781
1890
  config
1782
- })
1891
+ }),
1892
+ rewriteStatus
1783
1893
  };
1784
1894
  })() : await resolveConfiguredProject({
1785
1895
  apiKey: resolvedApiKey.apiKey,
1786
1896
  baseUrl: flags["base-url"],
1787
1897
  configPath: flags.config
1788
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.");
1789
1900
  const candidateFiles = args.file ? [{
1790
1901
  filePath: path.resolve(args.file),
1791
1902
  relativePath: path.relative(process.cwd(), path.resolve(args.file)),
@@ -1797,33 +1908,33 @@ var PushCommand = class PushCommand extends Command {
1797
1908
  }
1798
1909
  }] : files.filter(supportsMachineDiscovery);
1799
1910
  if (candidateFiles.length === 0) {
1800
- this.log("No matching local machine source files were discovered.");
1911
+ this.log(colorize("No matching local machine source files were discovered.", ANSI_YELLOW));
1801
1912
  return;
1802
1913
  }
1803
1914
  const { machineFiles } = await classifyPushCandidates(candidateFiles);
1804
1915
  if (machineFiles.length === 0) {
1805
- 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));
1806
1917
  return;
1807
1918
  }
1808
- 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));
1809
1920
  if (flags["dry-run"]) {
1810
1921
  const wouldLink = [];
1811
1922
  const wouldUpdate = [];
1812
1923
  for (const file of machineFiles) {
1813
1924
  const pragma = getStatelyPragma(await fsPromises.readFile(file.filePath, "utf8"), file.filePath);
1814
- if (pragma?.id) wouldUpdate.push(`${file.relativePath}: ${pragma.id}`);
1925
+ if (pragma?.id) wouldUpdate.push(`${file.relativePath}: ${formatSecondaryText(pragma.id)}`);
1815
1926
  else wouldLink.push(file.relativePath);
1816
1927
  }
1817
- if (wouldLink.length > 0) this.log(`Would link:\n${wouldLink.join("\n")}`);
1818
- 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")}`);
1819
1930
  return;
1820
1931
  }
1821
- this.log(`Processing ${machineFiles.length} ${pluralize(machineFiles.length, "source file")}...`);
1932
+ this.log(colorize(`Processing ${machineFiles.length} ${pluralize(machineFiles.length, "source file")}...`, ANSI_CYAN));
1822
1933
  const linked = [];
1823
1934
  const refreshed = [];
1824
1935
  const skipped = [];
1825
1936
  for (const file of machineFiles) {
1826
- this.log(`Pushing ${file.relativePath}...`);
1937
+ this.log(colorize(`Pushing ${file.relativePath}...`, ANSI_CYAN));
1827
1938
  if (!supportsMachineDiscovery(file)) {
1828
1939
  skipped.push(`${file.relativePath}: unsupported format ${file.source.format}`);
1829
1940
  continue;
@@ -1848,7 +1959,7 @@ var PushCommand = class PushCommand extends Command {
1848
1959
  skipped.push(`${file.relativePath}: linked remote machine is missing or inaccessible.`);
1849
1960
  continue;
1850
1961
  }
1851
- this.log(`Relinking ${file.relativePath} as a new remote machine...`);
1962
+ this.log(colorize(`Relinking ${file.relativePath} as a new remote machine...`, ANSI_YELLOW));
1852
1963
  result = await pushLocalMachineLinks({
1853
1964
  source: file.filePath,
1854
1965
  apiKey: resolvedApiKey.apiKey,
@@ -1860,13 +1971,13 @@ var PushCommand = class PushCommand extends Command {
1860
1971
  });
1861
1972
  } else throw error;
1862
1973
  }
1863
- 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(", ")}`);
1864
- 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(", ")}`);
1865
1976
  for (const entry of result.skipped) skipped.push(`${file.relativePath} [${entry.machineIndex}]: ${entry.reason}`);
1866
1977
  }
1867
- if (linked.length > 0) this.log(`Linked:\n${linked.join("\n")}`);
1868
- if (refreshed.length > 0) this.log(`Updated:\n${refreshed.join("\n")}`);
1869
- 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")}`);
1870
1981
  }
1871
1982
  };
1872
1983
  var OpenCommand = class OpenCommand extends Command {
@@ -1975,7 +2086,7 @@ var InitCommand = class InitCommand extends Command {
1975
2086
  ...result.config,
1976
2087
  include: suggestions.flatMap((suggestion) => suggestion.include),
1977
2088
  exclude: [...new Set(suggestions.flatMap((suggestion) => suggestion.exclude ?? []))],
1978
- ...inferredNewMachineDir ? { newMachineDir: inferredNewMachineDir } : {}
2089
+ ...inferredNewMachineDir ? { newMachinesDir: inferredNewMachineDir } : {}
1979
2090
  };
1980
2091
  await fsPromises.writeFile(result.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
1981
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.");
@@ -2074,4 +2185,4 @@ function isDirectExecution() {
2074
2185
  if (isDirectExecution()) run$1();
2075
2186
 
2076
2187
  //#endregion
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 };
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,7 +18,7 @@ interface StatelyProjectConfig {
18
18
  defaultXStateVersion: number;
19
19
  include: string[];
20
20
  exclude?: string[];
21
- newMachineDir?: string;
21
+ newMachinesDir?: string;
22
22
  }
23
23
  interface DiscoveredSourceFile {
24
24
  filePath: string;
@@ -66,6 +66,19 @@ interface PushCandidateClassification {
66
66
  machineFiles: DiscoveredSourceFile[];
67
67
  nonMachineFiles: DiscoveredSourceFile[];
68
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
+ }
69
82
  type ApiKeyResolution = {
70
83
  apiKey: string;
71
84
  detail: string;
@@ -81,6 +94,10 @@ declare function getEnvApiKey(): {
81
94
  } | undefined;
82
95
  declare function resolveApiKey(explicitApiKey?: string): Promise<ApiKeyResolution>;
83
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;
84
101
  declare function isFileGitDirty(filePath: string): Promise<boolean>;
85
102
  declare function discoverLinkedPullTargets(files: DiscoveredSourceFile[]): Promise<LinkedPullTarget[]>;
86
103
  declare function discoverRemoteProjectMachineTargets(options: {
@@ -88,10 +105,18 @@ declare function discoverRemoteProjectMachineTargets(options: {
88
105
  config: StatelyProjectConfig;
89
106
  project: ProjectData;
90
107
  linkedMachineIds: Set<string>;
108
+ client?: Pick<StudioClient, 'machines'>;
91
109
  }): Promise<RemoteProjectMachineTarget[]>;
92
110
  declare function initProject(options: InitProjectOptions): Promise<InitProjectResult>;
93
111
  declare function scanProjectSources(options: ScanProjectSourcesOptions): Promise<Array<Pick<StatelyProjectConfig, 'include' | 'exclude'>>>;
94
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>;
95
120
  declare function formatPlanSummary(plan: SyncPlan): string;
96
121
  declare abstract class BaseSyncCommand extends Command {
97
122
  static enableJsonFlag: boolean;
@@ -240,4 +265,4 @@ declare const COMMANDS: {
240
265
  };
241
266
  declare function run(argv?: any, entryUrl?: string): Promise<void>;
242
267
  //#endregion
243
- export { ApiKeyResolution, COMMANDS, InitProjectOptions, InitProjectResult, LinkedPullTarget, PushCandidateClassification, RemoteProjectMachineTarget, ScanProjectSourcesOptions, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverRemoteProjectMachineTargets, 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 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";
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, discoverRemoteProjectMachineTargets, 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.5.0",
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.10.0"
24
+ "@statelyai/sdk": "0.10.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsdown": "0.21.0-beta.2",